device-monitor.vue 95 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613
  1. <template>
  2. <div class="device-monitor-container">
  3. <!-- 控制面板 -->
  4. <div class="control-panel">
  5. <div class="control-left">
  6. <div class="form-group">
  7. <label>农场选择</label>
  8. <select v-model="selectedFarm">
  9. <option value="all">全部农场</option>
  10. <option value="farm1">东湖智慧农场</option>
  11. <option value="farm2">西湖有机农场</option>
  12. </select>
  13. </div>
  14. <div class="form-group">
  15. <label>地块选择</label>
  16. <select v-model="selectedArea">
  17. <option value="all">全部地块</option>
  18. <option value="area1">1号地块</option>
  19. <option value="area2">2号地块</option>
  20. </select>
  21. </div>
  22. <div class="form-group">
  23. <label>时间范围</label>
  24. <select v-model="timeRange">
  25. <option value="realtime">实时数据</option>
  26. <option value="1h">近1小时</option>
  27. <option value="24h">近24小时</option>
  28. </select>
  29. </div>
  30. </div>
  31. <div class="control-right">
  32. <button class="refresh-btn" @click="refreshData">
  33. <i class="icon-refresh"></i>
  34. 刷新数据
  35. </button>
  36. <div class="auto-refresh">
  37. <span>自动刷新:</span>
  38. <select v-model="autoRefresh" class="auto-refresh-select">
  39. <option value="15">15秒</option>
  40. <option value="30">30秒</option>
  41. <option value="60">60秒</option>
  42. </select>
  43. </div>
  44. <div class="last-update">
  45. 最后更新:<span>{{ lastUpdateTime }}</span>
  46. </div>
  47. </div>
  48. </div>
  49. <!-- 设备统计 -->
  50. <div class="stats-container">
  51. <!-- 综合统计卡片 -->
  52. <div class="overview-stat-card">
  53. <div class="overview-stats">
  54. <div class="overview-item">
  55. <div class="stat-number online">42</div>
  56. <div class="stat-label">在线设备</div>
  57. </div>
  58. <div class="overview-item">
  59. <div class="stat-number offline">3</div>
  60. <div class="stat-label">离线设备</div>
  61. </div>
  62. <div class="overview-item">
  63. <div class="stat-number alert">5</div>
  64. <div class="stat-label">告警设备</div>
  65. </div>
  66. </div>
  67. </div>
  68. <!-- 分类统计卡片 -->
  69. <div class="category-stats">
  70. <div class="stat-card">
  71. <div class="stat-number camera">{{ cameraList.length }}</div>
  72. <div class="stat-label">摄像头</div>
  73. <div class="stat-detail">{{ getOnlineCameraCount() }}在线 / {{ getOfflineCameraCount() }}离线</div>
  74. </div>
  75. <div class="stat-card">
  76. <div class="stat-number sensor">{{ deviceList.length }}</div>
  77. <div class="stat-label">传感器</div>
  78. <div class="stat-detail">{{ getDeviceCountByStatus('online') }}在线 / {{ getDeviceCountByStatus('warning') }}告警</div>
  79. </div>
  80. <div class="stat-card">
  81. <div class="stat-number weather">8</div>
  82. <div class="stat-label">气象设备</div>
  83. <div class="stat-detail">7在线 / 1离线</div>
  84. </div>
  85. <div class="stat-card">
  86. <div class="stat-number control">10</div>
  87. <div class="stat-label">控制设备</div>
  88. <div class="stat-detail">10在线</div>
  89. </div>
  90. <div class="stat-card">
  91. <div class="stat-number alert">7</div>
  92. <div class="stat-label">今日告警</div>
  93. <div class="stat-detail"><span class="processed">5已处理</span></div>
  94. </div>
  95. </div>
  96. </div>
  97. <!-- 主要内容区域 -->
  98. <div class="main-content">
  99. <!-- 视频监控 -->
  100. <div class="content-section">
  101. <div class="video-header">
  102. <div class="video-header-left">
  103. <h2 class="section-title">视频监控</h2>
  104. <div class="video-count">
  105. 共 {{ cameraList.length }} 个摄像头
  106. </div>
  107. </div>
  108. <div class="video-header-right">
  109. <div class="video-nav-controls">
  110. <button class="video-nav-btn" @click="prevPage" :disabled="pageIndex === 1" title="上一页">
  111. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  112. <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" />
  113. </svg>
  114. </button>
  115. <div class="page-indicator">
  116. <span>{{ getPageRange() }} / {{ cameraList.length }}</span>
  117. </div>
  118. <button class="video-nav-btn" @click="nextPage" :disabled="pageIndex === totalCameraPages" title="下一页">
  119. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  120. <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" />
  121. </svg>
  122. </button>
  123. </div>
  124. <button class="video-nav-btn" @click="toggleFullscreen" title="全屏显示">
  125. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  126. <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" />
  127. </svg>
  128. </button>
  129. </div>
  130. </div>
  131. <!-- 视频卡片网格 -->
  132. <div class="camera-grid">
  133. <div
  134. class="camera-card"
  135. v-for="cam in pageCameras"
  136. :key="cam.id"
  137. >
  138. <div class="card-header">
  139. <span class="title">{{ cam.name }}</span>
  140. <el-tag size="mini" :type="cam.status === '在线' ? 'success' : 'info'">
  141. {{ cam.status || '离线' }}
  142. </el-tag>
  143. </div>
  144. <!-- 预览区:优先使用 cam.previewUrl(iframe),否则渲染厂商 SDK 容器 cam.domId;若都没有,显示占位 -->
  145. <div class="card-preview">
  146. <iframe
  147. v-if="cam.previewUrl && cam.status === '在线'"
  148. class="player-iframe"
  149. :src="cam.previewUrl"
  150. frameborder="0"
  151. allow="autoplay; encrypted-media"
  152. allowfullscreen
  153. ></iframe>
  154. <div
  155. v-else-if="cam.domId && cam.status === '在线'"
  156. class="player-sdk"
  157. :id="cam.domId"
  158. ></div>
  159. <!-- 离线状态显示 -->
  160. <div v-else-if="cam.status === '离线'" class="player-offline">
  161. <svg class="offline-camera-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  162. <path d="M23 7l-7 5 7 5V7z"/>
  163. <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
  164. <line x1="1" y1="1" x2="23" y2="23"/>
  165. </svg>
  166. <span class="offline-text">设备离线</span>
  167. </div>
  168. <!-- 其他情况显示占位 -->
  169. <div v-else class="player-empty">
  170. <i class="el-icon-video-camera"></i>
  171. <span>暂无预览</span>
  172. </div>
  173. </div>
  174. <div class="card-footer">
  175. <span class="location">{{ cam.location || '-' }}</span>
  176. </div>
  177. </div>
  178. </div>
  179. </div>
  180. <!-- 设备监控 -->
  181. <div class="content-section">
  182. <!-- 顶部控制栏 -->
  183. <div class="device-monitor-header">
  184. <div class="device-monitor-title-section">
  185. <h2 class="section-title">设备监控</h2>
  186. <div class="filter-buttons">
  187. <button class="filter-btn" :class="{ active: deviceFilter === 'all' }" @click="setFilter('all')">
  188. 全部设备
  189. <span class="filter-count">({{ deviceList.length }})</span>
  190. </button>
  191. <button class="filter-btn" :class="{ active: deviceFilter === 'warning' }" @click="setFilter('warning')">
  192. <span class="status-dot warning"></span>
  193. 告警设备
  194. <span class="filter-count">({{ getDeviceCountByStatus('warning') }})</span>
  195. </button>
  196. <button class="filter-btn" :class="{ active: deviceFilter === 'offline' }" @click="setFilter('offline')">
  197. <span class="status-dot offline"></span>
  198. 离线设备
  199. <span class="filter-count">({{ getDeviceCountByStatus('offline') }})</span>
  200. </button>
  201. </div>
  202. <div class="filter-divider"></div>
  203. <div class="type-filters">
  204. <button class="filter-btn" :class="{ active: deviceTypeFilter === 'soil' }" @click="setTypeFilter('soil')">土壤监测</button>
  205. <button class="filter-btn" :class="{ active: deviceTypeFilter === 'water' }" @click="setTypeFilter('water')">水质监测</button>
  206. <button class="filter-btn" :class="{ active: deviceTypeFilter === 'weather' }" @click="setTypeFilter('weather')">气象监测</button>
  207. </div>
  208. </div>
  209. <div class="device-monitor-controls">
  210. <select class="sort-select" v-model="sortBy">
  211. <option value="alert">告警优先</option>
  212. <option value="time">更新时间</option>
  213. <option value="name">设备名称</option>
  214. </select>
  215. <div class="device-pagination">
  216. <button class="page-btn" @click="prevDevicePage" :disabled="deviceCurrentPage === 1">
  217. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  218. <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" />
  219. </svg>
  220. </button>
  221. <span class="page-indicator">{{ getDevicePageRange() }} / {{ filteredDevices.length }}</span>
  222. <button class="page-btn" @click="nextDevicePage" :disabled="deviceCurrentPage === deviceTotalPages">
  223. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  224. <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" />
  225. </svg>
  226. </button>
  227. </div>
  228. </div>
  229. </div>
  230. <!-- 设备卡片网格 -->
  231. <div class="device-grid">
  232. <div v-for="device in paginatedDevices" :key="device.id" class="device-card" :class="device.status">
  233. <!-- 右上角数据按钮 -->
  234. <button class="data-btn" @click="showDeviceHistory(device)" title="查看数据">
  235. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  236. <path d="M12 20v-6M6 20V10M18 20V4"/>
  237. </svg>
  238. </button>
  239. <div class="device-header">
  240. <div class="device-title-row">
  241. <div class="device-name">{{ device.name }}</div>
  242. <div class="device-status" :class="device.status">{{ device.statusText }}</div>
  243. </div>
  244. </div>
  245. <div class="device-location">{{ device.location }}</div>
  246. <div class="device-metrics"
  247. @mouseenter="pauseCarousel(device.id)"
  248. @mouseleave="resumeCarousel(device.id)">
  249. <div class="metrics-container">
  250. <transition name="flip" mode="out-in">
  251. <div class="metric-pair" :key="getCurrentMetricIndex(device.id)">
  252. <div class="metric">
  253. <div class="metric-label">{{ getCurrentMetrics(device)[0].name }}</div>
  254. <div class="metric-value" :class="{
  255. 'metric-warning': device.status === 'warning' && getCurrentMetrics(device)[0].isAlert,
  256. 'metric-alert': getCurrentMetrics(device)[0].isAlert
  257. }">
  258. {{ getCurrentMetrics(device)[0].value }}{{ getCurrentMetrics(device)[0].unit }}
  259. </div>
  260. </div>
  261. <div class="metric" v-if="getCurrentMetrics(device)[1]">
  262. <div class="metric-label">{{ getCurrentMetrics(device)[1].name }}</div>
  263. <div class="metric-value" :class="{
  264. 'metric-warning': device.status === 'warning' && getCurrentMetrics(device)[1].isAlert,
  265. 'metric-alert': getCurrentMetrics(device)[1].isAlert
  266. }">
  267. {{ getCurrentMetrics(device)[1].value }}{{ getCurrentMetrics(device)[1].unit }}
  268. </div>
  269. </div>
  270. </div>
  271. </transition>
  272. </div>
  273. </div>
  274. <div class="device-footer">
  275. <div class="last-update" :class="{ 'text-warning': device.status === 'warning', 'text-offline': device.status === 'offline' }">
  276. {{ device.lastUpdate }}
  277. </div>
  278. <button class="detail-btn" :class="device.status">
  279. {{ device.status === 'warning' ? '处理告警' : '查看详情' }}
  280. </button>
  281. </div>
  282. </div>
  283. </div>
  284. </div>
  285. </div>
  286. <!-- 摄像头预览对话框 -->
  287. <CameraPreview
  288. v-model="previewVisible"
  289. :camera="currentCamera"
  290. />
  291. <!-- 单个摄像头全屏模式 -->
  292. <div v-if="singleCameraFullscreen.show" class="single-camera-fullscreen">
  293. <div class="video-area">
  294. <div class="fullscreen-video-container">
  295. <!-- 左上角摄像头名称 -->
  296. <div class="camera-title-overlay" v-if="singleCameraFullscreen.camera">
  297. <h3 class="camera-title-text">{{ singleCameraFullscreen.camera.name }}</h3>
  298. </div>
  299. <!-- 右上角关闭按钮 -->
  300. <div class="close-button-overlay">
  301. <button class="video-nav-btn" @click="exitSingleFullscreen" title="退出全屏">
  302. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  303. <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"/>
  304. </svg>
  305. </button>
  306. </div>
  307. <!-- 视频画面区域 -->
  308. <div class="fullscreen-video-preview">
  309. <svg class="fullscreen-camera-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  310. <path d="M23 7l-7 5 7 5V7z"/>
  311. <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
  312. </svg>
  313. </div>
  314. </div>
  315. </div>
  316. <div class="control-panel">
  317. <div class="control-sections">
  318. <!-- 云台控制 -->
  319. <div class="control-section">
  320. <h4 class="control-title">云台控制</h4>
  321. <div class="ptz-controls">
  322. <button class="ptz-btn" title="左上">↖</button>
  323. <button class="ptz-btn" title="向上">↑</button>
  324. <button class="ptz-btn" title="右上">↗</button>
  325. <button class="ptz-btn" title="向左">←</button>
  326. <button class="ptz-btn" title="停止">●</button>
  327. <button class="ptz-btn" title="向右">→</button>
  328. <button class="ptz-btn" title="左下">↙</button>
  329. <button class="ptz-btn" title="向下">↓</button>
  330. <button class="ptz-btn" title="右下">↘</button>
  331. </div>
  332. </div>
  333. <!-- 变倍控制 -->
  334. <div class="control-section">
  335. <h4 class="control-title">变倍控制</h4>
  336. <div class="zoom-controls">
  337. <button class="ptz-btn zoom-btn" title="放大">
  338. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  339. <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"/>
  340. </svg>
  341. </button>
  342. <button class="ptz-btn zoom-btn" title="缩小">
  343. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  344. <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"/>
  345. </svg>
  346. </button>
  347. </div>
  348. </div>
  349. <!-- 预置位 -->
  350. <div class="control-section">
  351. <h4 class="control-title">预置位</h4>
  352. <div class="preset-grid">
  353. <button v-for="n in 6" :key="n" class="preset-btn">位置{{ n }}</button>
  354. </div>
  355. </div>
  356. <!-- 回放控制 -->
  357. <div class="control-section">
  358. <h4 class="control-title">回放控制</h4>
  359. <div class="playback-controls">
  360. <button class="ptz-btn playback-btn" title="开始回放">
  361. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  362. <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"/>
  363. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
  364. </svg>
  365. </button>
  366. <button class="ptz-btn playback-btn" title="暂停回放">
  367. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  368. <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"/>
  369. </svg>
  370. </button>
  371. </div>
  372. </div>
  373. <!-- 其他控制 -->
  374. <div class="control-section">
  375. <h4 class="control-title">其他控制</h4>
  376. <div class="other-controls">
  377. <button class="ptz-btn other-btn" title="截图">
  378. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  379. <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"/>
  380. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"/>
  381. </svg>
  382. </button>
  383. <button class="ptz-btn other-btn" title="录制">
  384. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  385. <circle cx="12" cy="12" r="10" stroke-width="2"/>
  386. <circle cx="12" cy="12" r="3" fill="currentColor"/>
  387. </svg>
  388. </button>
  389. </div>
  390. </div>
  391. </div>
  392. </div>
  393. </div>
  394. <!-- 网格全屏模式 -->
  395. <div v-if="gridFullscreen.show" class="grid-fullscreen-mode">
  396. <div class="fullscreen-header">
  397. <div class="fullscreen-title">
  398. <h2 class="fullscreen-title-text">视频监控</h2>
  399. <div class="fullscreen-subtitle">共 {{ cameraList.length }} 个摄像头</div>
  400. </div>
  401. <!-- 分页控制 -->
  402. <div class="fullscreen-pagination" v-if="gridTotalPages > 1">
  403. <button class="fullscreen-nav-btn" @click="gridPrevPage" :disabled="gridFullscreen.currentPage === 1" title="上一页">
  404. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  405. <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" />
  406. </svg>
  407. </button>
  408. <div class="fullscreen-page-indicator">
  409. <span>{{ gridPageRange }} / {{ cameraList.length }}</span>
  410. <div class="fullscreen-page-dots">
  411. <span
  412. v-for="page in gridTotalPages"
  413. :key="page"
  414. class="page-dot"
  415. :class="{ active: page === gridFullscreen.currentPage }"
  416. @click="gridFullscreen.currentPage = page"
  417. ></span>
  418. </div>
  419. </div>
  420. <button class="fullscreen-nav-btn" @click="gridNextPage" :disabled="gridFullscreen.currentPage === gridTotalPages" title="下一页">
  421. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  422. <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" />
  423. </svg>
  424. </button>
  425. </div>
  426. <div class="fullscreen-controls">
  427. <!-- 键盘快捷键提示 -->
  428. <div class="keyboard-hints" v-if="gridTotalPages > 1">
  429. <div class="hint-item">
  430. <kbd>←</kbd><kbd>→</kbd> 翻页
  431. </div>
  432. <div class="hint-item">
  433. <kbd>1-9</kbd> 跳转
  434. </div>
  435. <div class="hint-item">
  436. <kbd>ESC</kbd> 退出
  437. </div>
  438. </div>
  439. <button class="fullscreen-close-btn" @click="exitGridFullscreen" title="退出全屏 (ESC)">
  440. <svg class="w-6 h-6" viewBox="0 0 20 20" fill="currentColor">
  441. <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"/>
  442. </svg>
  443. </button>
  444. </div>
  445. </div>
  446. <div class="fullscreen-video-grid">
  447. <div v-for="camera in gridPaginatedVideos" :key="camera.id" class="fullscreen-video-card">
  448. <button class="fullscreen-camera-btn" @click="openVideoFullscreen(camera)" title="单独查看">
  449. <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
  450. <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" />
  451. </svg>
  452. </button>
  453. <div class="fullscreen-video-preview">
  454. <div class="fullscreen-camera-placeholder">
  455. <svg class="fullscreen-camera-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  456. <path d="M23 7l-7 5 7 5V7z"/>
  457. <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
  458. </svg>
  459. </div>
  460. <div class="fullscreen-video-info">
  461. <div class="fullscreen-video-details">
  462. <div class="fullscreen-video-name">{{ camera.name }}</div>
  463. <div class="fullscreen-video-location">{{ camera.location }}</div>
  464. </div>
  465. <span class="fullscreen-video-status" :class="camera.status === '在线' ? 'online' : 'offline'">
  466. {{ camera.status === '在线' ? '在线' : '离线' }}
  467. </span>
  468. </div>
  469. </div>
  470. </div>
  471. </div>
  472. </div>
  473. <!-- 设备历史数据模态框 -->
  474. <div v-if="historyModal.show" class="history-modal-backdrop" @click="closeHistoryModal">
  475. <div class="history-modal-content" @click.stop>
  476. <!-- 模态框头部 -->
  477. <div class="history-modal-header">
  478. <div class="history-modal-title-section">
  479. <h3 class="history-modal-title">{{ historyModal.device.name }} - 历史数据</h3>
  480. <span class="device-status" :class="historyModal.device.status">{{ historyModal.device.statusText }}</span>
  481. </div>
  482. <button class="history-modal-close" @click="closeHistoryModal">
  483. <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  484. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
  485. </svg>
  486. </button>
  487. </div>
  488. <!-- 图表控制区域 -->
  489. <div class="history-modal-controls">
  490. <div class="time-range-buttons">
  491. <button
  492. v-for="range in timeRanges"
  493. :key="range.value"
  494. class="time-range-btn"
  495. :class="{ active: historyModal.timeRange === range.value }"
  496. @click="setTimeRange(range.value)"
  497. >
  498. {{ range.label }}
  499. </button>
  500. </div>
  501. <div class="indicator-buttons">
  502. <button
  503. v-for="indicator in getDeviceIndicators()"
  504. :key="indicator.value"
  505. class="indicator-btn"
  506. :class="{ active: historyModal.activeIndicator === indicator.value }"
  507. @click="setActiveIndicator(indicator.value)"
  508. >
  509. {{ indicator.label }}
  510. </button>
  511. </div>
  512. </div>
  513. <!-- 图表区域 -->
  514. <div class="history-modal-body">
  515. <div ref="chartContainer" class="chart-container"></div>
  516. </div>
  517. </div>
  518. </div>
  519. </div>
  520. </template>
  521. <script>
  522. import { playback, stopPlaying } from "@/api/base/device"
  523. export default {
  524. name: 'DeviceMonitor',
  525. data() {
  526. return {
  527. selectedFarm: 'all',
  528. selectedArea: 'all',
  529. timeRange: 'realtime',
  530. autoRefresh: '30',
  531. lastUpdateTime: '刚刚',
  532. // 摄像头相关
  533. cameraList: [],
  534. pageIndex: 1,
  535. pageSize: 3,
  536. // 设备监控相关
  537. deviceFilter: 'all',
  538. deviceTypeFilter: 'all',
  539. sortBy: 'alert',
  540. deviceCurrentPage: 1,
  541. devicePerPage: 8,
  542. iframeSrc: '',
  543. // 预览对话框状态
  544. previewVisible: false,
  545. currentCamera: null,
  546. // 单个摄像头全屏模式
  547. singleCameraFullscreen: {
  548. show: false,
  549. camera: null
  550. },
  551. // 网格全屏模式
  552. gridFullscreen: {
  553. show: false,
  554. currentPage: 1,
  555. videosPerPage: 6
  556. },
  557. // 历史数据模态框
  558. historyModal: {
  559. show: false,
  560. device: null,
  561. timeRange: '24h',
  562. activeIndicator: 'all'
  563. },
  564. // 时间范围选项
  565. timeRanges: [
  566. { label: '近24小时', value: '24h' },
  567. { label: '近7天', value: '7d' },
  568. { label: '近30天', value: '30d' }
  569. ],
  570. // 图表实例
  571. chartInstance: null,
  572. // 原始样式保存
  573. originalBodyBg: '',
  574. originalBodyColor: '',
  575. originalAppBg: '',
  576. // 轮播控制状态
  577. carouselStates: {},
  578. carouselTimers: {},
  579. deviceList: [
  580. {
  581. id: 1,
  582. name: '土壤气象监测器 #1',
  583. location: '第一家葡萄园',
  584. status: 'online',
  585. statusText: '在线',
  586. lastUpdate: '2分钟前更新',
  587. type: 'soil',
  588. metrics: [
  589. { name: '气温', value: '30.5', unit: '℃', isAlert: false },
  590. { name: '湿度', value: '93.1', unit: '%', isAlert: false },
  591. { name: '降雨量', value: '0.6', unit: 'mm', isAlert: false },
  592. { name: '风向', value: '北', unit: '', isAlert: false },
  593. { name: '风速', value: '1.2', unit: 'm/s', isAlert: false },
  594. { name: '气压', value: '994.00', unit: 'hPa', isAlert: false },
  595. { name: '光照强度', value: '-230.8', unit: 'lux', isAlert: false },
  596. { name: '土壤湿度', value: '22.10', unit: '%', isAlert: false },
  597. { name: '土壤温度', value: '27.10', unit: '℃', isAlert: false },
  598. { name: '氮含量', value: '1', unit: 'mg/kg', isAlert: false },
  599. { name: '磷含量', value: '1', unit: 'mg/kg', isAlert: false },
  600. { name: '钾含量', value: '0', unit: 'mg/kg', isAlert: false }
  601. ]
  602. },
  603. /* {
  604. id: 2,
  605. name: '水质监测器 #2',
  606. location: '西区1号地块',
  607. status: 'warning',
  608. statusText: '告警',
  609. lastUpdate: 'pH值超标',
  610. type: 'water',
  611. metrics: [
  612. { name: 'pH值', value: '8.5', unit: '', isAlert: true },
  613. { name: '溶解氧', value: '6.2', unit: 'mg/L', isAlert: false },
  614. { name: '电导率', value: '1.2', unit: 'mS/cm', isAlert: false },
  615. { name: '浊度', value: '15', unit: 'NTU', isAlert: false },
  616. { name: '氨氮', value: '0.8', unit: 'mg/L', isAlert: false }
  617. ]
  618. }, */
  619. {
  620. id: 3,
  621. name: '气象土壤监测器 #2',
  622. location: '东区气象站',
  623. status: 'online',
  624. statusText: '在线',
  625. lastUpdate: '1分钟前更新',
  626. type: 'weather',
  627. metrics: [
  628. { name: '温度', value: '26.5', unit: '℃', isAlert: false },
  629. { name: '湿度', value: '68', unit: '%', isAlert: false },
  630. { name: '光照强度', value: '45000', unit: 'lux', isAlert: false },
  631. { name: '风速', value: '2.3', unit: 'm/s', isAlert: false },
  632. { name: '降雨量', value: '0', unit: 'mm', isAlert: false },
  633. { name: '气压', value: '1013', unit: 'hPa', isAlert: false }
  634. ]
  635. },
  636. /*{
  637. id: 4,
  638. name: '土壤监测器 #4',
  639. location: '西区2号地块',
  640. status: 'offline',
  641. statusText: '离线',
  642. lastUpdate: '设备离线 > 24h',
  643. type: 'soil',
  644. metrics: [
  645. { name: '土壤湿度', value: '--', unit: '%', isAlert: false },
  646. { name: '土壤温度', value: '--', unit: '℃', isAlert: false }
  647. ]
  648. },
  649. {
  650. id: 5,
  651. name: '水质监测器 #3',
  652. location: '东区2号地块',
  653. status: 'online',
  654. statusText: '在线',
  655. lastUpdate: '5分钟前更新',
  656. type: 'water',
  657. metrics: [
  658. { name: 'pH值', value: '7.2', unit: '', isAlert: false },
  659. { name: '溶解氧', value: '5.8', unit: 'mg/L', isAlert: false },
  660. { name: '电导率', value: '0.9', unit: 'mS/cm', isAlert: false },
  661. { name: '浊度', value: '8', unit: 'NTU', isAlert: false }
  662. ]
  663. },
  664. {
  665. id: 6,
  666. name: '气象监测器 #2',
  667. location: '西区气象站',
  668. status: 'warning',
  669. statusText: '告警',
  670. lastUpdate: '温度过高',
  671. type: 'weather',
  672. metrics: [
  673. { name: '温度', value: '35.8', unit: '℃', isAlert: true },
  674. { name: '湿度', value: '45', unit: '%', isAlert: false },
  675. { name: '光照强度', value: '52000', unit: 'lux', isAlert: false },
  676. { name: '风速', value: '4.2', unit: 'm/s', isAlert: false },
  677. { name: '降雨量', value: '2.5', unit: 'mm', isAlert: false }
  678. ]
  679. },
  680. {
  681. id: 7,
  682. name: '土壤监测器 #5',
  683. location: '南区1号地块',
  684. status: 'online',
  685. statusText: '在线',
  686. lastUpdate: '3分钟前更新',
  687. type: 'soil',
  688. metrics: [
  689. { name: '土壤湿度', value: '28.5', unit: '%', isAlert: false },
  690. { name: '土壤温度', value: '22.8', unit: '℃', isAlert: false },
  691. { name: 'EC值', value: '2.1', unit: 'mS/cm', isAlert: false },
  692. { name: 'P含量', value: '38', unit: 'mg/kg', isAlert: false },
  693. { name: 'K含量', value: '125', unit: 'mg/kg', isAlert: false }
  694. ]
  695. },
  696. {
  697. id: 8,
  698. name: '水质监测器 #4',
  699. location: '南区水质站',
  700. status: 'online',
  701. statusText: '在线',
  702. lastUpdate: '1分钟前更新',
  703. type: 'water',
  704. metrics: [
  705. { name: 'pH值', value: '7.1', unit: '', isAlert: false },
  706. { name: '溶解氧', value: '6.5', unit: 'mg/L', isAlert: false },
  707. { name: '电导率', value: '1.1', unit: 'mS/cm', isAlert: false },
  708. { name: '温度', value: '23.5', unit: '℃', isAlert: false },
  709. { name: '浊度', value: '5', unit: 'NTU', isAlert: false }
  710. ]
  711. },
  712. {
  713. id: 9,
  714. name: '土壤监测器 #6',
  715. location: '北区1号地块',
  716. status: 'offline',
  717. statusText: '离线',
  718. lastUpdate: '设备离线 > 12h',
  719. type: 'soil',
  720. metrics: [
  721. { name: '土壤湿度', value: '--', unit: '%', isAlert: false },
  722. { name: '土壤温度', value: '--', unit: '℃', isAlert: false }
  723. ]
  724. },
  725. {
  726. id: 10,
  727. name: '气象监测器 #3',
  728. location: '北区气象站',
  729. status: 'online',
  730. statusText: '在线',
  731. lastUpdate: '30秒前更新',
  732. type: 'weather',
  733. metrics: [
  734. { name: '温度', value: '28.2', unit: '℃', isAlert: false },
  735. { name: '湿度', value: '72', unit: '%', isAlert: false },
  736. { name: '光照强度', value: '38000', unit: 'lux', isAlert: false },
  737. { name: '风速', value: '1.8', unit: 'm/s', isAlert: false },
  738. { name: '气压', value: '1015', unit: 'hPa', isAlert: false },
  739. { name: 'UV指数', value: '8', unit: '', isAlert: false }
  740. ]
  741. } */
  742. ]
  743. }
  744. },
  745. computed: {
  746. // 摄像头分页
  747. pageCameras() {
  748. const start = (this.pageIndex - 1) * this.pageSize
  749. const end = start + this.pageSize
  750. return this.cameraList.slice(start, end)
  751. },
  752. totalCameraPages() {
  753. return Math.ceil(this.cameraList.length / this.pageSize)
  754. },
  755. // 网格全屏模式计算属性
  756. gridTotalPages() {
  757. return Math.ceil(this.cameraList.length / this.gridFullscreen.videosPerPage)
  758. },
  759. gridPaginatedVideos() {
  760. const start = (this.gridFullscreen.currentPage - 1) * this.gridFullscreen.videosPerPage
  761. const end = start + this.gridFullscreen.videosPerPage
  762. return this.cameraList.slice(start, end)
  763. },
  764. gridPageRange() {
  765. const start = (this.gridFullscreen.currentPage - 1) * this.gridFullscreen.videosPerPage + 1
  766. const end = Math.min(this.gridFullscreen.currentPage * this.gridFullscreen.videosPerPage, this.cameraList.length)
  767. return `${start}-${end}`
  768. },
  769. // 设备监控计算属性
  770. filteredDevices() {
  771. let devices = this.deviceList
  772. // 按状态过滤
  773. if (this.deviceFilter !== 'all') {
  774. devices = devices.filter(device => device.status === this.deviceFilter)
  775. }
  776. // 按类型过滤
  777. if (this.deviceTypeFilter !== 'all') {
  778. devices = devices.filter(device => device.type === this.deviceTypeFilter)
  779. }
  780. // 排序
  781. if (this.sortBy === 'alert') {
  782. devices = devices.sort((a, b) => {
  783. const statusOrder = { warning: 0, offline: 1, online: 2 }
  784. return statusOrder[a.status] - statusOrder[b.status]
  785. })
  786. } else if (this.sortBy === 'name') {
  787. devices = devices.sort((a, b) => a.name.localeCompare(b.name))
  788. } else if (this.sortBy === 'time') {
  789. devices = devices.sort((a, b) => {
  790. // 简单的时间排序逻辑,实际应该根据真实时间戳
  791. const getUpdateTime = (update) => {
  792. if (update.includes('分钟前')) return parseInt(update.match(/\d+/)[0])
  793. if (update.includes('刚刚')) return 0
  794. return 999 // 其他情况放最后
  795. }
  796. return getUpdateTime(a.lastUpdate) - getUpdateTime(b.lastUpdate)
  797. })
  798. }
  799. return devices
  800. },
  801. deviceTotalPages() {
  802. return Math.ceil(this.filteredDevices.length / this.devicePerPage)
  803. },
  804. paginatedDevices() {
  805. const start = (this.deviceCurrentPage - 1) * this.devicePerPage
  806. const end = start + this.devicePerPage
  807. return this.filteredDevices.slice(start, end)
  808. }
  809. },
  810. methods: {
  811. refreshData() {
  812. console.log('刷新数据')
  813. this.lastUpdateTime = '刚刚'
  814. // 这里可以添加实际的数据刷新逻辑
  815. },
  816. prevPage() {
  817. if (this.pageIndex > 1) {
  818. this.pageIndex--
  819. this.$nextTick(() => {
  820. this.initSdkPlayer()
  821. })
  822. }
  823. },
  824. nextPage() {
  825. if (this.pageIndex < this.totalCameraPages) {
  826. this.pageIndex++
  827. this.$nextTick(() => {
  828. this.initSdkPlayer()
  829. })
  830. }
  831. },
  832. getPageRange() {
  833. const start = (this.pageIndex - 1) * this.pageSize + 1
  834. const end = Math.min(this.pageIndex * this.pageSize, this.cameraList.length)
  835. return `${start}-${end}`
  836. },
  837. toggleFullscreen() {
  838. console.log('切换网格全屏模式')
  839. this.gridFullscreen.show = true
  840. },
  841. exitGridFullscreen() {
  842. this.gridFullscreen.show = false
  843. this.gridFullscreen.currentPage = 1
  844. },
  845. // 网格全屏分页方法
  846. gridPrevPage() {
  847. if (this.gridFullscreen.currentPage > 1) {
  848. this.gridFullscreen.currentPage--
  849. }
  850. },
  851. gridNextPage() {
  852. if (this.gridFullscreen.currentPage < this.gridTotalPages) {
  853. this.gridFullscreen.currentPage++
  854. }
  855. },
  856. openVideoFullscreen(camera) {
  857. console.log('打开单个视频全屏:', camera.name)
  858. console.log('Camera data:', camera)
  859. // 如果是从网格全屏模式进入,先关闭网格全屏
  860. if (this.gridFullscreen.show) {
  861. this.gridFullscreen.show = false
  862. }
  863. this.singleCameraFullscreen.show = true
  864. this.singleCameraFullscreen.camera = camera
  865. // 确保数据更新后再强制更新视图
  866. this.$nextTick(() => {
  867. console.log('Fullscreen camera:', this.singleCameraFullscreen.camera)
  868. })
  869. },
  870. exitSingleFullscreen() {
  871. this.singleCameraFullscreen.show = false
  872. this.singleCameraFullscreen.camera = null
  873. },
  874. // 设备监控方法
  875. setFilter(filter) {
  876. this.deviceFilter = filter
  877. this.deviceCurrentPage = 1 // 重置到第一页
  878. },
  879. setTypeFilter(type) {
  880. this.deviceTypeFilter = this.deviceTypeFilter === type ? 'all' : type
  881. this.deviceCurrentPage = 1 // 重置到第一页
  882. },
  883. getDeviceCountByStatus(status) {
  884. return this.deviceList.filter(device => device.status === status).length
  885. },
  886. // 摄像头统计方法
  887. getOnlineCameraCount() {
  888. return this.cameraList.filter(camera => camera.status === '在线').length
  889. },
  890. getOfflineCameraCount() {
  891. return this.cameraList.filter(camera => camera.status === '离线').length
  892. },
  893. // 打开摄像头预览
  894. openCameraPreview(camera) {
  895. this.currentCamera = camera
  896. this.previewVisible = true
  897. },
  898. // 获取摄像头列表
  899. async fetchCameras() {
  900. try {
  901. // 这里调用真实接口
  902. // const response = await getCameraList()
  903. // this.cameraList = response.data.map(item => ({
  904. // id: item.id,
  905. // name: item.name,
  906. // status: item.status === 1 ? '在线' : '离线',
  907. // location: item.location,
  908. // previewUrl: item.previewUrl,
  909. // domId: item.domId
  910. // }))
  911. // 兜底 mock 数据
  912. this.cameraList = [
  913. {
  914. id: 1,
  915. name: '合和智行',
  916. status: '在线',
  917. location: '智慧农场六号',
  918. previewUrl: 'http://121.4.16.100:28080/#/play/wasm/' + encodeURIComponent('ws://121.4.16.100:6080/rtp/34020000001110000001_34020000001320000012.live.flv'),
  919. domId: 'camera-1'
  920. },
  921. {
  922. id: 2,
  923. name: '葡萄园一号摄像头',
  924. status: '在线',
  925. location: '智慧农场六号',
  926. previewUrl: 'http://121.4.16.100:28080/#/play/wasm/' + encodeURIComponent('ws://121.4.16.100:6080/rtp/34020000001320000001.live.flv'),
  927. domId: 'camera-2'
  928. },
  929. {
  930. id: 3,
  931. name: '东区3号摄像头',
  932. status: '离线',
  933. location: '东区3号地块',
  934. previewUrl: '',
  935. domId: 'camera-3'
  936. },
  937. {
  938. id: 4,
  939. name: '西区1号摄像头',
  940. status: '在线',
  941. location: '西区1号地块',
  942. previewUrl: '',
  943. domId: 'camera-4'
  944. },
  945. {
  946. id: 5,
  947. name: '西区2号摄像头',
  948. status: '在线',
  949. location: '西区2号地块',
  950. previewUrl: '',
  951. domId: 'camera-5'
  952. },
  953. {
  954. id: 6,
  955. name: '西区3号摄像头',
  956. status: '在线',
  957. location: '西区3号地块',
  958. previewUrl: '',
  959. domId: 'camera-6'
  960. },
  961. {
  962. id: 7,
  963. name: '南区1号摄像头',
  964. status: '在线',
  965. location: '南区1号地块',
  966. previewUrl: '',
  967. domId: 'camera-7'
  968. },
  969. {
  970. id: 8,
  971. name: '南区2号摄像头',
  972. status: '离线',
  973. location: '南区2号地块',
  974. previewUrl: '',
  975. domId: 'camera-8'
  976. },
  977. {
  978. id: 9,
  979. name: '南区3号摄像头',
  980. status: '在线',
  981. location: '南区3号地块',
  982. previewUrl: '',
  983. domId: 'camera-9'
  984. },
  985. {
  986. id: 10,
  987. name: '北区1号摄像头',
  988. status: '在线',
  989. location: '北区1号地块',
  990. previewUrl: '',
  991. domId: 'camera-10'
  992. },
  993. {
  994. id: 11,
  995. name: '北区2号摄像头',
  996. status: '在线',
  997. location: '北区2号地块',
  998. previewUrl: '',
  999. domId: 'camera-11'
  1000. },
  1001. {
  1002. id: 12,
  1003. name: '北区3号摄像头',
  1004. status: '在线',
  1005. location: '北区3号地块',
  1006. previewUrl: '',
  1007. domId: 'camera-12'
  1008. }
  1009. ]
  1010. // 初始化当前页的 SDK 播放器
  1011. this.$nextTick(() => {
  1012. this.initSdkPlayer()
  1013. })
  1014. } catch (error) {
  1015. console.error('获取摄像头列表失败:', error)
  1016. // 接口超时兜底 mock
  1017. this.cameraList = [
  1018. { id: 1, name: '东区1号摄像头', status: '在线', location: '东区1号地块', previewUrl: '', domId: 'camera-1' },
  1019. { id: 2, name: '东区2号摄像头', status: '在线', location: '东区2号地块', previewUrl: '', domId: 'camera-2' },
  1020. { id: 3, name: '东区3号摄像头', status: '离线', location: '东区3号地块', previewUrl: '', domId: 'camera-3' }
  1021. ]
  1022. }
  1023. },
  1024. // 初始化 SDK 播放器
  1025. initSdkPlayer() {
  1026. this.pageCameras.forEach(cam => {
  1027. if (cam.domId && cam.status === '在线' && !cam.previewUrl) {
  1028. // 这里调用厂商 SDK 初始化逻辑
  1029. // 例如:initHikvisionPlayer(cam.domId, cam.streamUrl)
  1030. console.log(`初始化摄像头 ${cam.id} 的 SDK 播放器,容器ID: ${cam.domId}`)
  1031. }
  1032. })
  1033. },
  1034. // 处理预览
  1035. handlePreview(cam) {
  1036. this.currentCamera = {
  1037. id: cam.id,
  1038. name: cam.name,
  1039. location: cam.location,
  1040. status: cam.status === '在线' ? 'online' : 'offline'
  1041. }
  1042. this.previewVisible = true
  1043. },
  1044. // 处理全屏
  1045. handleFullscreen(cam) {
  1046. // 简化实现:通过打开对话框实现
  1047. this.handlePreview(cam)
  1048. },
  1049. prevDevicePage() {
  1050. if (this.deviceCurrentPage > 1) {
  1051. this.deviceCurrentPage--
  1052. }
  1053. },
  1054. nextDevicePage() {
  1055. if (this.deviceCurrentPage < this.deviceTotalPages) {
  1056. this.deviceCurrentPage++
  1057. }
  1058. },
  1059. getDevicePageRange() {
  1060. if (this.filteredDevices.length === 0) return '0-0'
  1061. const start = (this.deviceCurrentPage - 1) * this.devicePerPage + 1
  1062. const end = Math.min(this.deviceCurrentPage * this.devicePerPage, this.filteredDevices.length)
  1063. return `${start}-${end}`
  1064. },
  1065. showDeviceHistory(device) {
  1066. console.log('显示设备数据:', device.name)
  1067. this.historyModal.show = true
  1068. this.historyModal.device = device
  1069. this.historyModal.device.type = 'soil'
  1070. this.historyModal.timeRange = '24h'
  1071. this.historyModal.activeIndicator = 'all'
  1072. // 等待模态框渲染完成后初始化图表
  1073. this.$nextTick(() => {
  1074. this.initChart()
  1075. })
  1076. },
  1077. closeHistoryModal() {
  1078. this.historyModal.show = false
  1079. this.historyModal.device = null
  1080. // 销毁图表实例
  1081. if (this.chartInstance) {
  1082. this.chartInstance.dispose()
  1083. this.chartInstance = null
  1084. }
  1085. },
  1086. setTimeRange(range) {
  1087. this.historyModal.timeRange = range
  1088. this.updateChart()
  1089. },
  1090. setActiveIndicator(indicator) {
  1091. this.historyModal.activeIndicator = indicator
  1092. this.updateChart()
  1093. },
  1094. getDeviceIndicators() {
  1095. if (!this.historyModal.device) return []
  1096. const indicators = {
  1097. soil: [
  1098. { label: '所有指标', value: 'all' },
  1099. { label: '土壤湿度', value: 'soilHumidity' },
  1100. { label: '土壤温度', value: 'soilTemperature' },
  1101. { label: 'EC值', value: 'ec' }
  1102. ],
  1103. water: [
  1104. { label: '所有指标', value: 'all' },
  1105. { label: 'pH值', value: 'ph' },
  1106. { label: '溶解氧', value: 'oxygen' },
  1107. { label: '电导率', value: 'conductivity' },
  1108. { label: '浊度', value: 'turbidity' }
  1109. ],
  1110. weather: [
  1111. { label: '所有指标', value: 'all' },
  1112. { label: '温度', value: 'temperature' },
  1113. { label: '湿度', value: 'humidity' },
  1114. { label: '光照', value: 'light' },
  1115. { label: '风速', value: 'windSpeed' },
  1116. { label: '降雨量', value: 'rainfall' }
  1117. ]
  1118. }
  1119. return indicators[this.historyModal.device.type] || indicators.soil
  1120. },
  1121. initChart() {
  1122. if (!this.$refs.chartContainer) return
  1123. // 销毁已存在的图表实例
  1124. if (this.chartInstance) {
  1125. this.chartInstance.dispose()
  1126. this.chartInstance = null
  1127. }
  1128. // 创建新的图表实例
  1129. this.chartInstance = this.$echarts.init(this.$refs.chartContainer)
  1130. this.updateChart()
  1131. },
  1132. updateChart() {
  1133. if (!this.chartInstance) return
  1134. // 生成模拟数据
  1135. const data = this.generateMockData()
  1136. console.log("data");
  1137. console.log(data);
  1138. const seriesNames = this.getSeriesNames()
  1139. console.log("seriesNames");
  1140. console.log(seriesNames);
  1141. const option = {
  1142. backgroundColor: 'transparent',
  1143. tooltip: {
  1144. trigger: 'axis',
  1145. backgroundColor: 'rgba(255, 255, 255, 0.95)',
  1146. borderColor: '#e5e7eb',
  1147. textStyle: { color: '#1f2937' }
  1148. },
  1149. legend: {
  1150. data: seriesNames,
  1151. textStyle: { color: '#6b7280' },
  1152. top: 0
  1153. },
  1154. grid: {
  1155. left: '3%',
  1156. right: '4%',
  1157. bottom: '3%',
  1158. containLabel: true,
  1159. top: 40
  1160. },
  1161. xAxis: {
  1162. type: 'category',
  1163. boundaryGap: false,
  1164. data: data.times,
  1165. axisLine: { lineStyle: { color: '#d1d5db' } },
  1166. axisLabel: { color: '#6b7280' }
  1167. },
  1168. yAxis: {
  1169. type: 'value',
  1170. axisLine: { lineStyle: { color: '#d1d5db' } },
  1171. axisLabel: { color: '#6b7280' },
  1172. splitLine: { lineStyle: { color: '#f3f4f6' } }
  1173. },
  1174. series: this.generateSeries(data)
  1175. }
  1176. this.chartInstance.setOption(option)
  1177. },
  1178. generateMockData() {
  1179. const times = []
  1180. const values = {}
  1181. const points = this.historyModal.timeRange === '24h' ? 24 : this.historyModal.timeRange === '7d' ? 7 : 30
  1182. const now = new Date()
  1183. // 根据设备类型生成不同的数据
  1184. const ranges = this.getDataRanges()
  1185. console.log("generateMockData");
  1186. console.log(ranges);
  1187. for (let i = points - 1; i >= 0; i--) {
  1188. const time = new Date(now - i * (this.historyModal.timeRange === '24h' ? 3600000 : 86400000))
  1189. times.push(this.historyModal.timeRange === '24h' ?
  1190. time.getHours() + ':00' :
  1191. (time.getMonth() + 1) + '/' + time.getDate())
  1192. ranges.forEach((range, index) => {
  1193. if (!values[`values${index + 1}`]) {
  1194. values[`values${index + 1}`] = []
  1195. }
  1196. values[`values${index + 1}`].push(
  1197. (Math.random() * (range[1] - range[0]) + range[0]).toFixed(1)
  1198. )
  1199. })
  1200. }
  1201. return { times, ...values }
  1202. },
  1203. getDataRanges() {
  1204. const ranges = {
  1205. soil: [[20, 40], [15, 30], [0.5, 2]], // 土壤湿度, 土壤温度, EC值
  1206. water: [[6.5, 8.5], [4, 8], [0.5, 2], [0, 10]], // pH值, 溶解氧, 电导率, 浊度
  1207. weather: [[15, 35], [40, 80], [0, 1000], [0, 10], [0, 50], [0, 10], [0, 10], [0, 10]] // 温度, 湿度, 光照, 风速, 降雨量,土壤氮,土壤磷,土壤钾
  1208. }
  1209. return ranges[this.historyModal.device.type] || ranges.soil
  1210. },
  1211. getSeriesNames() {
  1212. const names = {
  1213. soil: ['土壤湿度', '土壤温度', 'EC值'],
  1214. water: ['pH值', '溶解氧', '电导率', '浊度'],
  1215. weather: ['温度', '湿度', '光照', '风速', '降雨量','土壤氮','土壤磷','土壤钾']
  1216. }
  1217. const allNames = names[this.historyModal.device.type] || names.soil
  1218. if (this.historyModal.activeIndicator === 'all') {
  1219. return allNames
  1220. } else {
  1221. const indicatorMap = {
  1222. soilHumidity: '土壤湿度',
  1223. soilTemperature: '土壤温度',
  1224. ec: 'EC值',
  1225. ph: 'pH值',
  1226. oxygen: '溶解氧',
  1227. conductivity: '电导率',
  1228. turbidity: '浊度',
  1229. temperature: '温度',
  1230. humidity: '湿度',
  1231. light: '光照',
  1232. windSpeed: '风速',
  1233. rainfall: '降雨量'
  1234. }
  1235. return [indicatorMap[this.historyModal.activeIndicator] || allNames[0]]
  1236. }
  1237. },
  1238. generateSeries(data) {
  1239. console.log("generateSeries_data");
  1240. console.log(data);
  1241. console.log(this.historyModal.device.type);
  1242. console.log(this.historyModal.activeIndicator);
  1243. const deviceType = this.historyModal.device.type
  1244. const activeIndicator = this.historyModal.activeIndicator
  1245. const indicators = {
  1246. soil: {
  1247. names: ['土壤湿度', '土壤温度', 'EC值'],
  1248. colors: ['#10b981', '#34d399', '#f59e0b']
  1249. },
  1250. water: {
  1251. names: ['pH值', '溶解氧', '电导率', '浊度'],
  1252. colors: ['#10b981', '#34d399', '#6ee7b7', '#f59e0b']
  1253. },
  1254. weather: {
  1255. names: ['温度', '湿度', '光照', '风速', '降雨量', '土壤氮', '土壤磷', '土壤钾'],
  1256. colors: ['#10b981', '#34d399', '#6ee7b7', '#a7f3d0', '#f59e0b', '#a7f3d2', '#a7f3d3', '#a7f3d4']
  1257. }
  1258. }[deviceType] || { names: [], colors: [] }
  1259. console.log(indicators);
  1260. if (activeIndicator === 'all') {
  1261. return indicators.names.map((name, index) => ({
  1262. name: name,
  1263. type: 'line',
  1264. data: data[`values${index + 1}`] || [],
  1265. smooth: true,
  1266. showSymbol: false,
  1267. lineStyle: { width: 3 },
  1268. areaStyle: {
  1269. color: {
  1270. type: 'linear',
  1271. x: 0, y: 0, x2: 0, y2: 1,
  1272. colorStops: [
  1273. { offset: 0, color: indicators.colors[index] + '50' },
  1274. { offset: 0.8, color: indicators.colors[index] + '20' },
  1275. { offset: 1, color: indicators.colors[index] + '05' }
  1276. ]
  1277. }
  1278. },
  1279. itemStyle: { color: indicators.colors[index] }
  1280. }))
  1281. } else {
  1282. const index = indicators.names.indexOf(this.getIndicatorLabel(activeIndicator))
  1283. const color = indicators.colors[index]
  1284. return [{
  1285. name: this.getIndicatorLabel(activeIndicator),
  1286. type: 'line',
  1287. data: data[`values${index + 1}`] || [],
  1288. smooth: true,
  1289. showSymbol: false,
  1290. lineStyle: { width: 3 },
  1291. areaStyle: {
  1292. color: {
  1293. type: 'linear',
  1294. x: 0, y: 0, x2: 0, y2: 1,
  1295. colorStops: [
  1296. { offset: 0, color: color + '60' },
  1297. { offset: 0.7, color: color + '30' },
  1298. { offset: 1, color: color + '08' }
  1299. ]
  1300. }
  1301. },
  1302. itemStyle: { color: color }
  1303. }]
  1304. }
  1305. },
  1306. getIndicatorLabel(value) {
  1307. const indicatorMap = {
  1308. soilHumidity: '土壤湿度',
  1309. soilTemperature: '土壤温度',
  1310. ec: 'EC值',
  1311. ph: 'pH值',
  1312. oxygen: '溶解氧',
  1313. conductivity: '电导率',
  1314. turbidity: '浊度',
  1315. temperature: '温度',
  1316. humidity: '湿度',
  1317. light: '光照',
  1318. windSpeed: '风速',
  1319. rainfall: '降雨量'
  1320. }
  1321. return indicatorMap[value] || value
  1322. },
  1323. // 轮播控制方法
  1324. initializeCarousels() {
  1325. this.deviceList.forEach(device => {
  1326. if (device.metrics.length > 2) {
  1327. this.$set(this.carouselStates, device.id, 0)
  1328. this.startCarouselTimer(device.id)
  1329. }
  1330. })
  1331. },
  1332. startCarouselTimer(deviceId) {
  1333. if (this.carouselTimers[deviceId]) {
  1334. clearInterval(this.carouselTimers[deviceId])
  1335. }
  1336. this.carouselTimers[deviceId] = setInterval(() => {
  1337. this.nextMetric(deviceId)
  1338. }, 3000) // 3秒切换一次
  1339. },
  1340. pauseCarousel(deviceId) {
  1341. if (this.carouselTimers[deviceId]) {
  1342. clearInterval(this.carouselTimers[deviceId])
  1343. this.carouselTimers[deviceId] = null
  1344. }
  1345. },
  1346. resumeCarousel(deviceId) {
  1347. const device = this.deviceList.find(d => d.id === deviceId)
  1348. if (device && device.metrics.length > 2) {
  1349. this.startCarouselTimer(deviceId)
  1350. }
  1351. },
  1352. nextMetric(deviceId) {
  1353. const device = this.deviceList.find(d => d.id === deviceId)
  1354. if (!device || device.metrics.length <= 2) return
  1355. const totalGroups = Math.ceil(device.metrics.length / 2)
  1356. const currentIndex = this.carouselStates[deviceId] || 0
  1357. const nextIndex = (currentIndex + 1) % totalGroups
  1358. this.$set(this.carouselStates, deviceId, nextIndex)
  1359. },
  1360. getCurrentMetricIndex(deviceId) {
  1361. return this.carouselStates[deviceId] || 0
  1362. },
  1363. getCurrentMetrics(device) {
  1364. if (device.metrics.length <= 2) {
  1365. return device.metrics
  1366. }
  1367. const currentIndex = this.carouselStates[device.id] || 0
  1368. const startIndex = currentIndex * 2
  1369. return device.metrics.slice(startIndex, startIndex + 2)
  1370. }
  1371. },
  1372. mounted() {
  1373. // 保存原始背景色和设置浅色主题
  1374. this.originalBodyBg = document.body.style.backgroundColor || ''
  1375. this.originalBodyColor = document.body.style.color || ''
  1376. this.originalAppBg = document.getElementById('app')?.style.backgroundColor || ''
  1377. // 应用浅色主题
  1378. document.body.style.backgroundColor = '#f8fafc'
  1379. document.body.style.color = '#1f2937'
  1380. const appEl = document.getElementById('app')
  1381. if (appEl) {
  1382. appEl.style.backgroundColor = '#f8fafc'
  1383. }
  1384. // 初始化轮播
  1385. this.initializeCarousels()
  1386. // 获取摄像头列表
  1387. this.fetchCameras()
  1388. // 监听键盘事件
  1389. document.addEventListener('keydown', (e) => {
  1390. if (e.key === 'Escape') {
  1391. if (this.historyModal.show) {
  1392. this.closeHistoryModal()
  1393. } else if (this.singleCameraFullscreen.show) {
  1394. this.exitSingleFullscreen()
  1395. } else if (this.gridFullscreen.show) {
  1396. this.exitGridFullscreen()
  1397. }
  1398. } else if (this.gridFullscreen.show) {
  1399. // 网格全屏模式下的键盘快捷键
  1400. if (e.key === 'ArrowLeft') {
  1401. e.preventDefault()
  1402. this.gridPrevPage()
  1403. } else if (e.key === 'ArrowRight') {
  1404. e.preventDefault()
  1405. this.gridNextPage()
  1406. } else if (e.key >= '1' && e.key <= '9') {
  1407. // 数字键快速跳转页面
  1408. const pageNum = parseInt(e.key)
  1409. if (pageNum <= this.gridTotalPages) {
  1410. this.gridFullscreen.currentPage = pageNum
  1411. }
  1412. }
  1413. }
  1414. })
  1415. const query = { deviceId: '34020000001110000001' };
  1416. const query2 = { deviceId: '34020000001320000001' };
  1417. playback(query).then(response => {
  1418. })
  1419. playback(query2).then(response => {
  1420. this.iframeSrc = "http://121.4.16.100:28080/#/play/wasm/"+encodeURIComponent(response.msg);
  1421. })
  1422. },
  1423. beforeDestroy() {
  1424. // 清理所有轮播定时器
  1425. Object.values(this.carouselTimers).forEach(timer => {
  1426. if (timer) {
  1427. clearInterval(timer)
  1428. }
  1429. })
  1430. this.carouselTimers = {}
  1431. // 恢复原始背景色
  1432. document.body.style.backgroundColor = this.originalBodyBg
  1433. document.body.style.color = this.originalBodyColor
  1434. const appEl = document.getElementById('app')
  1435. if (appEl) {
  1436. appEl.style.backgroundColor = this.originalAppBg
  1437. }
  1438. // 销毁图表实例
  1439. if (this.chartInstance) {
  1440. this.chartInstance.dispose()
  1441. this.chartInstance = null
  1442. }
  1443. const query = { deviceId: '34020000001110000001' };
  1444. const query2 = { deviceId: '34020000001320000001' };
  1445. stopPlaying(query).then(response => {
  1446. console.log(response)
  1447. })
  1448. stopPlaying(query2).then(response => {
  1449. console.log(response)
  1450. })
  1451. }
  1452. }
  1453. </script>
  1454. <style scoped>
  1455. .device-monitor-container {
  1456. padding: 24px;
  1457. background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
  1458. color: #1f2937;
  1459. min-height: 100vh;
  1460. }
  1461. /* 控制面板 */
  1462. .device-monitor-container > .control-panel {
  1463. background: white;
  1464. border: 1px solid #e5e7eb;
  1465. border-radius: 16px;
  1466. padding: 1.5rem;
  1467. margin-bottom: 2rem;
  1468. display: flex;
  1469. justify-content: space-between;
  1470. align-items: center;
  1471. gap: 2rem;
  1472. transition: all 0.3s ease;
  1473. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  1474. }
  1475. .device-monitor-container > .control-panel:hover {
  1476. transform: translateY(-4px);
  1477. box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.15);
  1478. border-color: #10b981;
  1479. }
  1480. .control-left {
  1481. display: flex;
  1482. gap: 1.5rem;
  1483. flex: 1;
  1484. }
  1485. .form-group {
  1486. display: flex;
  1487. align-items: center;
  1488. gap: 0.5rem;
  1489. }
  1490. .form-group label {
  1491. font-size: 0.875rem;
  1492. color: #6b7280;
  1493. white-space: nowrap;
  1494. }
  1495. .form-group select {
  1496. background-color: white;
  1497. border: 1px solid #d1d5db;
  1498. color: #1f2937;
  1499. padding: 8px 12px;
  1500. border-radius: 8px;
  1501. min-width: 150px;
  1502. }
  1503. .form-group select:focus {
  1504. outline: none;
  1505. border-color: #10b981;
  1506. box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
  1507. }
  1508. .control-right {
  1509. display: flex;
  1510. align-items: center;
  1511. gap: 1rem;
  1512. }
  1513. .refresh-btn {
  1514. background-color: #10b981;
  1515. color: white;
  1516. border: none;
  1517. padding: 10px 20px;
  1518. border-radius: 8px;
  1519. cursor: pointer;
  1520. display: flex;
  1521. align-items: center;
  1522. gap: 8px;
  1523. font-weight: 500;
  1524. transition: all 0.2s ease;
  1525. }
  1526. .refresh-btn:hover {
  1527. background-color: #059669;
  1528. transform: translateY(-1px);
  1529. box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
  1530. }
  1531. .auto-refresh {
  1532. display: flex;
  1533. align-items: center;
  1534. gap: 0.5rem;
  1535. font-size: 0.875rem;
  1536. color: #6b7280;
  1537. }
  1538. .auto-refresh-select {
  1539. background-color: white;
  1540. border: 1px solid #d1d5db;
  1541. color: #1f2937;
  1542. padding: 4px 8px;
  1543. border-radius: 6px;
  1544. font-size: 0.875rem;
  1545. }
  1546. .auto-refresh-select:focus {
  1547. outline: none;
  1548. border-color: #10b981;
  1549. box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
  1550. }
  1551. .last-update {
  1552. font-size: 0.875rem;
  1553. color: #6b7280;
  1554. }
  1555. .last-update span {
  1556. color: #1f2937;
  1557. font-weight: 500;
  1558. }
  1559. /* 统计容器 */
  1560. .stats-container {
  1561. display: grid;
  1562. grid-template-columns: 2fr 5fr;
  1563. gap: 1rem;
  1564. margin-bottom: 2rem;
  1565. }
  1566. /* 综合统计卡片 */
  1567. .overview-stat-card {
  1568. background: white;
  1569. border: 1px solid #e5e7eb;
  1570. border-radius: 16px;
  1571. padding: 1.5rem;
  1572. transition: all 0.3s ease;
  1573. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  1574. }
  1575. .overview-stat-card:hover {
  1576. transform: translateY(-4px);
  1577. box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.15);
  1578. border-color: #10b981;
  1579. }
  1580. .overview-stats {
  1581. display: grid;
  1582. grid-template-columns: repeat(3, 1fr);
  1583. gap: 1.5rem;
  1584. }
  1585. .overview-item {
  1586. text-align: center;
  1587. }
  1588. /* 分类统计区域 */
  1589. .category-stats {
  1590. display: grid;
  1591. grid-template-columns: repeat(5, 1fr);
  1592. gap: 1rem;
  1593. }
  1594. .stat-card {
  1595. background: white;
  1596. border: 1px solid #e5e7eb;
  1597. border-radius: 12px;
  1598. padding: 1.5rem;
  1599. text-align: center;
  1600. transition: all 0.3s ease;
  1601. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  1602. }
  1603. .stat-card:hover {
  1604. transform: translateY(-4px);
  1605. box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.15);
  1606. border-color: #10b981;
  1607. }
  1608. .stat-card.alert {
  1609. border-color: #ef4444;
  1610. background: linear-gradient(to right, rgba(239, 68, 68, 0.05), transparent);
  1611. animation: pulse 2s infinite;
  1612. }
  1613. @keyframes pulse {
  1614. 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
  1615. 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
  1616. 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
  1617. }
  1618. .stat-number {
  1619. font-size: 2rem;
  1620. font-weight: 700;
  1621. margin-bottom: 0.5rem;
  1622. }
  1623. .stat-number.online { color: #10b981; }
  1624. .stat-number.offline { color: #ef4444; }
  1625. .stat-number.alert { color: #ef4444; }
  1626. .stat-number.camera { color: #3b82f6; }
  1627. .stat-number.sensor { color: #10b981; }
  1628. .stat-number.weather { color: #8b5cf6; }
  1629. .stat-number.control { color: #f59e0b; }
  1630. .stat-label {
  1631. font-size: 0.875rem;
  1632. color: #6b7280;
  1633. margin-bottom: 0.25rem;
  1634. font-weight: 500;
  1635. }
  1636. .stat-detail {
  1637. font-size: 0.75rem;
  1638. color: #9ca3af;
  1639. }
  1640. .stat-detail .processed {
  1641. color: #10b981;
  1642. }
  1643. /* 主要内容区域 */
  1644. .main-content {
  1645. display: flex;
  1646. flex-direction: column;
  1647. gap: 2rem;
  1648. }
  1649. .content-section {
  1650. background: white;
  1651. border: 1px solid #e5e7eb;
  1652. border-radius: 16px;
  1653. padding: 24px;
  1654. transition: all 0.3s ease;
  1655. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  1656. }
  1657. .content-section:hover {
  1658. transform: translateY(-4px);
  1659. box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.15);
  1660. border-color: #10b981;
  1661. }
  1662. .section-title {
  1663. font-size: 1.25rem;
  1664. font-weight: 600;
  1665. margin-bottom: 0;
  1666. color: #1f2937;
  1667. }
  1668. /* 视频头部样式 */
  1669. .video-header {
  1670. display: flex;
  1671. justify-content: space-between;
  1672. align-items: center;
  1673. margin-bottom: 1.5rem;
  1674. }
  1675. .video-header-left {
  1676. display: flex;
  1677. align-items: baseline;
  1678. gap: 1rem;
  1679. }
  1680. .video-count {
  1681. font-size: 0.875rem;
  1682. color: #6b7280;
  1683. }
  1684. .video-header-right {
  1685. display: flex;
  1686. align-items: center;
  1687. gap: 0.75rem;
  1688. }
  1689. .video-nav-controls {
  1690. display: flex;
  1691. align-items: center;
  1692. gap: 0.5rem;
  1693. }
  1694. .video-nav-btn {
  1695. width: 32px;
  1696. height: 32px;
  1697. display: flex;
  1698. align-items: center;
  1699. justify-content: center;
  1700. background: white;
  1701. border: 1px solid #d1d5db;
  1702. border-radius: 8px;
  1703. color: #6b7280;
  1704. transition: all 0.2s ease;
  1705. cursor: pointer;
  1706. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  1707. }
  1708. .video-nav-btn:hover:not(:disabled) {
  1709. background: #10b981;
  1710. color: white;
  1711. border-color: #10b981;
  1712. transform: translateY(-1px);
  1713. box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
  1714. }
  1715. .video-nav-btn:disabled {
  1716. opacity: 0.5;
  1717. cursor: not-allowed;
  1718. }
  1719. .page-indicator {
  1720. padding: 0 12px;
  1721. height: 32px;
  1722. display: flex;
  1723. align-items: center;
  1724. background: white;
  1725. border: 1px solid #d1d5db;
  1726. border-radius: 8px;
  1727. color: #6b7280;
  1728. font-size: 14px;
  1729. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  1730. }
  1731. /* 视频容器样式 */
  1732. .video-container {
  1733. padding: 0;
  1734. }
  1735. /* 摄像头网格 */
  1736. .camera-grid {
  1737. display: grid;
  1738. grid-template-columns: repeat(3, 1fr);
  1739. grid-gap: 16px;
  1740. margin-top: 12px;
  1741. }
  1742. .camera-card {
  1743. background: #fff;
  1744. border: 1px solid #eef1f5;
  1745. border-radius: 12px;
  1746. box-shadow: 0 2px 8px rgba(0,0,0,.04);
  1747. overflow: hidden;
  1748. display: flex;
  1749. flex-direction: column;
  1750. }
  1751. .card-header {
  1752. padding: 12px 16px;
  1753. display: flex;
  1754. align-items: center;
  1755. justify-content: space-between;
  1756. }
  1757. .card-footer {
  1758. padding: 12px 16px;
  1759. }
  1760. .card-header .title {
  1761. font-weight: 600;
  1762. color: #1f2937;
  1763. }
  1764. .card-preview {
  1765. height: 180px; /* 统一预览高度,避免卡片跳动 */
  1766. background: #f6f8fa;
  1767. position: relative;
  1768. }
  1769. .player-iframe, .player-sdk {
  1770. position: absolute;
  1771. inset: 0;
  1772. width: 100%;
  1773. height: 100%;
  1774. border: none;
  1775. }
  1776. .player-empty {
  1777. position: absolute;
  1778. inset: 0;
  1779. display: flex;
  1780. align-items: center;
  1781. justify-content: center;
  1782. color: #8c9aa3;
  1783. gap: 6px;
  1784. flex-direction: column;
  1785. }
  1786. .player-offline {
  1787. position: absolute;
  1788. inset: 0;
  1789. display: flex;
  1790. align-items: center;
  1791. justify-content: center;
  1792. background: #f5f5f5;
  1793. color: #9ca3af;
  1794. gap: 8px;
  1795. flex-direction: column;
  1796. }
  1797. .offline-camera-icon {
  1798. width: 48px;
  1799. height: 48px;
  1800. color: #9ca3af;
  1801. stroke-width: 1.5;
  1802. }
  1803. .offline-text {
  1804. font-size: 14px;
  1805. color: #6b7280;
  1806. font-weight: 500;
  1807. }
  1808. .card-footer .location {
  1809. color: #6b7280;
  1810. font-size: 14px;
  1811. }
  1812. @media (max-width: 1200px) {
  1813. .camera-grid {
  1814. grid-template-columns: repeat(2, 1fr);
  1815. }
  1816. }
  1817. @media (max-width: 768px) {
  1818. .camera-grid {
  1819. grid-template-columns: 1fr;
  1820. }
  1821. }
  1822. /* CameraPreview 预览对话框样式 */
  1823. .camera-preview-modal {
  1824. position: fixed;
  1825. top: 0;
  1826. left: 0;
  1827. right: 0;
  1828. bottom: 0;
  1829. background: rgba(0, 0, 0, 0.8);
  1830. display: flex;
  1831. align-items: center;
  1832. justify-content: center;
  1833. z-index: 10000;
  1834. padding: 20px;
  1835. }
  1836. .preview-dialog {
  1837. background: white;
  1838. border-radius: 16px;
  1839. width: 100%;
  1840. max-width: 1200px;
  1841. max-height: 90vh;
  1842. overflow: hidden;
  1843. display: flex;
  1844. flex-direction: column;
  1845. box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2);
  1846. }
  1847. .preview-header {
  1848. display: flex;
  1849. justify-content: space-between;
  1850. align-items: center;
  1851. padding: 20px 24px;
  1852. border-bottom: 1px solid #e5e7eb;
  1853. background: #f8fafc;
  1854. }
  1855. .preview-title {
  1856. display: flex;
  1857. align-items: center;
  1858. gap: 12px;
  1859. }
  1860. .preview-title h3 {
  1861. margin: 0;
  1862. font-size: 18px;
  1863. font-weight: 600;
  1864. color: #1f2937;
  1865. }
  1866. .preview-status {
  1867. font-size: 12px;
  1868. padding: 4px 8px;
  1869. border-radius: 12px;
  1870. font-weight: 500;
  1871. }
  1872. .preview-status.online {
  1873. background-color: rgba(16, 185, 129, 0.15);
  1874. color: #059669;
  1875. border: 1px solid rgba(16, 185, 129, 0.4);
  1876. }
  1877. .preview-status.offline {
  1878. background-color: rgba(239, 68, 68, 0.15);
  1879. color: #dc2626;
  1880. border: 1px solid rgba(239, 68, 68, 0.4);
  1881. }
  1882. .close-btn {
  1883. width: 40px;
  1884. height: 40px;
  1885. display: flex;
  1886. align-items: center;
  1887. justify-content: center;
  1888. background: transparent;
  1889. border: 1px solid #d1d5db;
  1890. border-radius: 8px;
  1891. color: #6b7280;
  1892. cursor: pointer;
  1893. transition: all 0.2s ease;
  1894. }
  1895. .close-btn:hover {
  1896. background: #f3f4f6;
  1897. border-color: #9ca3af;
  1898. color: #374151;
  1899. }
  1900. .close-btn svg {
  1901. width: 20px;
  1902. height: 20px;
  1903. }
  1904. .preview-video-container {
  1905. flex: 1;
  1906. background: #000;
  1907. position: relative;
  1908. min-height: 400px;
  1909. }
  1910. .preview-video {
  1911. width: 100%;
  1912. height: 100%;
  1913. object-fit: contain;
  1914. }
  1915. .preview-offline {
  1916. position: absolute;
  1917. inset: 0;
  1918. display: flex;
  1919. flex-direction: column;
  1920. align-items: center;
  1921. justify-content: center;
  1922. background: #f9fafb;
  1923. }
  1924. .preview-controls {
  1925. display: flex;
  1926. justify-content: space-between;
  1927. align-items: center;
  1928. padding: 16px 24px;
  1929. background: #f8fafc;
  1930. border-top: 1px solid #e5e7eb;
  1931. }
  1932. .controls-left,
  1933. .controls-right {
  1934. display: flex;
  1935. align-items: center;
  1936. gap: 12px;
  1937. }
  1938. .control-btn {
  1939. width: 40px;
  1940. height: 40px;
  1941. display: flex;
  1942. align-items: center;
  1943. justify-content: center;
  1944. background: white;
  1945. border: 1px solid #d1d5db;
  1946. border-radius: 8px;
  1947. color: #6b7280;
  1948. cursor: pointer;
  1949. transition: all 0.2s ease;
  1950. }
  1951. .control-btn:hover {
  1952. background: #10b981;
  1953. border-color: #10b981;
  1954. color: white;
  1955. }
  1956. .control-btn svg {
  1957. width: 20px;
  1958. height: 20px;
  1959. }
  1960. .volume-control {
  1961. display: flex;
  1962. align-items: center;
  1963. gap: 8px;
  1964. }
  1965. .volume-control svg {
  1966. width: 20px;
  1967. height: 20px;
  1968. color: #6b7280;
  1969. }
  1970. .volume-slider {
  1971. width: 100px;
  1972. height: 4px;
  1973. background: #e5e7eb;
  1974. border-radius: 2px;
  1975. outline: none;
  1976. appearance: none;
  1977. }
  1978. .volume-slider::-webkit-slider-thumb {
  1979. appearance: none;
  1980. width: 16px;
  1981. height: 16px;
  1982. background: #10b981;
  1983. border-radius: 50%;
  1984. cursor: pointer;
  1985. }
  1986. .volume-slider::-moz-range-thumb {
  1987. width: 16px;
  1988. height: 16px;
  1989. background: #10b981;
  1990. border-radius: 50%;
  1991. cursor: pointer;
  1992. border: none;
  1993. }
  1994. .camera-icon {
  1995. position: absolute;
  1996. width: 48px;
  1997. height: 48px;
  1998. color: #10b981;
  1999. top: 50%;
  2000. left: 50%;
  2001. transform: translate(-50%, -50%);
  2002. opacity: 0.7;
  2003. }
  2004. /* 强制居中的新样式 */
  2005. .centered-container {
  2006. display: flex !important;
  2007. align-items: center !important;
  2008. justify-content: center !important;
  2009. }
  2010. .centered-icon {
  2011. position: relative !important;
  2012. top: auto !important;
  2013. left: auto !important;
  2014. transform: none !important;
  2015. margin: auto !important;
  2016. }
  2017. .video-info-overlay {
  2018. position: absolute;
  2019. bottom: 0;
  2020. left: 0;
  2021. right: 0;
  2022. background: linear-gradient(to top, rgba(248, 250, 252, 0.95), rgba(248, 250, 252, 0.8), rgba(248, 250, 252, 0.4), transparent);
  2023. padding: 0.75rem;
  2024. display: flex;
  2025. justify-content: space-between;
  2026. align-items: flex-end;
  2027. z-index: 5;
  2028. backdrop-filter: blur(8px);
  2029. border-top: 1px solid rgba(229, 231, 235, 0.3);
  2030. }
  2031. .video-details {
  2032. flex: 1;
  2033. }
  2034. .video-name {
  2035. font-weight: 600;
  2036. margin-bottom: 0.25rem;
  2037. color: #1f2937;
  2038. font-size: 0.875rem;
  2039. text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
  2040. }
  2041. .video-location {
  2042. font-size: 0.75rem;
  2043. color: #6b7280;
  2044. text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
  2045. }
  2046. .video-status {
  2047. font-size: 0.75rem;
  2048. padding: 4px 8px;
  2049. border-radius: 12px;
  2050. font-weight: 500;
  2051. white-space: nowrap;
  2052. backdrop-filter: blur(4px);
  2053. }
  2054. .video-status.online {
  2055. background-color: rgba(16, 185, 129, 0.15);
  2056. color: #059669;
  2057. border: 1px solid rgba(16, 185, 129, 0.4);
  2058. font-weight: 600;
  2059. }
  2060. .video-status.offline {
  2061. background-color: rgba(239, 68, 68, 0.15);
  2062. color: #dc2626;
  2063. border: 1px solid rgba(239, 68, 68, 0.4);
  2064. font-weight: 600;
  2065. }
  2066. /* 设备监控头部 */
  2067. .device-monitor-header {
  2068. display: flex;
  2069. justify-content: space-between;
  2070. align-items: center;
  2071. margin-bottom: 1.5rem;
  2072. gap: 2rem;
  2073. }
  2074. .device-monitor-title-section {
  2075. display: flex;
  2076. align-items: center;
  2077. gap: 1rem;
  2078. flex: 1;
  2079. }
  2080. .section-title {
  2081. margin: 0;
  2082. line-height: 1.5;
  2083. }
  2084. .filter-buttons {
  2085. display: flex;
  2086. gap: 0.5rem;
  2087. align-items: center;
  2088. }
  2089. .filter-divider {
  2090. height: 24px;
  2091. width: 1px;
  2092. background-color: #e5e7eb;
  2093. }
  2094. .type-filters {
  2095. display: flex;
  2096. gap: 0.5rem;
  2097. }
  2098. .filter-btn {
  2099. padding: 0.5rem 1rem;
  2100. border-radius: 8px;
  2101. font-size: 0.875rem;
  2102. color: #6b7280;
  2103. background: transparent;
  2104. border: 1px solid #d1d5db;
  2105. transition: all 0.2s ease;
  2106. display: flex;
  2107. align-items: center;
  2108. gap: 0.5rem;
  2109. cursor: pointer;
  2110. height: 36px; /* 固定高度确保对齐 */
  2111. line-height: 1.5;
  2112. }
  2113. .filter-btn:hover {
  2114. background: rgba(16, 185, 129, 0.05);
  2115. border-color: #10b981;
  2116. color: #059669;
  2117. }
  2118. .filter-btn.active {
  2119. background: #10b981;
  2120. border-color: #10b981;
  2121. color: white;
  2122. }
  2123. .filter-count {
  2124. font-size: 0.75rem;
  2125. color: #9ca3af;
  2126. }
  2127. .filter-btn.active .filter-count {
  2128. color: rgba(255, 255, 255, 0.8);
  2129. }
  2130. .status-dot {
  2131. width: 8px;
  2132. height: 8px;
  2133. border-radius: 50%;
  2134. display: inline-block;
  2135. }
  2136. .status-dot.warning {
  2137. background-color: #f59e0b;
  2138. }
  2139. .status-dot.offline {
  2140. background-color: #ef4444;
  2141. }
  2142. .device-monitor-controls {
  2143. display: flex;
  2144. align-items: center;
  2145. gap: 1rem;
  2146. }
  2147. .sort-select {
  2148. background-color: white;
  2149. border: 1px solid #d1d5db;
  2150. color: #1f2937;
  2151. padding: 0.5rem 2rem 0.5rem 1rem;
  2152. border-radius: 8px;
  2153. font-size: 0.875rem;
  2154. appearance: none;
  2155. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236b7280'%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");
  2156. background-repeat: no-repeat;
  2157. background-position: right 0.75rem center;
  2158. background-size: 1rem;
  2159. height: 36px; /* 与按钮高度保持一致 */
  2160. line-height: 1.5;
  2161. }
  2162. .sort-select:focus {
  2163. outline: none;
  2164. border-color: #10b981;
  2165. box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
  2166. }
  2167. .device-pagination {
  2168. display: flex;
  2169. align-items: center;
  2170. gap: 0.5rem;
  2171. }
  2172. .page-btn {
  2173. width: 32px;
  2174. height: 32px;
  2175. display: flex;
  2176. align-items: center;
  2177. justify-content: center;
  2178. border-radius: 8px;
  2179. color: #6b7280;
  2180. transition: all 0.2s ease;
  2181. background: white;
  2182. border: 1px solid #d1d5db;
  2183. cursor: pointer;
  2184. }
  2185. .page-indicator {
  2186. padding: 0 12px;
  2187. height: 32px;
  2188. display: flex;
  2189. align-items: center;
  2190. color: #6b7280;
  2191. font-size: 14px;
  2192. line-height: 1;
  2193. font-weight: 500;
  2194. }
  2195. .page-btn:hover:not(:disabled) {
  2196. background: #10b981;
  2197. border-color: #10b981;
  2198. color: white;
  2199. transform: translateY(-1px);
  2200. box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
  2201. }
  2202. .page-btn:disabled {
  2203. opacity: 0.5;
  2204. cursor: not-allowed;
  2205. }
  2206. /* 设备网格 */
  2207. .device-grid {
  2208. display: grid;
  2209. grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  2210. gap: 1rem;
  2211. padding: 0.5rem;
  2212. min-height: 400px;
  2213. }
  2214. @media (min-width: 1280px) {
  2215. .device-grid {
  2216. grid-template-columns: repeat(3, 1fr);
  2217. }
  2218. }
  2219. @media (min-width: 1536px) {
  2220. .device-grid {
  2221. grid-template-columns: repeat(4, 1fr);
  2222. }
  2223. }
  2224. .device-card {
  2225. position: relative;
  2226. background: white;
  2227. border: 1px solid #e5e7eb;
  2228. border-radius: 12px;
  2229. padding: 1.5rem;
  2230. transition: all 0.3s ease;
  2231. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  2232. min-height: 250px;
  2233. display: flex;
  2234. flex-direction: column;
  2235. overflow: hidden;
  2236. }
  2237. /* 右上角数据按钮 */
  2238. .data-btn {
  2239. position: absolute;
  2240. top: 1rem;
  2241. right: 1rem;
  2242. width: 32px;
  2243. height: 32px;
  2244. display: flex;
  2245. align-items: center;
  2246. justify-content: center;
  2247. background: white;
  2248. border: 1px solid #d1d5db;
  2249. border-radius: 8px;
  2250. color: #6b7280;
  2251. transition: all 0.2s ease;
  2252. z-index: 1;
  2253. cursor: pointer;
  2254. }
  2255. .data-btn:hover {
  2256. background: #10b981;
  2257. border-color: #10b981;
  2258. color: white;
  2259. transform: translateY(-1px);
  2260. box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
  2261. }
  2262. .data-btn svg {
  2263. width: 16px;
  2264. height: 16px;
  2265. }
  2266. .device-card:hover {
  2267. transform: translateY(-4px);
  2268. box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.15);
  2269. border-color: #10b981;
  2270. }
  2271. .device-card.warning {
  2272. border-color: #f59e0b;
  2273. background: linear-gradient(to right, rgba(245, 158, 11, 0.05), rgba(245, 158, 11, 0.02));
  2274. }
  2275. .device-card.offline {
  2276. opacity: 0.8;
  2277. }
  2278. .device-header {
  2279. margin-bottom: 0.5rem;
  2280. padding-right: 3rem; /* 为右上角数据按钮留出空间 */
  2281. }
  2282. .device-title-row {
  2283. display: flex;
  2284. align-items: center;
  2285. gap: 0.75rem;
  2286. flex-wrap: wrap;
  2287. }
  2288. .device-name {
  2289. font-weight: 600;
  2290. color: #1f2937;
  2291. font-size: 1.125rem;
  2292. }
  2293. .device-status {
  2294. font-size: 0.75rem;
  2295. padding: 4px 12px;
  2296. border-radius: 12px;
  2297. font-weight: 500;
  2298. }
  2299. .device-status.online {
  2300. background-color: rgba(16, 185, 129, 0.1);
  2301. color: #10b981;
  2302. border: 1px solid rgba(16, 185, 129, 0.2);
  2303. }
  2304. .device-status.warning {
  2305. background-color: rgba(245, 158, 11, 0.1);
  2306. color: #f59e0b;
  2307. border: 1px solid rgba(245, 158, 11, 0.2);
  2308. }
  2309. .device-status.offline {
  2310. background-color: rgba(239, 68, 68, 0.1);
  2311. color: #ef4444;
  2312. border: 1px solid rgba(239, 68, 68, 0.2);
  2313. }
  2314. .device-location {
  2315. font-size: 0.875rem;
  2316. color: #6b7280;
  2317. margin-bottom: 1.5rem;
  2318. }
  2319. .device-metrics {
  2320. margin-bottom: 1.5rem;
  2321. flex: 1;
  2322. }
  2323. .metrics-container {
  2324. position: relative;
  2325. min-height: 80px;
  2326. overflow: hidden;
  2327. }
  2328. .metric-pair {
  2329. display: grid;
  2330. grid-template-columns: 1fr 1fr;
  2331. gap: 2rem;
  2332. width: 100%;
  2333. }
  2334. .metric {
  2335. text-align: left;
  2336. }
  2337. .metric-label {
  2338. font-size: 0.875rem;
  2339. color: #6b7280;
  2340. margin-bottom: 0.25rem;
  2341. font-weight: 500;
  2342. }
  2343. .metric-value {
  2344. font-size: 1.875rem;
  2345. font-weight: 600;
  2346. color: #1f2937;
  2347. display: flex;
  2348. align-items: baseline;
  2349. }
  2350. .metric-value span {
  2351. font-size: 1.25rem;
  2352. margin-left: 0.25rem;
  2353. color: #6b7280;
  2354. font-weight: 400;
  2355. }
  2356. .metric-value.metric-warning {
  2357. color: #ef4444;
  2358. }
  2359. .metric-value.metric-alert {
  2360. color: #ef4444;
  2361. }
  2362. /* 机场翻牌动画效果 */
  2363. .flip-enter-active,
  2364. .flip-leave-active {
  2365. transition: all 0.4s ease-in-out;
  2366. transform-style: preserve-3d;
  2367. }
  2368. .flip-enter {
  2369. transform: perspective(600px) rotateX(90deg);
  2370. opacity: 0;
  2371. }
  2372. .flip-enter-to {
  2373. transform: perspective(600px) rotateX(0deg);
  2374. opacity: 1;
  2375. }
  2376. .flip-leave {
  2377. transform: perspective(600px) rotateX(0deg);
  2378. opacity: 1;
  2379. }
  2380. .flip-leave-to {
  2381. transform: perspective(600px) rotateX(-90deg);
  2382. opacity: 0;
  2383. }
  2384. .device-card.offline .metric-value {
  2385. color: #9ca3af;
  2386. }
  2387. .device-footer {
  2388. display: flex;
  2389. justify-content: space-between;
  2390. align-items: center;
  2391. margin-top: auto;
  2392. }
  2393. .last-update {
  2394. font-size: 0.875rem;
  2395. color: #6b7280;
  2396. }
  2397. .last-update.text-warning {
  2398. color: #ef4444;
  2399. }
  2400. .last-update.text-offline {
  2401. color: #9ca3af;
  2402. }
  2403. .detail-btn {
  2404. background: transparent;
  2405. border: 1px solid #d1d5db;
  2406. color: #1f2937;
  2407. padding: 8px 16px;
  2408. border-radius: 8px;
  2409. font-size: 0.875rem;
  2410. cursor: pointer;
  2411. transition: all 0.2s ease;
  2412. font-weight: 500;
  2413. }
  2414. .detail-btn:hover {
  2415. border-color: #10b981;
  2416. background-color: rgba(16, 185, 129, 0.1);
  2417. color: #059669;
  2418. transform: translateY(-1px);
  2419. box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
  2420. }
  2421. .detail-btn.warning {
  2422. border-color: #ef4444;
  2423. color: #ef4444;
  2424. }
  2425. .detail-btn.warning:hover {
  2426. background-color: #ef4444;
  2427. color: white;
  2428. transform: translateY(-1px);
  2429. box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
  2430. }
  2431. /* 单个摄像头全屏模式 */
  2432. .single-camera-fullscreen {
  2433. position: fixed !important;
  2434. top: 0 !important;
  2435. left: 0 !important;
  2436. width: 100vw !important;
  2437. height: 100vh !important;
  2438. background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important;
  2439. z-index: 10000 !important;
  2440. display: flex !important;
  2441. margin: 0 !important;
  2442. padding: 0 !important;
  2443. }
  2444. .video-area {
  2445. flex: 1 !important;
  2446. min-width: 0 !important;
  2447. display: flex !important;
  2448. flex-direction: column !important;
  2449. position: relative !important;
  2450. }
  2451. .fullscreen-video-container {
  2452. flex: 1;
  2453. position: relative;
  2454. display: flex;
  2455. align-items: center;
  2456. justify-content: center;
  2457. background: linear-gradient(135deg, #f0fdf4 0%, #f8fafc 100%);
  2458. }
  2459. /* 左上角摄像头名称 */
  2460. .camera-title-overlay {
  2461. position: absolute !important;
  2462. top: 20px !important;
  2463. left: 20px !important;
  2464. z-index: 10001 !important;
  2465. background: rgba(255, 255, 255, 0.95) !important;
  2466. padding: 12px 20px !important;
  2467. border-radius: 12px !important;
  2468. backdrop-filter: blur(8px) !important;
  2469. border: 1px solid rgba(229, 231, 235, 0.8) !important;
  2470. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1) !important;
  2471. }
  2472. .camera-title-text {
  2473. color: #1f2937;
  2474. font-size: 1.25rem;
  2475. font-weight: 600;
  2476. margin: 0;
  2477. text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
  2478. letter-spacing: 0.5px;
  2479. }
  2480. /* 右上角关闭按钮 */
  2481. .close-button-overlay {
  2482. position: absolute !important;
  2483. top: 20px !important;
  2484. right: 20px !important;
  2485. z-index: 10001 !important;
  2486. }
  2487. .fullscreen-video-preview {
  2488. width: 100%;
  2489. height: 100%;
  2490. display: flex;
  2491. align-items: center;
  2492. justify-content: center;
  2493. background: linear-gradient(135deg, #f0fdf4 0%, #f8fafc 100%);
  2494. }
  2495. .fullscreen-camera-icon {
  2496. width: 120px;
  2497. height: 120px;
  2498. color: #10b981;
  2499. opacity: 0.6;
  2500. }
  2501. .single-camera-fullscreen .control-panel {
  2502. width: 280px;
  2503. background: white;
  2504. border-left: 1px solid #e5e7eb;
  2505. padding: 1.5rem;
  2506. overflow-y: auto;
  2507. height: 100vh;
  2508. display: flex;
  2509. flex-direction: column;
  2510. box-shadow: -4px 0 6px -1px rgba(0, 0, 0, 0.1);
  2511. }
  2512. .control-sections {
  2513. display: flex;
  2514. flex-direction: column;
  2515. gap: 1.25rem;
  2516. flex: 1;
  2517. justify-content: flex-start;
  2518. }
  2519. .control-section {
  2520. background: transparent;
  2521. }
  2522. .control-title {
  2523. font-size: 0.875rem;
  2524. font-weight: 600;
  2525. margin-bottom: 0.75rem;
  2526. color: #1f2937;
  2527. text-align: left;
  2528. }
  2529. /* 云台控制按钮网格 */
  2530. .ptz-controls {
  2531. display: grid;
  2532. grid-template-columns: repeat(3, 1fr);
  2533. gap: 0.5rem;
  2534. }
  2535. .ptz-btn {
  2536. aspect-ratio: 1;
  2537. display: flex;
  2538. align-items: center;
  2539. justify-content: center;
  2540. background: white;
  2541. border: 1px solid #d1d5db;
  2542. color: #6b7280;
  2543. border-radius: 8px;
  2544. font-size: 1.25rem;
  2545. transition: all 0.2s ease;
  2546. cursor: pointer;
  2547. min-height: 44px;
  2548. }
  2549. .ptz-btn:hover {
  2550. background: #10b981;
  2551. border-color: #10b981;
  2552. color: white;
  2553. transform: translateY(-1px);
  2554. box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
  2555. }
  2556. .ptz-btn:active {
  2557. transform: scale(0.95);
  2558. }
  2559. /* 统一所有控制按钮的样式 */
  2560. .zoom-btn, .playback-btn, .other-btn {
  2561. aspect-ratio: 1;
  2562. display: flex;
  2563. align-items: center;
  2564. justify-content: center;
  2565. min-height: 44px;
  2566. }
  2567. .zoom-btn svg, .playback-btn svg, .other-btn svg {
  2568. width: 16px !important;
  2569. height: 16px !important;
  2570. }
  2571. /* 变倍控制 */
  2572. .zoom-controls {
  2573. display: grid;
  2574. grid-template-columns: repeat(2, 1fr);
  2575. gap: 0.5rem;
  2576. }
  2577. /* 预置位按钮网格 */
  2578. .preset-grid {
  2579. display: grid;
  2580. grid-template-columns: repeat(2, 1fr);
  2581. gap: 0.5rem;
  2582. }
  2583. .preset-btn {
  2584. padding: 0.75rem;
  2585. background: white;
  2586. border: 1px solid #d1d5db;
  2587. color: #6b7280;
  2588. border-radius: 8px;
  2589. transition: all 0.2s ease;
  2590. text-align: center;
  2591. cursor: pointer;
  2592. font-size: 0.875rem;
  2593. min-height: 44px;
  2594. display: flex;
  2595. align-items: center;
  2596. justify-content: center;
  2597. }
  2598. .preset-btn:hover {
  2599. background: #10b981;
  2600. border-color: #10b981;
  2601. color: white;
  2602. transform: translateY(-1px);
  2603. box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
  2604. }
  2605. /* 回放控制 */
  2606. .playback-controls {
  2607. display: grid;
  2608. grid-template-columns: repeat(2, 1fr);
  2609. gap: 0.5rem;
  2610. }
  2611. /* 其他控制 */
  2612. .other-controls {
  2613. display: grid;
  2614. grid-template-columns: repeat(2, 1fr);
  2615. gap: 0.5rem;
  2616. }
  2617. /* 网格全屏模式 */
  2618. .grid-fullscreen-mode {
  2619. position: fixed !important;
  2620. top: 0 !important;
  2621. left: 0 !important;
  2622. width: 100vw !important;
  2623. height: 100vh !important;
  2624. background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important;
  2625. z-index: 10000 !important;
  2626. display: flex !important;
  2627. flex-direction: column !important;
  2628. padding: 1rem !important;
  2629. }
  2630. .fullscreen-header {
  2631. display: flex;
  2632. justify-content: space-between;
  2633. align-items: center;
  2634. padding: 1rem 0;
  2635. border-bottom: 1px solid #e5e7eb;
  2636. margin-bottom: 1rem;
  2637. gap: 2rem;
  2638. }
  2639. .fullscreen-title {
  2640. display: flex;
  2641. align-items: baseline;
  2642. gap: 1rem;
  2643. }
  2644. .fullscreen-title-text {
  2645. font-size: 1.5rem;
  2646. font-weight: 600;
  2647. color: #1f2937;
  2648. margin: 0;
  2649. }
  2650. .fullscreen-subtitle {
  2651. font-size: 0.875rem;
  2652. color: #6b7280;
  2653. }
  2654. .fullscreen-close-btn {
  2655. width: 40px;
  2656. height: 40px;
  2657. display: flex;
  2658. align-items: center;
  2659. justify-content: center;
  2660. background: white;
  2661. border: 1px solid #d1d5db;
  2662. border-radius: 8px;
  2663. color: #6b7280;
  2664. transition: all 0.2s ease;
  2665. cursor: pointer;
  2666. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  2667. }
  2668. .fullscreen-close-btn:hover {
  2669. background: #10b981;
  2670. color: white;
  2671. border-color: #10b981;
  2672. transform: translateY(-1px);
  2673. box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
  2674. }
  2675. /* 全屏分页控制样式 */
  2676. .fullscreen-pagination {
  2677. display: flex;
  2678. align-items: center;
  2679. gap: 1rem;
  2680. flex: 1;
  2681. justify-content: center;
  2682. }
  2683. .fullscreen-nav-btn {
  2684. width: 36px;
  2685. height: 36px;
  2686. display: flex;
  2687. align-items: center;
  2688. justify-content: center;
  2689. background: white;
  2690. border: 1px solid #d1d5db;
  2691. border-radius: 8px;
  2692. color: #6b7280;
  2693. transition: all 0.2s ease;
  2694. cursor: pointer;
  2695. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  2696. }
  2697. .fullscreen-nav-btn:hover:not(:disabled) {
  2698. background: #10b981;
  2699. color: white;
  2700. border-color: #10b981;
  2701. transform: translateY(-1px);
  2702. box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
  2703. }
  2704. .fullscreen-nav-btn:disabled {
  2705. opacity: 0.5;
  2706. cursor: not-allowed;
  2707. }
  2708. .fullscreen-page-indicator {
  2709. display: flex;
  2710. flex-direction: column;
  2711. align-items: center;
  2712. gap: 0.5rem;
  2713. color: #6b7280;
  2714. font-size: 0.875rem;
  2715. }
  2716. .fullscreen-page-dots {
  2717. display: flex;
  2718. gap: 0.5rem;
  2719. align-items: center;
  2720. }
  2721. .page-dot {
  2722. width: 8px;
  2723. height: 8px;
  2724. border-radius: 50%;
  2725. background-color: #d1d5db;
  2726. cursor: pointer;
  2727. transition: all 0.2s ease;
  2728. }
  2729. .page-dot.active {
  2730. background-color: #10b981;
  2731. transform: scale(1.2);
  2732. }
  2733. .page-dot:hover {
  2734. background-color: #6b7280;
  2735. }
  2736. /* 全屏控制区域 */
  2737. .fullscreen-controls {
  2738. display: flex;
  2739. align-items: center;
  2740. gap: 1rem;
  2741. }
  2742. /* 键盘快捷键提示 */
  2743. .keyboard-hints {
  2744. display: flex;
  2745. gap: 1rem;
  2746. align-items: center;
  2747. }
  2748. .hint-item {
  2749. display: flex;
  2750. align-items: center;
  2751. gap: 0.25rem;
  2752. font-size: 0.75rem;
  2753. color: #6b7280;
  2754. }
  2755. .hint-item kbd {
  2756. background: white;
  2757. border: 1px solid #d1d5db;
  2758. border-radius: 4px;
  2759. color: #374151;
  2760. font-size: 0.75rem;
  2761. padding: 2px 6px;
  2762. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  2763. font-family: monospace;
  2764. min-width: 20px;
  2765. text-align: center;
  2766. }
  2767. .fullscreen-video-grid {
  2768. display: grid;
  2769. grid-template-columns: repeat(3, 1fr);
  2770. grid-template-rows: repeat(2, 1fr);
  2771. gap: 1rem;
  2772. flex: 1;
  2773. height: calc(100vh - 120px);
  2774. }
  2775. .fullscreen-video-card {
  2776. position: relative;
  2777. background: white;
  2778. border: 1px solid #e5e7eb;
  2779. border-radius: 12px;
  2780. overflow: hidden;
  2781. transition: all 0.3s ease;
  2782. display: flex;
  2783. flex-direction: column;
  2784. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  2785. }
  2786. .fullscreen-video-card:hover {
  2787. border-color: #10b981;
  2788. transform: translateY(-4px);
  2789. box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.15);
  2790. }
  2791. .fullscreen-camera-btn {
  2792. position: absolute;
  2793. top: 12px;
  2794. right: 12px;
  2795. width: 32px;
  2796. height: 32px;
  2797. display: flex;
  2798. align-items: center;
  2799. justify-content: center;
  2800. background: rgba(255, 255, 255, 0.9);
  2801. border: 1px solid rgba(229, 231, 235, 0.8);
  2802. border-radius: 8px;
  2803. color: #6b7280;
  2804. transition: all 0.2s ease;
  2805. z-index: 10;
  2806. cursor: pointer;
  2807. backdrop-filter: blur(8px);
  2808. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  2809. }
  2810. .fullscreen-camera-btn:hover {
  2811. background: #10b981;
  2812. color: white;
  2813. border-color: #10b981;
  2814. }
  2815. .fullscreen-video-preview {
  2816. flex: 1;
  2817. position: relative;
  2818. background: linear-gradient(135deg, #f0fdf4 0%, #f8fafc 100%);
  2819. display: flex;
  2820. flex-direction: column;
  2821. }
  2822. .fullscreen-camera-placeholder {
  2823. flex: 1;
  2824. display: flex;
  2825. align-items: center;
  2826. justify-content: center;
  2827. }
  2828. .fullscreen-camera-icon {
  2829. width: 64px;
  2830. height: 64px;
  2831. color: #10b981;
  2832. opacity: 0.6;
  2833. }
  2834. .fullscreen-video-info {
  2835. position: absolute;
  2836. bottom: 0;
  2837. left: 0;
  2838. right: 0;
  2839. background: linear-gradient(to top, rgba(248, 250, 252, 0.95), rgba(248, 250, 252, 0.8), rgba(248, 250, 252, 0.4), transparent);
  2840. padding: 0.75rem;
  2841. display: flex;
  2842. justify-content: space-between;
  2843. align-items: flex-end;
  2844. z-index: 5;
  2845. backdrop-filter: blur(8px);
  2846. border-top: 1px solid rgba(229, 231, 235, 0.3);
  2847. }
  2848. .fullscreen-video-details {
  2849. flex: 1;
  2850. }
  2851. .fullscreen-video-name {
  2852. font-weight: 600;
  2853. margin-bottom: 0.25rem;
  2854. color: #1f2937;
  2855. font-size: 0.875rem;
  2856. text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
  2857. }
  2858. .fullscreen-video-location {
  2859. font-size: 0.75rem;
  2860. color: #6b7280;
  2861. text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
  2862. }
  2863. .fullscreen-video-status {
  2864. font-size: 0.75rem;
  2865. padding: 4px 8px;
  2866. border-radius: 12px;
  2867. font-weight: 500;
  2868. white-space: nowrap;
  2869. backdrop-filter: blur(4px);
  2870. }
  2871. .fullscreen-video-status.online {
  2872. background-color: rgba(16, 185, 129, 0.15);
  2873. color: #059669;
  2874. border: 1px solid rgba(16, 185, 129, 0.4);
  2875. font-weight: 600;
  2876. }
  2877. .fullscreen-video-status.offline {
  2878. background-color: rgba(239, 68, 68, 0.15);
  2879. color: #dc2626;
  2880. border: 1px solid rgba(239, 68, 68, 0.4);
  2881. font-weight: 600;
  2882. }
  2883. /* 从网格全屏进入单摄像头全屏时的处理 */
  2884. .grid-fullscreen-mode .single-camera-fullscreen {
  2885. position: fixed !important;
  2886. z-index: 10001 !important;
  2887. }
  2888. /* 网格全屏模式响应式设计 */
  2889. @media (max-width: 1024px) {
  2890. .fullscreen-video-grid {
  2891. grid-template-columns: repeat(2, 1fr);
  2892. grid-template-rows: repeat(3, 1fr);
  2893. }
  2894. .fullscreen-camera-icon {
  2895. width: 48px;
  2896. height: 48px;
  2897. }
  2898. }
  2899. @media (max-width: 768px) {
  2900. .grid-fullscreen-mode {
  2901. padding: 0.5rem !important;
  2902. }
  2903. .fullscreen-header {
  2904. padding: 0.5rem 0;
  2905. flex-direction: column;
  2906. gap: 1rem;
  2907. }
  2908. .fullscreen-title-text {
  2909. font-size: 1.25rem;
  2910. }
  2911. .fullscreen-pagination {
  2912. order: 2;
  2913. gap: 0.5rem;
  2914. }
  2915. .fullscreen-controls {
  2916. order: 3;
  2917. gap: 0.5rem;
  2918. }
  2919. .keyboard-hints {
  2920. display: none;
  2921. }
  2922. .fullscreen-video-grid {
  2923. grid-template-columns: repeat(1, 1fr);
  2924. grid-template-rows: repeat(6, 1fr);
  2925. gap: 0.5rem;
  2926. height: calc(100vh - 140px);
  2927. }
  2928. .fullscreen-camera-icon {
  2929. width: 40px;
  2930. height: 40px;
  2931. }
  2932. .fullscreen-page-indicator {
  2933. font-size: 0.75rem;
  2934. }
  2935. .fullscreen-nav-btn {
  2936. width: 32px;
  2937. height: 32px;
  2938. }
  2939. }
  2940. /* 响应式设计 */
  2941. @media (max-width: 1280px) {
  2942. .stats-container {
  2943. grid-template-columns: 1fr;
  2944. gap: 1.5rem;
  2945. }
  2946. .category-stats {
  2947. grid-template-columns: repeat(4, 1fr);
  2948. }
  2949. .device-monitor-header {
  2950. flex-direction: column;
  2951. align-items: stretch;
  2952. gap: 1rem;
  2953. }
  2954. .device-monitor-title-section {
  2955. flex-direction: column;
  2956. align-items: stretch;
  2957. gap: 1rem;
  2958. }
  2959. .filter-buttons {
  2960. flex-wrap: wrap;
  2961. }
  2962. .type-filters {
  2963. flex-wrap: wrap;
  2964. }
  2965. }
  2966. @media (max-width: 768px) {
  2967. .control-panel {
  2968. flex-direction: column;
  2969. align-items: stretch;
  2970. gap: 1rem;
  2971. }
  2972. .control-left {
  2973. flex-direction: column;
  2974. gap: 1rem;
  2975. }
  2976. .control-right {
  2977. flex-direction: column;
  2978. align-items: stretch;
  2979. gap: 1rem;
  2980. }
  2981. .form-group {
  2982. justify-content: space-between;
  2983. }
  2984. .stats-container {
  2985. grid-template-columns: 1fr;
  2986. }
  2987. .overview-stats {
  2988. grid-template-columns: repeat(3, 1fr);
  2989. gap: 1rem;
  2990. }
  2991. .category-stats {
  2992. grid-template-columns: repeat(2, 1fr);
  2993. }
  2994. .video-grid {
  2995. grid-template-columns: repeat(2, 1fr);
  2996. }
  2997. .device-monitor-header {
  2998. gap: 0.75rem;
  2999. }
  3000. .device-monitor-title-section {
  3001. gap: 0.75rem;
  3002. }
  3003. .device-monitor-controls {
  3004. flex-direction: column;
  3005. align-items: stretch;
  3006. gap: 0.75rem;
  3007. }
  3008. .filter-btn {
  3009. padding: 0.375rem 0.75rem;
  3010. font-size: 0.8rem;
  3011. }
  3012. .device-pagination {
  3013. justify-content: center;
  3014. }
  3015. .device-grid {
  3016. padding: 0;
  3017. }
  3018. .device-metrics {
  3019. gap: 1rem;
  3020. }
  3021. .metric-value {
  3022. font-size: 1.5rem;
  3023. }
  3024. .metric-value span {
  3025. font-size: 1rem;
  3026. }
  3027. .detail-btn {
  3028. width: 100%;
  3029. justify-content: center;
  3030. text-align: center;
  3031. }
  3032. }
  3033. /* 历史数据模态框样式 */
  3034. .history-modal-backdrop {
  3035. position: fixed !important;
  3036. top: 0 !important; /* 从页面顶部开始覆盖 */
  3037. left: 200px !important; /* 考虑左侧菜单栏宽度 */
  3038. right: 0 !important;
  3039. bottom: 0 !important;
  3040. background-color: rgba(248, 250, 252, 0.85) !important;
  3041. backdrop-filter: blur(8px) !important;
  3042. z-index: 1000 !important;
  3043. display: flex !important;
  3044. align-items: center !important;
  3045. justify-content: center !important;
  3046. padding: 1rem !important;
  3047. padding-top: 84px !important; /* 给顶部留出导航栏空间,但仍然覆盖 */
  3048. opacity: 1 !important;
  3049. transition: opacity 0.3s ease !important;
  3050. box-sizing: border-box !important;
  3051. }
  3052. /* 当侧边栏隐藏时的样式调整 */
  3053. .hideSidebar .history-modal-backdrop {
  3054. left: 54px !important; /* 折叠后的侧边栏宽度 */
  3055. }
  3056. /* 移动端适配 */
  3057. @media (max-width: 1024px) {
  3058. .history-modal-backdrop {
  3059. left: 0 !important;
  3060. padding-top: 50px !important;
  3061. }
  3062. }
  3063. .history-modal-content {
  3064. background-color: white !important;
  3065. border-radius: 16px !important;
  3066. width: 100% !important;
  3067. max-width: 1000px !important;
  3068. max-height: 90vh !important;
  3069. box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04) !important;
  3070. border: 1px solid #e5e7eb !important;
  3071. overflow: hidden !important;
  3072. display: flex !important;
  3073. flex-direction: column !important;
  3074. }
  3075. .history-modal-header {
  3076. display: flex !important;
  3077. justify-content: space-between !important;
  3078. align-items: center !important;
  3079. padding: 1rem 1.5rem !important;
  3080. border-bottom: 1px solid #e5e7eb !important;
  3081. background: #f8fafc !important;
  3082. }
  3083. .history-modal-title-section {
  3084. display: flex !important;
  3085. align-items: center !important;
  3086. gap: 0.75rem !important;
  3087. }
  3088. .history-modal-title {
  3089. font-size: 1.125rem !important;
  3090. font-weight: 600 !important;
  3091. color: #1f2937 !important;
  3092. margin: 0 !important;
  3093. }
  3094. .history-modal-close {
  3095. background: transparent !important;
  3096. border: none !important;
  3097. color: #6b7280 !important;
  3098. cursor: pointer !important;
  3099. padding: 0.5rem !important;
  3100. transition: all 0.2s ease !important;
  3101. border-radius: 8px !important;
  3102. display: flex !important;
  3103. align-items: center !important;
  3104. justify-content: center !important;
  3105. width: 40px !important;
  3106. height: 40px !important;
  3107. }
  3108. .history-modal-close:hover {
  3109. color: #1f2937 !important;
  3110. background: rgba(16, 185, 129, 0.1) !important;
  3111. }
  3112. .history-modal-close svg {
  3113. width: 20px !important;
  3114. height: 20px !important;
  3115. stroke: currentColor !important;
  3116. fill: none !important;
  3117. }
  3118. .history-modal-controls {
  3119. display: flex !important;
  3120. justify-content: space-between !important;
  3121. align-items: center !important;
  3122. padding: 1rem 1.5rem !important;
  3123. border-bottom: 1px solid #e5e7eb !important;
  3124. background: white !important;
  3125. }
  3126. .time-range-buttons {
  3127. display: flex !important;
  3128. gap: 0.5rem !important;
  3129. }
  3130. .indicator-buttons {
  3131. display: flex !important;
  3132. gap: 0.5rem !important;
  3133. }
  3134. .time-range-btn, .indicator-btn {
  3135. padding: 0.5rem 1rem !important;
  3136. border-radius: 8px !important;
  3137. font-size: 0.875rem !important;
  3138. color: #6b7280 !important;
  3139. background: transparent !important;
  3140. border: 1px solid #d1d5db !important;
  3141. transition: all 0.2s ease !important;
  3142. cursor: pointer !important;
  3143. }
  3144. .time-range-btn:hover, .indicator-btn:hover {
  3145. background: rgba(16, 185, 129, 0.05) !important;
  3146. border-color: #10b981 !important;
  3147. color: #059669 !important;
  3148. }
  3149. .time-range-btn.active, .indicator-btn.active {
  3150. background: #10b981 !important;
  3151. border-color: #10b981 !important;
  3152. color: white !important;
  3153. }
  3154. .history-modal-body {
  3155. flex: 1 !important;
  3156. padding: 1.5rem !important;
  3157. background: white !important;
  3158. overflow: hidden !important;
  3159. }
  3160. .chart-container {
  3161. width: 100% !important;
  3162. height: 400px !important;
  3163. display: flex !important;
  3164. align-items: center !important;
  3165. justify-content: center !important;
  3166. background: transparent !important;
  3167. border-radius: 6px !important;
  3168. }
  3169. /* 响应式设计 */
  3170. @media (max-width: 768px) {
  3171. .history-modal-content {
  3172. margin: 0.5rem !important;
  3173. max-width: calc(100vw - 1rem) !important;
  3174. max-height: calc(100vh - 1rem) !important;
  3175. }
  3176. .history-modal-controls {
  3177. flex-direction: column !important;
  3178. gap: 1rem !important;
  3179. align-items: stretch !important;
  3180. }
  3181. .time-range-buttons, .indicator-buttons {
  3182. flex-wrap: wrap !important;
  3183. justify-content: center !important;
  3184. }
  3185. .time-range-btn, .indicator-btn {
  3186. padding: 0.375rem 0.75rem !important;
  3187. font-size: 0.8rem !important;
  3188. }
  3189. .chart-container {
  3190. height: 300px !important;
  3191. }
  3192. }
  3193. </style>