| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613 |
- <template>
- <div class="device-monitor-container">
- <!-- 控制面板 -->
- <div class="control-panel">
- <div class="control-left">
- <div class="form-group">
- <label>农场选择</label>
- <select v-model="selectedFarm">
- <option value="all">全部农场</option>
- <option value="farm1">东湖智慧农场</option>
- <option value="farm2">西湖有机农场</option>
- </select>
- </div>
- <div class="form-group">
- <label>地块选择</label>
- <select v-model="selectedArea">
- <option value="all">全部地块</option>
- <option value="area1">1号地块</option>
- <option value="area2">2号地块</option>
- </select>
- </div>
- <div class="form-group">
- <label>时间范围</label>
- <select v-model="timeRange">
- <option value="realtime">实时数据</option>
- <option value="1h">近1小时</option>
- <option value="24h">近24小时</option>
- </select>
- </div>
- </div>
- <div class="control-right">
- <button class="refresh-btn" @click="refreshData">
- <i class="icon-refresh"></i>
- 刷新数据
- </button>
- <div class="auto-refresh">
- <span>自动刷新:</span>
- <select v-model="autoRefresh" class="auto-refresh-select">
- <option value="15">15秒</option>
- <option value="30">30秒</option>
- <option value="60">60秒</option>
- </select>
- </div>
- <div class="last-update">
- 最后更新:<span>{{ lastUpdateTime }}</span>
- </div>
- </div>
- </div>
- <!-- 设备统计 -->
- <div class="stats-container">
- <!-- 综合统计卡片 -->
- <div class="overview-stat-card">
- <div class="overview-stats">
- <div class="overview-item">
- <div class="stat-number online">42</div>
- <div class="stat-label">在线设备</div>
- </div>
- <div class="overview-item">
- <div class="stat-number offline">3</div>
- <div class="stat-label">离线设备</div>
- </div>
- <div class="overview-item">
- <div class="stat-number alert">5</div>
- <div class="stat-label">告警设备</div>
- </div>
- </div>
- </div>
- <!-- 分类统计卡片 -->
- <div class="category-stats">
- <div class="stat-card">
- <div class="stat-number camera">{{ cameraList.length }}</div>
- <div class="stat-label">摄像头</div>
- <div class="stat-detail">{{ getOnlineCameraCount() }}在线 / {{ getOfflineCameraCount() }}离线</div>
- </div>
- <div class="stat-card">
- <div class="stat-number sensor">{{ deviceList.length }}</div>
- <div class="stat-label">传感器</div>
- <div class="stat-detail">{{ getDeviceCountByStatus('online') }}在线 / {{ getDeviceCountByStatus('warning') }}告警</div>
- </div>
- <div class="stat-card">
- <div class="stat-number weather">8</div>
- <div class="stat-label">气象设备</div>
- <div class="stat-detail">7在线 / 1离线</div>
- </div>
- <div class="stat-card">
- <div class="stat-number control">10</div>
- <div class="stat-label">控制设备</div>
- <div class="stat-detail">10在线</div>
- </div>
- <div class="stat-card">
- <div class="stat-number alert">7</div>
- <div class="stat-label">今日告警</div>
- <div class="stat-detail"><span class="processed">5已处理</span></div>
- </div>
- </div>
- </div>
- <!-- 主要内容区域 -->
- <div class="main-content">
- <!-- 视频监控 -->
- <div class="content-section">
- <div class="video-header">
- <div class="video-header-left">
- <h2 class="section-title">视频监控</h2>
- <div class="video-count">
- 共 {{ cameraList.length }} 个摄像头
- </div>
- </div>
- <div class="video-header-right">
- <div class="video-nav-controls">
- <button class="video-nav-btn" @click="prevPage" :disabled="pageIndex === 1" title="上一页">
- <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
- <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" />
- </svg>
- </button>
- <div class="page-indicator">
- <span>{{ getPageRange() }} / {{ cameraList.length }}</span>
- </div>
- <button class="video-nav-btn" @click="nextPage" :disabled="pageIndex === totalCameraPages" title="下一页">
- <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
- <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" />
- </svg>
- </button>
- </div>
- <button class="video-nav-btn" @click="toggleFullscreen" title="全屏显示">
- <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
- <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" />
- </svg>
- </button>
- </div>
- </div>
- <!-- 视频卡片网格 -->
- <div class="camera-grid">
- <div
- class="camera-card"
- v-for="cam in pageCameras"
- :key="cam.id"
- >
- <div class="card-header">
- <span class="title">{{ cam.name }}</span>
- <el-tag size="mini" :type="cam.status === '在线' ? 'success' : 'info'">
- {{ cam.status || '离线' }}
- </el-tag>
- </div>
- <!-- 预览区:优先使用 cam.previewUrl(iframe),否则渲染厂商 SDK 容器 cam.domId;若都没有,显示占位 -->
- <div class="card-preview">
- <iframe
- v-if="cam.previewUrl && cam.status === '在线'"
- class="player-iframe"
- :src="cam.previewUrl"
- frameborder="0"
- allow="autoplay; encrypted-media"
- allowfullscreen
- ></iframe>
- <div
- v-else-if="cam.domId && cam.status === '在线'"
- class="player-sdk"
- :id="cam.domId"
- ></div>
- <!-- 离线状态显示 -->
- <div v-else-if="cam.status === '离线'" class="player-offline">
- <svg class="offline-camera-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
- <path d="M23 7l-7 5 7 5V7z"/>
- <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
- <line x1="1" y1="1" x2="23" y2="23"/>
- </svg>
- <span class="offline-text">设备离线</span>
- </div>
- <!-- 其他情况显示占位 -->
- <div v-else class="player-empty">
- <i class="el-icon-video-camera"></i>
- <span>暂无预览</span>
- </div>
- </div>
- <div class="card-footer">
- <span class="location">{{ cam.location || '-' }}</span>
- </div>
- </div>
- </div>
- </div>
- <!-- 设备监控 -->
- <div class="content-section">
- <!-- 顶部控制栏 -->
- <div class="device-monitor-header">
- <div class="device-monitor-title-section">
- <h2 class="section-title">设备监控</h2>
- <div class="filter-buttons">
- <button class="filter-btn" :class="{ active: deviceFilter === 'all' }" @click="setFilter('all')">
- 全部设备
- <span class="filter-count">({{ deviceList.length }})</span>
- </button>
- <button class="filter-btn" :class="{ active: deviceFilter === 'warning' }" @click="setFilter('warning')">
- <span class="status-dot warning"></span>
- 告警设备
- <span class="filter-count">({{ getDeviceCountByStatus('warning') }})</span>
- </button>
- <button class="filter-btn" :class="{ active: deviceFilter === 'offline' }" @click="setFilter('offline')">
- <span class="status-dot offline"></span>
- 离线设备
- <span class="filter-count">({{ getDeviceCountByStatus('offline') }})</span>
- </button>
- </div>
- <div class="filter-divider"></div>
- <div class="type-filters">
- <button class="filter-btn" :class="{ active: deviceTypeFilter === 'soil' }" @click="setTypeFilter('soil')">土壤监测</button>
- <button class="filter-btn" :class="{ active: deviceTypeFilter === 'water' }" @click="setTypeFilter('water')">水质监测</button>
- <button class="filter-btn" :class="{ active: deviceTypeFilter === 'weather' }" @click="setTypeFilter('weather')">气象监测</button>
- </div>
- </div>
- <div class="device-monitor-controls">
- <select class="sort-select" v-model="sortBy">
- <option value="alert">告警优先</option>
- <option value="time">更新时间</option>
- <option value="name">设备名称</option>
- </select>
- <div class="device-pagination">
- <button class="page-btn" @click="prevDevicePage" :disabled="deviceCurrentPage === 1">
- <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
- <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" />
- </svg>
- </button>
- <span class="page-indicator">{{ getDevicePageRange() }} / {{ filteredDevices.length }}</span>
- <button class="page-btn" @click="nextDevicePage" :disabled="deviceCurrentPage === deviceTotalPages">
- <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
- <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" />
- </svg>
- </button>
- </div>
- </div>
- </div>
- <!-- 设备卡片网格 -->
- <div class="device-grid">
- <div v-for="device in paginatedDevices" :key="device.id" class="device-card" :class="device.status">
- <!-- 右上角数据按钮 -->
- <button class="data-btn" @click="showDeviceHistory(device)" title="查看数据">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <path d="M12 20v-6M6 20V10M18 20V4"/>
- </svg>
- </button>
- <div class="device-header">
- <div class="device-title-row">
- <div class="device-name">{{ device.name }}</div>
- <div class="device-status" :class="device.status">{{ device.statusText }}</div>
- </div>
- </div>
- <div class="device-location">{{ device.location }}</div>
- <div class="device-metrics"
- @mouseenter="pauseCarousel(device.id)"
- @mouseleave="resumeCarousel(device.id)">
- <div class="metrics-container">
- <transition name="flip" mode="out-in">
- <div class="metric-pair" :key="getCurrentMetricIndex(device.id)">
- <div class="metric">
- <div class="metric-label">{{ getCurrentMetrics(device)[0].name }}</div>
- <div class="metric-value" :class="{
- 'metric-warning': device.status === 'warning' && getCurrentMetrics(device)[0].isAlert,
- 'metric-alert': getCurrentMetrics(device)[0].isAlert
- }">
- {{ getCurrentMetrics(device)[0].value }}{{ getCurrentMetrics(device)[0].unit }}
- </div>
- </div>
- <div class="metric" v-if="getCurrentMetrics(device)[1]">
- <div class="metric-label">{{ getCurrentMetrics(device)[1].name }}</div>
- <div class="metric-value" :class="{
- 'metric-warning': device.status === 'warning' && getCurrentMetrics(device)[1].isAlert,
- 'metric-alert': getCurrentMetrics(device)[1].isAlert
- }">
- {{ getCurrentMetrics(device)[1].value }}{{ getCurrentMetrics(device)[1].unit }}
- </div>
- </div>
- </div>
- </transition>
- </div>
- </div>
- <div class="device-footer">
- <div class="last-update" :class="{ 'text-warning': device.status === 'warning', 'text-offline': device.status === 'offline' }">
- {{ device.lastUpdate }}
- </div>
- <button class="detail-btn" :class="device.status">
- {{ device.status === 'warning' ? '处理告警' : '查看详情' }}
- </button>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 摄像头预览对话框 -->
- <CameraPreview
- v-model="previewVisible"
- :camera="currentCamera"
- />
- <!-- 单个摄像头全屏模式 -->
- <div v-if="singleCameraFullscreen.show" class="single-camera-fullscreen">
- <div class="video-area">
- <div class="fullscreen-video-container">
- <!-- 左上角摄像头名称 -->
- <div class="camera-title-overlay" v-if="singleCameraFullscreen.camera">
- <h3 class="camera-title-text">{{ singleCameraFullscreen.camera.name }}</h3>
- </div>
- <!-- 右上角关闭按钮 -->
- <div class="close-button-overlay">
- <button class="video-nav-btn" @click="exitSingleFullscreen" title="退出全屏">
- <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
- <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"/>
- </svg>
- </button>
- </div>
- <!-- 视频画面区域 -->
- <div class="fullscreen-video-preview">
- <svg class="fullscreen-camera-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <path d="M23 7l-7 5 7 5V7z"/>
- <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
- </svg>
- </div>
- </div>
- </div>
- <div class="control-panel">
- <div class="control-sections">
- <!-- 云台控制 -->
- <div class="control-section">
- <h4 class="control-title">云台控制</h4>
- <div class="ptz-controls">
- <button class="ptz-btn" title="左上">↖</button>
- <button class="ptz-btn" title="向上">↑</button>
- <button class="ptz-btn" title="右上">↗</button>
- <button class="ptz-btn" title="向左">←</button>
- <button class="ptz-btn" title="停止">●</button>
- <button class="ptz-btn" title="向右">→</button>
- <button class="ptz-btn" title="左下">↙</button>
- <button class="ptz-btn" title="向下">↓</button>
- <button class="ptz-btn" title="右下">↘</button>
- </div>
- </div>
- <!-- 变倍控制 -->
- <div class="control-section">
- <h4 class="control-title">变倍控制</h4>
- <div class="zoom-controls">
- <button class="ptz-btn zoom-btn" title="放大">
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <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"/>
- </svg>
- </button>
- <button class="ptz-btn zoom-btn" title="缩小">
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <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"/>
- </svg>
- </button>
- </div>
- </div>
- <!-- 预置位 -->
- <div class="control-section">
- <h4 class="control-title">预置位</h4>
- <div class="preset-grid">
- <button v-for="n in 6" :key="n" class="preset-btn">位置{{ n }}</button>
- </div>
- </div>
- <!-- 回放控制 -->
- <div class="control-section">
- <h4 class="control-title">回放控制</h4>
- <div class="playback-controls">
- <button class="ptz-btn playback-btn" title="开始回放">
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <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"/>
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
- </svg>
- </button>
- <button class="ptz-btn playback-btn" title="暂停回放">
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <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"/>
- </svg>
- </button>
- </div>
- </div>
- <!-- 其他控制 -->
- <div class="control-section">
- <h4 class="control-title">其他控制</h4>
- <div class="other-controls">
- <button class="ptz-btn other-btn" title="截图">
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <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"/>
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"/>
- </svg>
- </button>
- <button class="ptz-btn other-btn" title="录制">
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <circle cx="12" cy="12" r="10" stroke-width="2"/>
- <circle cx="12" cy="12" r="3" fill="currentColor"/>
- </svg>
- </button>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 网格全屏模式 -->
- <div v-if="gridFullscreen.show" class="grid-fullscreen-mode">
- <div class="fullscreen-header">
- <div class="fullscreen-title">
- <h2 class="fullscreen-title-text">视频监控</h2>
- <div class="fullscreen-subtitle">共 {{ cameraList.length }} 个摄像头</div>
- </div>
- <!-- 分页控制 -->
- <div class="fullscreen-pagination" v-if="gridTotalPages > 1">
- <button class="fullscreen-nav-btn" @click="gridPrevPage" :disabled="gridFullscreen.currentPage === 1" title="上一页">
- <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
- <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" />
- </svg>
- </button>
- <div class="fullscreen-page-indicator">
- <span>{{ gridPageRange }} / {{ cameraList.length }}</span>
- <div class="fullscreen-page-dots">
- <span
- v-for="page in gridTotalPages"
- :key="page"
- class="page-dot"
- :class="{ active: page === gridFullscreen.currentPage }"
- @click="gridFullscreen.currentPage = page"
- ></span>
- </div>
- </div>
- <button class="fullscreen-nav-btn" @click="gridNextPage" :disabled="gridFullscreen.currentPage === gridTotalPages" title="下一页">
- <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
- <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" />
- </svg>
- </button>
- </div>
- <div class="fullscreen-controls">
- <!-- 键盘快捷键提示 -->
- <div class="keyboard-hints" v-if="gridTotalPages > 1">
- <div class="hint-item">
- <kbd>←</kbd><kbd>→</kbd> 翻页
- </div>
- <div class="hint-item">
- <kbd>1-9</kbd> 跳转
- </div>
- <div class="hint-item">
- <kbd>ESC</kbd> 退出
- </div>
- </div>
- <button class="fullscreen-close-btn" @click="exitGridFullscreen" title="退出全屏 (ESC)">
- <svg class="w-6 h-6" viewBox="0 0 20 20" fill="currentColor">
- <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"/>
- </svg>
- </button>
- </div>
- </div>
- <div class="fullscreen-video-grid">
- <div v-for="camera in gridPaginatedVideos" :key="camera.id" class="fullscreen-video-card">
- <button class="fullscreen-camera-btn" @click="openVideoFullscreen(camera)" title="单独查看">
- <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
- <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" />
- </svg>
- </button>
- <div class="fullscreen-video-preview">
- <div class="fullscreen-camera-placeholder">
- <svg class="fullscreen-camera-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <path d="M23 7l-7 5 7 5V7z"/>
- <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
- </svg>
- </div>
- <div class="fullscreen-video-info">
- <div class="fullscreen-video-details">
- <div class="fullscreen-video-name">{{ camera.name }}</div>
- <div class="fullscreen-video-location">{{ camera.location }}</div>
- </div>
- <span class="fullscreen-video-status" :class="camera.status === '在线' ? 'online' : 'offline'">
- {{ camera.status === '在线' ? '在线' : '离线' }}
- </span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 设备历史数据模态框 -->
- <div v-if="historyModal.show" class="history-modal-backdrop" @click="closeHistoryModal">
- <div class="history-modal-content" @click.stop>
- <!-- 模态框头部 -->
- <div class="history-modal-header">
- <div class="history-modal-title-section">
- <h3 class="history-modal-title">{{ historyModal.device.name }} - 历史数据</h3>
- <span class="device-status" :class="historyModal.device.status">{{ historyModal.device.statusText }}</span>
- </div>
- <button class="history-modal-close" @click="closeHistoryModal">
- <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
- </svg>
- </button>
- </div>
- <!-- 图表控制区域 -->
- <div class="history-modal-controls">
- <div class="time-range-buttons">
- <button
- v-for="range in timeRanges"
- :key="range.value"
- class="time-range-btn"
- :class="{ active: historyModal.timeRange === range.value }"
- @click="setTimeRange(range.value)"
- >
- {{ range.label }}
- </button>
- </div>
- <div class="indicator-buttons">
- <button
- v-for="indicator in getDeviceIndicators()"
- :key="indicator.value"
- class="indicator-btn"
- :class="{ active: historyModal.activeIndicator === indicator.value }"
- @click="setActiveIndicator(indicator.value)"
- >
- {{ indicator.label }}
- </button>
- </div>
- </div>
- <!-- 图表区域 -->
- <div class="history-modal-body">
- <div ref="chartContainer" class="chart-container"></div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script>
- import { playback, stopPlaying } from "@/api/base/device"
- export default {
- name: 'DeviceMonitor',
- data() {
- return {
- selectedFarm: 'all',
- selectedArea: 'all',
- timeRange: 'realtime',
- autoRefresh: '30',
- lastUpdateTime: '刚刚',
- // 摄像头相关
- cameraList: [],
- pageIndex: 1,
- pageSize: 3,
- // 设备监控相关
- deviceFilter: 'all',
- deviceTypeFilter: 'all',
- sortBy: 'alert',
- deviceCurrentPage: 1,
- devicePerPage: 8,
- iframeSrc: '',
- // 预览对话框状态
- previewVisible: false,
- currentCamera: null,
- // 单个摄像头全屏模式
- singleCameraFullscreen: {
- show: false,
- camera: null
- },
- // 网格全屏模式
- gridFullscreen: {
- show: false,
- currentPage: 1,
- videosPerPage: 6
- },
- // 历史数据模态框
- historyModal: {
- show: false,
- device: null,
- timeRange: '24h',
- activeIndicator: 'all'
- },
- // 时间范围选项
- timeRanges: [
- { label: '近24小时', value: '24h' },
- { label: '近7天', value: '7d' },
- { label: '近30天', value: '30d' }
- ],
- // 图表实例
- chartInstance: null,
- // 原始样式保存
- originalBodyBg: '',
- originalBodyColor: '',
- originalAppBg: '',
- // 轮播控制状态
- carouselStates: {},
- carouselTimers: {},
- deviceList: [
- {
- id: 1,
- name: '土壤气象监测器 #1',
- location: '第一家葡萄园',
- status: 'online',
- statusText: '在线',
- lastUpdate: '2分钟前更新',
- type: 'soil',
- metrics: [
- { name: '气温', value: '30.5', unit: '℃', isAlert: false },
- { name: '湿度', value: '93.1', unit: '%', isAlert: false },
- { name: '降雨量', value: '0.6', unit: 'mm', isAlert: false },
- { name: '风向', value: '北', unit: '', isAlert: false },
- { name: '风速', value: '1.2', unit: 'm/s', isAlert: false },
- { name: '气压', value: '994.00', unit: 'hPa', isAlert: false },
- { name: '光照强度', value: '-230.8', unit: 'lux', isAlert: false },
- { name: '土壤湿度', value: '22.10', unit: '%', isAlert: false },
- { name: '土壤温度', value: '27.10', unit: '℃', isAlert: false },
- { name: '氮含量', value: '1', unit: 'mg/kg', isAlert: false },
- { name: '磷含量', value: '1', unit: 'mg/kg', isAlert: false },
- { name: '钾含量', value: '0', unit: 'mg/kg', isAlert: false }
- ]
- },
- /* {
- id: 2,
- name: '水质监测器 #2',
- location: '西区1号地块',
- status: 'warning',
- statusText: '告警',
- lastUpdate: 'pH值超标',
- type: 'water',
- metrics: [
- { name: 'pH值', value: '8.5', unit: '', isAlert: true },
- { name: '溶解氧', value: '6.2', unit: 'mg/L', isAlert: false },
- { name: '电导率', value: '1.2', unit: 'mS/cm', isAlert: false },
- { name: '浊度', value: '15', unit: 'NTU', isAlert: false },
- { name: '氨氮', value: '0.8', unit: 'mg/L', isAlert: false }
- ]
- }, */
- {
- id: 3,
- name: '气象土壤监测器 #2',
- location: '东区气象站',
- status: 'online',
- statusText: '在线',
- lastUpdate: '1分钟前更新',
- type: 'weather',
- metrics: [
- { name: '温度', value: '26.5', unit: '℃', isAlert: false },
- { name: '湿度', value: '68', unit: '%', isAlert: false },
- { name: '光照强度', value: '45000', unit: 'lux', isAlert: false },
- { name: '风速', value: '2.3', unit: 'm/s', isAlert: false },
- { name: '降雨量', value: '0', unit: 'mm', isAlert: false },
- { name: '气压', value: '1013', unit: 'hPa', isAlert: false }
- ]
- },
- /*{
- id: 4,
- name: '土壤监测器 #4',
- location: '西区2号地块',
- status: 'offline',
- statusText: '离线',
- lastUpdate: '设备离线 > 24h',
- type: 'soil',
- metrics: [
- { name: '土壤湿度', value: '--', unit: '%', isAlert: false },
- { name: '土壤温度', value: '--', unit: '℃', isAlert: false }
- ]
- },
- {
- id: 5,
- name: '水质监测器 #3',
- location: '东区2号地块',
- status: 'online',
- statusText: '在线',
- lastUpdate: '5分钟前更新',
- type: 'water',
- metrics: [
- { name: 'pH值', value: '7.2', unit: '', isAlert: false },
- { name: '溶解氧', value: '5.8', unit: 'mg/L', isAlert: false },
- { name: '电导率', value: '0.9', unit: 'mS/cm', isAlert: false },
- { name: '浊度', value: '8', unit: 'NTU', isAlert: false }
- ]
- },
- {
- id: 6,
- name: '气象监测器 #2',
- location: '西区气象站',
- status: 'warning',
- statusText: '告警',
- lastUpdate: '温度过高',
- type: 'weather',
- metrics: [
- { name: '温度', value: '35.8', unit: '℃', isAlert: true },
- { name: '湿度', value: '45', unit: '%', isAlert: false },
- { name: '光照强度', value: '52000', unit: 'lux', isAlert: false },
- { name: '风速', value: '4.2', unit: 'm/s', isAlert: false },
- { name: '降雨量', value: '2.5', unit: 'mm', isAlert: false }
- ]
- },
- {
- id: 7,
- name: '土壤监测器 #5',
- location: '南区1号地块',
- status: 'online',
- statusText: '在线',
- lastUpdate: '3分钟前更新',
- type: 'soil',
- metrics: [
- { name: '土壤湿度', value: '28.5', unit: '%', isAlert: false },
- { name: '土壤温度', value: '22.8', unit: '℃', isAlert: false },
- { name: 'EC值', value: '2.1', unit: 'mS/cm', isAlert: false },
- { name: 'P含量', value: '38', unit: 'mg/kg', isAlert: false },
- { name: 'K含量', value: '125', unit: 'mg/kg', isAlert: false }
- ]
- },
- {
- id: 8,
- name: '水质监测器 #4',
- location: '南区水质站',
- status: 'online',
- statusText: '在线',
- lastUpdate: '1分钟前更新',
- type: 'water',
- metrics: [
- { name: 'pH值', value: '7.1', unit: '', isAlert: false },
- { name: '溶解氧', value: '6.5', unit: 'mg/L', isAlert: false },
- { name: '电导率', value: '1.1', unit: 'mS/cm', isAlert: false },
- { name: '温度', value: '23.5', unit: '℃', isAlert: false },
- { name: '浊度', value: '5', unit: 'NTU', isAlert: false }
- ]
- },
- {
- id: 9,
- name: '土壤监测器 #6',
- location: '北区1号地块',
- status: 'offline',
- statusText: '离线',
- lastUpdate: '设备离线 > 12h',
- type: 'soil',
- metrics: [
- { name: '土壤湿度', value: '--', unit: '%', isAlert: false },
- { name: '土壤温度', value: '--', unit: '℃', isAlert: false }
- ]
- },
- {
- id: 10,
- name: '气象监测器 #3',
- location: '北区气象站',
- status: 'online',
- statusText: '在线',
- lastUpdate: '30秒前更新',
- type: 'weather',
- metrics: [
- { name: '温度', value: '28.2', unit: '℃', isAlert: false },
- { name: '湿度', value: '72', unit: '%', isAlert: false },
- { name: '光照强度', value: '38000', unit: 'lux', isAlert: false },
- { name: '风速', value: '1.8', unit: 'm/s', isAlert: false },
- { name: '气压', value: '1015', unit: 'hPa', isAlert: false },
- { name: 'UV指数', value: '8', unit: '', isAlert: false }
- ]
- } */
- ]
- }
- },
- computed: {
- // 摄像头分页
- pageCameras() {
- const start = (this.pageIndex - 1) * this.pageSize
- const end = start + this.pageSize
- return this.cameraList.slice(start, end)
- },
- totalCameraPages() {
- return Math.ceil(this.cameraList.length / this.pageSize)
- },
-
- // 网格全屏模式计算属性
- gridTotalPages() {
- return Math.ceil(this.cameraList.length / this.gridFullscreen.videosPerPage)
- },
- gridPaginatedVideos() {
- const start = (this.gridFullscreen.currentPage - 1) * this.gridFullscreen.videosPerPage
- const end = start + this.gridFullscreen.videosPerPage
- return this.cameraList.slice(start, end)
- },
- gridPageRange() {
- const start = (this.gridFullscreen.currentPage - 1) * this.gridFullscreen.videosPerPage + 1
- const end = Math.min(this.gridFullscreen.currentPage * this.gridFullscreen.videosPerPage, this.cameraList.length)
- return `${start}-${end}`
- },
-
- // 设备监控计算属性
- filteredDevices() {
- let devices = this.deviceList
- // 按状态过滤
- if (this.deviceFilter !== 'all') {
- devices = devices.filter(device => device.status === this.deviceFilter)
- }
- // 按类型过滤
- if (this.deviceTypeFilter !== 'all') {
- devices = devices.filter(device => device.type === this.deviceTypeFilter)
- }
- // 排序
- if (this.sortBy === 'alert') {
- devices = devices.sort((a, b) => {
- const statusOrder = { warning: 0, offline: 1, online: 2 }
- return statusOrder[a.status] - statusOrder[b.status]
- })
- } else if (this.sortBy === 'name') {
- devices = devices.sort((a, b) => a.name.localeCompare(b.name))
- } else if (this.sortBy === 'time') {
- devices = devices.sort((a, b) => {
- // 简单的时间排序逻辑,实际应该根据真实时间戳
- const getUpdateTime = (update) => {
- if (update.includes('分钟前')) return parseInt(update.match(/\d+/)[0])
- if (update.includes('刚刚')) return 0
- return 999 // 其他情况放最后
- }
- return getUpdateTime(a.lastUpdate) - getUpdateTime(b.lastUpdate)
- })
- }
- return devices
- },
- deviceTotalPages() {
- return Math.ceil(this.filteredDevices.length / this.devicePerPage)
- },
- paginatedDevices() {
- const start = (this.deviceCurrentPage - 1) * this.devicePerPage
- const end = start + this.devicePerPage
- return this.filteredDevices.slice(start, end)
- }
- },
- methods: {
- refreshData() {
- console.log('刷新数据')
- this.lastUpdateTime = '刚刚'
- // 这里可以添加实际的数据刷新逻辑
- },
- prevPage() {
- if (this.pageIndex > 1) {
- this.pageIndex--
- this.$nextTick(() => {
- this.initSdkPlayer()
- })
- }
- },
- nextPage() {
- if (this.pageIndex < this.totalCameraPages) {
- this.pageIndex++
- this.$nextTick(() => {
- this.initSdkPlayer()
- })
- }
- },
- getPageRange() {
- const start = (this.pageIndex - 1) * this.pageSize + 1
- const end = Math.min(this.pageIndex * this.pageSize, this.cameraList.length)
- return `${start}-${end}`
- },
- toggleFullscreen() {
- console.log('切换网格全屏模式')
- this.gridFullscreen.show = true
- },
- exitGridFullscreen() {
- this.gridFullscreen.show = false
- this.gridFullscreen.currentPage = 1
- },
- // 网格全屏分页方法
- gridPrevPage() {
- if (this.gridFullscreen.currentPage > 1) {
- this.gridFullscreen.currentPage--
- }
- },
- gridNextPage() {
- if (this.gridFullscreen.currentPage < this.gridTotalPages) {
- this.gridFullscreen.currentPage++
- }
- },
- openVideoFullscreen(camera) {
- console.log('打开单个视频全屏:', camera.name)
- console.log('Camera data:', camera)
- // 如果是从网格全屏模式进入,先关闭网格全屏
- if (this.gridFullscreen.show) {
- this.gridFullscreen.show = false
- }
- this.singleCameraFullscreen.show = true
- this.singleCameraFullscreen.camera = camera
- // 确保数据更新后再强制更新视图
- this.$nextTick(() => {
- console.log('Fullscreen camera:', this.singleCameraFullscreen.camera)
- })
- },
- exitSingleFullscreen() {
- this.singleCameraFullscreen.show = false
- this.singleCameraFullscreen.camera = null
- },
- // 设备监控方法
- setFilter(filter) {
- this.deviceFilter = filter
- this.deviceCurrentPage = 1 // 重置到第一页
- },
- setTypeFilter(type) {
- this.deviceTypeFilter = this.deviceTypeFilter === type ? 'all' : type
- this.deviceCurrentPage = 1 // 重置到第一页
- },
- getDeviceCountByStatus(status) {
- return this.deviceList.filter(device => device.status === status).length
- },
- // 摄像头统计方法
- getOnlineCameraCount() {
- return this.cameraList.filter(camera => camera.status === '在线').length
- },
- getOfflineCameraCount() {
- return this.cameraList.filter(camera => camera.status === '离线').length
- },
- // 打开摄像头预览
- openCameraPreview(camera) {
- this.currentCamera = camera
- this.previewVisible = true
- },
- // 获取摄像头列表
- async fetchCameras() {
- try {
- // 这里调用真实接口
- // const response = await getCameraList()
- // this.cameraList = response.data.map(item => ({
- // id: item.id,
- // name: item.name,
- // status: item.status === 1 ? '在线' : '离线',
- // location: item.location,
- // previewUrl: item.previewUrl,
- // domId: item.domId
- // }))
-
- // 兜底 mock 数据
- this.cameraList = [
- {
- id: 1,
- name: '合和智行',
- status: '在线',
- location: '智慧农场六号',
- previewUrl: 'http://121.4.16.100:28080/#/play/wasm/' + encodeURIComponent('ws://121.4.16.100:6080/rtp/34020000001110000001_34020000001320000012.live.flv'),
- domId: 'camera-1'
- },
- {
- id: 2,
- name: '葡萄园一号摄像头',
- status: '在线',
- location: '智慧农场六号',
- previewUrl: 'http://121.4.16.100:28080/#/play/wasm/' + encodeURIComponent('ws://121.4.16.100:6080/rtp/34020000001320000001.live.flv'),
- domId: 'camera-2'
- },
- {
- id: 3,
- name: '东区3号摄像头',
- status: '离线',
- location: '东区3号地块',
- previewUrl: '',
- domId: 'camera-3'
- },
- {
- id: 4,
- name: '西区1号摄像头',
- status: '在线',
- location: '西区1号地块',
- previewUrl: '',
- domId: 'camera-4'
- },
- {
- id: 5,
- name: '西区2号摄像头',
- status: '在线',
- location: '西区2号地块',
- previewUrl: '',
- domId: 'camera-5'
- },
- {
- id: 6,
- name: '西区3号摄像头',
- status: '在线',
- location: '西区3号地块',
- previewUrl: '',
- domId: 'camera-6'
- },
- {
- id: 7,
- name: '南区1号摄像头',
- status: '在线',
- location: '南区1号地块',
- previewUrl: '',
- domId: 'camera-7'
- },
- {
- id: 8,
- name: '南区2号摄像头',
- status: '离线',
- location: '南区2号地块',
- previewUrl: '',
- domId: 'camera-8'
- },
- {
- id: 9,
- name: '南区3号摄像头',
- status: '在线',
- location: '南区3号地块',
- previewUrl: '',
- domId: 'camera-9'
- },
- {
- id: 10,
- name: '北区1号摄像头',
- status: '在线',
- location: '北区1号地块',
- previewUrl: '',
- domId: 'camera-10'
- },
- {
- id: 11,
- name: '北区2号摄像头',
- status: '在线',
- location: '北区2号地块',
- previewUrl: '',
- domId: 'camera-11'
- },
- {
- id: 12,
- name: '北区3号摄像头',
- status: '在线',
- location: '北区3号地块',
- previewUrl: '',
- domId: 'camera-12'
- }
- ]
-
- // 初始化当前页的 SDK 播放器
- this.$nextTick(() => {
- this.initSdkPlayer()
- })
-
- } catch (error) {
- console.error('获取摄像头列表失败:', error)
- // 接口超时兜底 mock
- this.cameraList = [
- { id: 1, name: '东区1号摄像头', status: '在线', location: '东区1号地块', previewUrl: '', domId: 'camera-1' },
- { id: 2, name: '东区2号摄像头', status: '在线', location: '东区2号地块', previewUrl: '', domId: 'camera-2' },
- { id: 3, name: '东区3号摄像头', status: '离线', location: '东区3号地块', previewUrl: '', domId: 'camera-3' }
- ]
- }
- },
- // 初始化 SDK 播放器
- initSdkPlayer() {
- this.pageCameras.forEach(cam => {
- if (cam.domId && cam.status === '在线' && !cam.previewUrl) {
- // 这里调用厂商 SDK 初始化逻辑
- // 例如:initHikvisionPlayer(cam.domId, cam.streamUrl)
- console.log(`初始化摄像头 ${cam.id} 的 SDK 播放器,容器ID: ${cam.domId}`)
- }
- })
- },
- // 处理预览
- handlePreview(cam) {
- this.currentCamera = {
- id: cam.id,
- name: cam.name,
- location: cam.location,
- status: cam.status === '在线' ? 'online' : 'offline'
- }
- this.previewVisible = true
- },
- // 处理全屏
- handleFullscreen(cam) {
- // 简化实现:通过打开对话框实现
- this.handlePreview(cam)
- },
- prevDevicePage() {
- if (this.deviceCurrentPage > 1) {
- this.deviceCurrentPage--
- }
- },
- nextDevicePage() {
- if (this.deviceCurrentPage < this.deviceTotalPages) {
- this.deviceCurrentPage++
- }
- },
- getDevicePageRange() {
- if (this.filteredDevices.length === 0) return '0-0'
- const start = (this.deviceCurrentPage - 1) * this.devicePerPage + 1
- const end = Math.min(this.deviceCurrentPage * this.devicePerPage, this.filteredDevices.length)
- return `${start}-${end}`
- },
- showDeviceHistory(device) {
- console.log('显示设备数据:', device.name)
- this.historyModal.show = true
- this.historyModal.device = device
- this.historyModal.device.type = 'soil'
- this.historyModal.timeRange = '24h'
- this.historyModal.activeIndicator = 'all'
- // 等待模态框渲染完成后初始化图表
- this.$nextTick(() => {
- this.initChart()
- })
- },
- closeHistoryModal() {
- this.historyModal.show = false
- this.historyModal.device = null
- // 销毁图表实例
- if (this.chartInstance) {
- this.chartInstance.dispose()
- this.chartInstance = null
- }
- },
- setTimeRange(range) {
- this.historyModal.timeRange = range
- this.updateChart()
- },
- setActiveIndicator(indicator) {
- this.historyModal.activeIndicator = indicator
- this.updateChart()
- },
- getDeviceIndicators() {
- if (!this.historyModal.device) return []
- const indicators = {
- soil: [
- { label: '所有指标', value: 'all' },
- { label: '土壤湿度', value: 'soilHumidity' },
- { label: '土壤温度', value: 'soilTemperature' },
- { label: 'EC值', value: 'ec' }
- ],
- water: [
- { label: '所有指标', value: 'all' },
- { label: 'pH值', value: 'ph' },
- { label: '溶解氧', value: 'oxygen' },
- { label: '电导率', value: 'conductivity' },
- { label: '浊度', value: 'turbidity' }
- ],
- weather: [
- { label: '所有指标', value: 'all' },
- { label: '温度', value: 'temperature' },
- { label: '湿度', value: 'humidity' },
- { label: '光照', value: 'light' },
- { label: '风速', value: 'windSpeed' },
- { label: '降雨量', value: 'rainfall' }
- ]
- }
- return indicators[this.historyModal.device.type] || indicators.soil
- },
- initChart() {
- if (!this.$refs.chartContainer) return
- // 销毁已存在的图表实例
- if (this.chartInstance) {
- this.chartInstance.dispose()
- this.chartInstance = null
- }
- // 创建新的图表实例
- this.chartInstance = this.$echarts.init(this.$refs.chartContainer)
- this.updateChart()
- },
- updateChart() {
- if (!this.chartInstance) return
- // 生成模拟数据
- const data = this.generateMockData()
- console.log("data");
- console.log(data);
- const seriesNames = this.getSeriesNames()
- console.log("seriesNames");
- console.log(seriesNames);
- const option = {
- backgroundColor: 'transparent',
- tooltip: {
- trigger: 'axis',
- backgroundColor: 'rgba(255, 255, 255, 0.95)',
- borderColor: '#e5e7eb',
- textStyle: { color: '#1f2937' }
- },
- legend: {
- data: seriesNames,
- textStyle: { color: '#6b7280' },
- top: 0
- },
- grid: {
- left: '3%',
- right: '4%',
- bottom: '3%',
- containLabel: true,
- top: 40
- },
- xAxis: {
- type: 'category',
- boundaryGap: false,
- data: data.times,
- axisLine: { lineStyle: { color: '#d1d5db' } },
- axisLabel: { color: '#6b7280' }
- },
- yAxis: {
- type: 'value',
- axisLine: { lineStyle: { color: '#d1d5db' } },
- axisLabel: { color: '#6b7280' },
- splitLine: { lineStyle: { color: '#f3f4f6' } }
- },
- series: this.generateSeries(data)
- }
- this.chartInstance.setOption(option)
- },
- generateMockData() {
- const times = []
- const values = {}
- const points = this.historyModal.timeRange === '24h' ? 24 : this.historyModal.timeRange === '7d' ? 7 : 30
- const now = new Date()
- // 根据设备类型生成不同的数据
- const ranges = this.getDataRanges()
- console.log("generateMockData");
- console.log(ranges);
- for (let i = points - 1; i >= 0; i--) {
- const time = new Date(now - i * (this.historyModal.timeRange === '24h' ? 3600000 : 86400000))
- times.push(this.historyModal.timeRange === '24h' ?
- time.getHours() + ':00' :
- (time.getMonth() + 1) + '/' + time.getDate())
- ranges.forEach((range, index) => {
- if (!values[`values${index + 1}`]) {
- values[`values${index + 1}`] = []
- }
- values[`values${index + 1}`].push(
- (Math.random() * (range[1] - range[0]) + range[0]).toFixed(1)
- )
- })
- }
- return { times, ...values }
- },
- getDataRanges() {
- const ranges = {
- soil: [[20, 40], [15, 30], [0.5, 2]], // 土壤湿度, 土壤温度, EC值
- water: [[6.5, 8.5], [4, 8], [0.5, 2], [0, 10]], // pH值, 溶解氧, 电导率, 浊度
- weather: [[15, 35], [40, 80], [0, 1000], [0, 10], [0, 50], [0, 10], [0, 10], [0, 10]] // 温度, 湿度, 光照, 风速, 降雨量,土壤氮,土壤磷,土壤钾
- }
- return ranges[this.historyModal.device.type] || ranges.soil
- },
- getSeriesNames() {
- const names = {
- soil: ['土壤湿度', '土壤温度', 'EC值'],
- water: ['pH值', '溶解氧', '电导率', '浊度'],
- weather: ['温度', '湿度', '光照', '风速', '降雨量','土壤氮','土壤磷','土壤钾']
- }
- const allNames = names[this.historyModal.device.type] || names.soil
- if (this.historyModal.activeIndicator === 'all') {
- return allNames
- } else {
- const indicatorMap = {
- soilHumidity: '土壤湿度',
- soilTemperature: '土壤温度',
- ec: 'EC值',
- ph: 'pH值',
- oxygen: '溶解氧',
- conductivity: '电导率',
- turbidity: '浊度',
- temperature: '温度',
- humidity: '湿度',
- light: '光照',
- windSpeed: '风速',
- rainfall: '降雨量'
- }
- return [indicatorMap[this.historyModal.activeIndicator] || allNames[0]]
- }
- },
- generateSeries(data) {
- console.log("generateSeries_data");
- console.log(data);
- console.log(this.historyModal.device.type);
- console.log(this.historyModal.activeIndicator);
- const deviceType = this.historyModal.device.type
- const activeIndicator = this.historyModal.activeIndicator
- const indicators = {
- soil: {
- names: ['土壤湿度', '土壤温度', 'EC值'],
- colors: ['#10b981', '#34d399', '#f59e0b']
- },
- water: {
- names: ['pH值', '溶解氧', '电导率', '浊度'],
- colors: ['#10b981', '#34d399', '#6ee7b7', '#f59e0b']
- },
- weather: {
- names: ['温度', '湿度', '光照', '风速', '降雨量', '土壤氮', '土壤磷', '土壤钾'],
- colors: ['#10b981', '#34d399', '#6ee7b7', '#a7f3d0', '#f59e0b', '#a7f3d2', '#a7f3d3', '#a7f3d4']
- }
- }[deviceType] || { names: [], colors: [] }
- console.log(indicators);
- if (activeIndicator === 'all') {
- return indicators.names.map((name, index) => ({
- name: name,
- type: 'line',
- data: data[`values${index + 1}`] || [],
- smooth: true,
- showSymbol: false,
- lineStyle: { width: 3 },
- areaStyle: {
- color: {
- type: 'linear',
- x: 0, y: 0, x2: 0, y2: 1,
- colorStops: [
- { offset: 0, color: indicators.colors[index] + '50' },
- { offset: 0.8, color: indicators.colors[index] + '20' },
- { offset: 1, color: indicators.colors[index] + '05' }
- ]
- }
- },
- itemStyle: { color: indicators.colors[index] }
- }))
- } else {
- const index = indicators.names.indexOf(this.getIndicatorLabel(activeIndicator))
- const color = indicators.colors[index]
- return [{
- name: this.getIndicatorLabel(activeIndicator),
- type: 'line',
- data: data[`values${index + 1}`] || [],
- smooth: true,
- showSymbol: false,
- lineStyle: { width: 3 },
- areaStyle: {
- color: {
- type: 'linear',
- x: 0, y: 0, x2: 0, y2: 1,
- colorStops: [
- { offset: 0, color: color + '60' },
- { offset: 0.7, color: color + '30' },
- { offset: 1, color: color + '08' }
- ]
- }
- },
- itemStyle: { color: color }
- }]
- }
- },
- getIndicatorLabel(value) {
- const indicatorMap = {
- soilHumidity: '土壤湿度',
- soilTemperature: '土壤温度',
- ec: 'EC值',
- ph: 'pH值',
- oxygen: '溶解氧',
- conductivity: '电导率',
- turbidity: '浊度',
- temperature: '温度',
- humidity: '湿度',
- light: '光照',
- windSpeed: '风速',
- rainfall: '降雨量'
- }
- return indicatorMap[value] || value
- },
- // 轮播控制方法
- initializeCarousels() {
- this.deviceList.forEach(device => {
- if (device.metrics.length > 2) {
- this.$set(this.carouselStates, device.id, 0)
- this.startCarouselTimer(device.id)
- }
- })
- },
- startCarouselTimer(deviceId) {
- if (this.carouselTimers[deviceId]) {
- clearInterval(this.carouselTimers[deviceId])
- }
- this.carouselTimers[deviceId] = setInterval(() => {
- this.nextMetric(deviceId)
- }, 3000) // 3秒切换一次
- },
- pauseCarousel(deviceId) {
- if (this.carouselTimers[deviceId]) {
- clearInterval(this.carouselTimers[deviceId])
- this.carouselTimers[deviceId] = null
- }
- },
- resumeCarousel(deviceId) {
- const device = this.deviceList.find(d => d.id === deviceId)
- if (device && device.metrics.length > 2) {
- this.startCarouselTimer(deviceId)
- }
- },
- nextMetric(deviceId) {
- const device = this.deviceList.find(d => d.id === deviceId)
- if (!device || device.metrics.length <= 2) return
- const totalGroups = Math.ceil(device.metrics.length / 2)
- const currentIndex = this.carouselStates[deviceId] || 0
- const nextIndex = (currentIndex + 1) % totalGroups
- this.$set(this.carouselStates, deviceId, nextIndex)
- },
- getCurrentMetricIndex(deviceId) {
- return this.carouselStates[deviceId] || 0
- },
- getCurrentMetrics(device) {
- if (device.metrics.length <= 2) {
- return device.metrics
- }
- const currentIndex = this.carouselStates[device.id] || 0
- const startIndex = currentIndex * 2
- return device.metrics.slice(startIndex, startIndex + 2)
- }
- },
- mounted() {
- // 保存原始背景色和设置浅色主题
- this.originalBodyBg = document.body.style.backgroundColor || ''
- this.originalBodyColor = document.body.style.color || ''
- this.originalAppBg = document.getElementById('app')?.style.backgroundColor || ''
- // 应用浅色主题
- document.body.style.backgroundColor = '#f8fafc'
- document.body.style.color = '#1f2937'
- const appEl = document.getElementById('app')
- if (appEl) {
- appEl.style.backgroundColor = '#f8fafc'
- }
- // 初始化轮播
- this.initializeCarousels()
- // 获取摄像头列表
- this.fetchCameras()
- // 监听键盘事件
- document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape') {
- if (this.historyModal.show) {
- this.closeHistoryModal()
- } else if (this.singleCameraFullscreen.show) {
- this.exitSingleFullscreen()
- } else if (this.gridFullscreen.show) {
- this.exitGridFullscreen()
- }
- } else if (this.gridFullscreen.show) {
- // 网格全屏模式下的键盘快捷键
- if (e.key === 'ArrowLeft') {
- e.preventDefault()
- this.gridPrevPage()
- } else if (e.key === 'ArrowRight') {
- e.preventDefault()
- this.gridNextPage()
- } else if (e.key >= '1' && e.key <= '9') {
- // 数字键快速跳转页面
- const pageNum = parseInt(e.key)
- if (pageNum <= this.gridTotalPages) {
- this.gridFullscreen.currentPage = pageNum
- }
- }
- }
- })
- const query = { deviceId: '34020000001110000001' };
- const query2 = { deviceId: '34020000001320000001' };
- playback(query).then(response => {
- })
- playback(query2).then(response => {
-
- this.iframeSrc = "http://121.4.16.100:28080/#/play/wasm/"+encodeURIComponent(response.msg);
-
- })
- },
- beforeDestroy() {
- // 清理所有轮播定时器
- Object.values(this.carouselTimers).forEach(timer => {
- if (timer) {
- clearInterval(timer)
- }
- })
- this.carouselTimers = {}
- // 恢复原始背景色
- document.body.style.backgroundColor = this.originalBodyBg
- document.body.style.color = this.originalBodyColor
- const appEl = document.getElementById('app')
- if (appEl) {
- appEl.style.backgroundColor = this.originalAppBg
- }
- // 销毁图表实例
- if (this.chartInstance) {
- this.chartInstance.dispose()
- this.chartInstance = null
- }
- const query = { deviceId: '34020000001110000001' };
- const query2 = { deviceId: '34020000001320000001' };
- stopPlaying(query).then(response => {
- console.log(response)
- })
- stopPlaying(query2).then(response => {
- console.log(response)
- })
- }
- }
- </script>
- <style scoped>
- .device-monitor-container {
- padding: 24px;
- background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
- color: #1f2937;
- min-height: 100vh;
- }
- /* 控制面板 */
- .device-monitor-container > .control-panel {
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 16px;
- padding: 1.5rem;
- margin-bottom: 2rem;
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 2rem;
- transition: all 0.3s ease;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
- }
- .device-monitor-container > .control-panel:hover {
- transform: translateY(-4px);
- box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.15);
- border-color: #10b981;
- }
- .control-left {
- display: flex;
- gap: 1.5rem;
- flex: 1;
- }
- .form-group {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- }
- .form-group label {
- font-size: 0.875rem;
- color: #6b7280;
- white-space: nowrap;
- }
- .form-group select {
- background-color: white;
- border: 1px solid #d1d5db;
- color: #1f2937;
- padding: 8px 12px;
- border-radius: 8px;
- min-width: 150px;
- }
- .form-group select:focus {
- outline: none;
- border-color: #10b981;
- box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
- }
- .control-right {
- display: flex;
- align-items: center;
- gap: 1rem;
- }
- .refresh-btn {
- background-color: #10b981;
- color: white;
- border: none;
- padding: 10px 20px;
- border-radius: 8px;
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 8px;
- font-weight: 500;
- transition: all 0.2s ease;
- }
- .refresh-btn:hover {
- background-color: #059669;
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
- }
- .auto-refresh {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- font-size: 0.875rem;
- color: #6b7280;
- }
- .auto-refresh-select {
- background-color: white;
- border: 1px solid #d1d5db;
- color: #1f2937;
- padding: 4px 8px;
- border-radius: 6px;
- font-size: 0.875rem;
- }
- .auto-refresh-select:focus {
- outline: none;
- border-color: #10b981;
- box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
- }
- .last-update {
- font-size: 0.875rem;
- color: #6b7280;
- }
- .last-update span {
- color: #1f2937;
- font-weight: 500;
- }
- /* 统计容器 */
- .stats-container {
- display: grid;
- grid-template-columns: 2fr 5fr;
- gap: 1rem;
- margin-bottom: 2rem;
- }
- /* 综合统计卡片 */
- .overview-stat-card {
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 16px;
- padding: 1.5rem;
- transition: all 0.3s ease;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
- }
- .overview-stat-card:hover {
- transform: translateY(-4px);
- box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.15);
- border-color: #10b981;
- }
- .overview-stats {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 1.5rem;
- }
- .overview-item {
- text-align: center;
- }
- /* 分类统计区域 */
- .category-stats {
- display: grid;
- grid-template-columns: repeat(5, 1fr);
- gap: 1rem;
- }
- .stat-card {
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- padding: 1.5rem;
- text-align: center;
- transition: all 0.3s ease;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
- .stat-card:hover {
- transform: translateY(-4px);
- box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.15);
- border-color: #10b981;
- }
- .stat-card.alert {
- border-color: #ef4444;
- background: linear-gradient(to right, rgba(239, 68, 68, 0.05), transparent);
- animation: pulse 2s infinite;
- }
- @keyframes pulse {
- 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
- 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
- 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
- }
- .stat-number {
- font-size: 2rem;
- font-weight: 700;
- margin-bottom: 0.5rem;
- }
- .stat-number.online { color: #10b981; }
- .stat-number.offline { color: #ef4444; }
- .stat-number.alert { color: #ef4444; }
- .stat-number.camera { color: #3b82f6; }
- .stat-number.sensor { color: #10b981; }
- .stat-number.weather { color: #8b5cf6; }
- .stat-number.control { color: #f59e0b; }
- .stat-label {
- font-size: 0.875rem;
- color: #6b7280;
- margin-bottom: 0.25rem;
- font-weight: 500;
- }
- .stat-detail {
- font-size: 0.75rem;
- color: #9ca3af;
- }
- .stat-detail .processed {
- color: #10b981;
- }
- /* 主要内容区域 */
- .main-content {
- display: flex;
- flex-direction: column;
- gap: 2rem;
- }
- .content-section {
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 16px;
- padding: 24px;
- transition: all 0.3s ease;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
- }
- .content-section:hover {
- transform: translateY(-4px);
- box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.15);
- border-color: #10b981;
- }
- .section-title {
- font-size: 1.25rem;
- font-weight: 600;
- margin-bottom: 0;
- color: #1f2937;
- }
- /* 视频头部样式 */
- .video-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1.5rem;
- }
- .video-header-left {
- display: flex;
- align-items: baseline;
- gap: 1rem;
- }
- .video-count {
- font-size: 0.875rem;
- color: #6b7280;
- }
- .video-header-right {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- }
- .video-nav-controls {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- }
- .video-nav-btn {
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: white;
- border: 1px solid #d1d5db;
- border-radius: 8px;
- color: #6b7280;
- transition: all 0.2s ease;
- cursor: pointer;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
- .video-nav-btn:hover:not(:disabled) {
- background: #10b981;
- color: white;
- border-color: #10b981;
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
- }
- .video-nav-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- .page-indicator {
- padding: 0 12px;
- height: 32px;
- display: flex;
- align-items: center;
- background: white;
- border: 1px solid #d1d5db;
- border-radius: 8px;
- color: #6b7280;
- font-size: 14px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
- /* 视频容器样式 */
- .video-container {
- padding: 0;
- }
- /* 摄像头网格 */
- .camera-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- grid-gap: 16px;
- margin-top: 12px;
- }
- .camera-card {
- background: #fff;
- border: 1px solid #eef1f5;
- border-radius: 12px;
- box-shadow: 0 2px 8px rgba(0,0,0,.04);
- overflow: hidden;
- display: flex;
- flex-direction: column;
- }
- .card-header {
- padding: 12px 16px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .card-footer {
- padding: 12px 16px;
- }
- .card-header .title {
- font-weight: 600;
- color: #1f2937;
- }
- .card-preview {
- height: 180px; /* 统一预览高度,避免卡片跳动 */
- background: #f6f8fa;
- position: relative;
- }
- .player-iframe, .player-sdk {
- position: absolute;
- inset: 0;
- width: 100%;
- height: 100%;
- border: none;
- }
- .player-empty {
- position: absolute;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #8c9aa3;
- gap: 6px;
- flex-direction: column;
- }
- .player-offline {
- position: absolute;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #f5f5f5;
- color: #9ca3af;
- gap: 8px;
- flex-direction: column;
- }
- .offline-camera-icon {
- width: 48px;
- height: 48px;
- color: #9ca3af;
- stroke-width: 1.5;
- }
- .offline-text {
- font-size: 14px;
- color: #6b7280;
- font-weight: 500;
- }
- .card-footer .location {
- color: #6b7280;
- font-size: 14px;
- }
- @media (max-width: 1200px) {
- .camera-grid {
- grid-template-columns: repeat(2, 1fr);
- }
- }
- @media (max-width: 768px) {
- .camera-grid {
- grid-template-columns: 1fr;
- }
- }
- /* CameraPreview 预览对话框样式 */
- .camera-preview-modal {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.8);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 10000;
- padding: 20px;
- }
- .preview-dialog {
- background: white;
- border-radius: 16px;
- width: 100%;
- max-width: 1200px;
- max-height: 90vh;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2);
- }
- .preview-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 20px 24px;
- border-bottom: 1px solid #e5e7eb;
- background: #f8fafc;
- }
- .preview-title {
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .preview-title h3 {
- margin: 0;
- font-size: 18px;
- font-weight: 600;
- color: #1f2937;
- }
- .preview-status {
- font-size: 12px;
- padding: 4px 8px;
- border-radius: 12px;
- font-weight: 500;
- }
- .preview-status.online {
- background-color: rgba(16, 185, 129, 0.15);
- color: #059669;
- border: 1px solid rgba(16, 185, 129, 0.4);
- }
- .preview-status.offline {
- background-color: rgba(239, 68, 68, 0.15);
- color: #dc2626;
- border: 1px solid rgba(239, 68, 68, 0.4);
- }
- .close-btn {
- width: 40px;
- height: 40px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: transparent;
- border: 1px solid #d1d5db;
- border-radius: 8px;
- color: #6b7280;
- cursor: pointer;
- transition: all 0.2s ease;
- }
- .close-btn:hover {
- background: #f3f4f6;
- border-color: #9ca3af;
- color: #374151;
- }
- .close-btn svg {
- width: 20px;
- height: 20px;
- }
- .preview-video-container {
- flex: 1;
- background: #000;
- position: relative;
- min-height: 400px;
- }
- .preview-video {
- width: 100%;
- height: 100%;
- object-fit: contain;
- }
- .preview-offline {
- position: absolute;
- inset: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- background: #f9fafb;
- }
- .preview-controls {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px 24px;
- background: #f8fafc;
- border-top: 1px solid #e5e7eb;
- }
- .controls-left,
- .controls-right {
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .control-btn {
- width: 40px;
- height: 40px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: white;
- border: 1px solid #d1d5db;
- border-radius: 8px;
- color: #6b7280;
- cursor: pointer;
- transition: all 0.2s ease;
- }
- .control-btn:hover {
- background: #10b981;
- border-color: #10b981;
- color: white;
- }
- .control-btn svg {
- width: 20px;
- height: 20px;
- }
- .volume-control {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .volume-control svg {
- width: 20px;
- height: 20px;
- color: #6b7280;
- }
- .volume-slider {
- width: 100px;
- height: 4px;
- background: #e5e7eb;
- border-radius: 2px;
- outline: none;
- appearance: none;
- }
- .volume-slider::-webkit-slider-thumb {
- appearance: none;
- width: 16px;
- height: 16px;
- background: #10b981;
- border-radius: 50%;
- cursor: pointer;
- }
- .volume-slider::-moz-range-thumb {
- width: 16px;
- height: 16px;
- background: #10b981;
- border-radius: 50%;
- cursor: pointer;
- border: none;
- }
- .camera-icon {
- position: absolute;
- width: 48px;
- height: 48px;
- color: #10b981;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- opacity: 0.7;
- }
- /* 强制居中的新样式 */
- .centered-container {
- display: flex !important;
- align-items: center !important;
- justify-content: center !important;
- }
- .centered-icon {
- position: relative !important;
- top: auto !important;
- left: auto !important;
- transform: none !important;
- margin: auto !important;
- }
- .video-info-overlay {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- background: linear-gradient(to top, rgba(248, 250, 252, 0.95), rgba(248, 250, 252, 0.8), rgba(248, 250, 252, 0.4), transparent);
- padding: 0.75rem;
- display: flex;
- justify-content: space-between;
- align-items: flex-end;
- z-index: 5;
- backdrop-filter: blur(8px);
- border-top: 1px solid rgba(229, 231, 235, 0.3);
- }
- .video-details {
- flex: 1;
- }
- .video-name {
- font-weight: 600;
- margin-bottom: 0.25rem;
- color: #1f2937;
- font-size: 0.875rem;
- text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
- }
- .video-location {
- font-size: 0.75rem;
- color: #6b7280;
- text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
- }
- .video-status {
- font-size: 0.75rem;
- padding: 4px 8px;
- border-radius: 12px;
- font-weight: 500;
- white-space: nowrap;
- backdrop-filter: blur(4px);
- }
- .video-status.online {
- background-color: rgba(16, 185, 129, 0.15);
- color: #059669;
- border: 1px solid rgba(16, 185, 129, 0.4);
- font-weight: 600;
- }
- .video-status.offline {
- background-color: rgba(239, 68, 68, 0.15);
- color: #dc2626;
- border: 1px solid rgba(239, 68, 68, 0.4);
- font-weight: 600;
- }
- /* 设备监控头部 */
- .device-monitor-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1.5rem;
- gap: 2rem;
- }
- .device-monitor-title-section {
- display: flex;
- align-items: center;
- gap: 1rem;
- flex: 1;
- }
- .section-title {
- margin: 0;
- line-height: 1.5;
- }
- .filter-buttons {
- display: flex;
- gap: 0.5rem;
- align-items: center;
- }
- .filter-divider {
- height: 24px;
- width: 1px;
- background-color: #e5e7eb;
- }
- .type-filters {
- display: flex;
- gap: 0.5rem;
- }
- .filter-btn {
- padding: 0.5rem 1rem;
- border-radius: 8px;
- font-size: 0.875rem;
- color: #6b7280;
- background: transparent;
- border: 1px solid #d1d5db;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- cursor: pointer;
- height: 36px; /* 固定高度确保对齐 */
- line-height: 1.5;
- }
- .filter-btn:hover {
- background: rgba(16, 185, 129, 0.05);
- border-color: #10b981;
- color: #059669;
- }
- .filter-btn.active {
- background: #10b981;
- border-color: #10b981;
- color: white;
- }
- .filter-count {
- font-size: 0.75rem;
- color: #9ca3af;
- }
- .filter-btn.active .filter-count {
- color: rgba(255, 255, 255, 0.8);
- }
- .status-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- display: inline-block;
- }
- .status-dot.warning {
- background-color: #f59e0b;
- }
- .status-dot.offline {
- background-color: #ef4444;
- }
- .device-monitor-controls {
- display: flex;
- align-items: center;
- gap: 1rem;
- }
- .sort-select {
- background-color: white;
- border: 1px solid #d1d5db;
- color: #1f2937;
- padding: 0.5rem 2rem 0.5rem 1rem;
- border-radius: 8px;
- font-size: 0.875rem;
- appearance: none;
- 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");
- background-repeat: no-repeat;
- background-position: right 0.75rem center;
- background-size: 1rem;
- height: 36px; /* 与按钮高度保持一致 */
- line-height: 1.5;
- }
- .sort-select:focus {
- outline: none;
- border-color: #10b981;
- box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
- }
- .device-pagination {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- }
- .page-btn {
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 8px;
- color: #6b7280;
- transition: all 0.2s ease;
- background: white;
- border: 1px solid #d1d5db;
- cursor: pointer;
- }
- .page-indicator {
- padding: 0 12px;
- height: 32px;
- display: flex;
- align-items: center;
- color: #6b7280;
- font-size: 14px;
- line-height: 1;
- font-weight: 500;
- }
- .page-btn:hover:not(:disabled) {
- background: #10b981;
- border-color: #10b981;
- color: white;
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
- }
- .page-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- /* 设备网格 */
- .device-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
- gap: 1rem;
- padding: 0.5rem;
- min-height: 400px;
- }
- @media (min-width: 1280px) {
- .device-grid {
- grid-template-columns: repeat(3, 1fr);
- }
- }
- @media (min-width: 1536px) {
- .device-grid {
- grid-template-columns: repeat(4, 1fr);
- }
- }
- .device-card {
- position: relative;
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- padding: 1.5rem;
- transition: all 0.3s ease;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- min-height: 250px;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- }
- /* 右上角数据按钮 */
- .data-btn {
- position: absolute;
- top: 1rem;
- right: 1rem;
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: white;
- border: 1px solid #d1d5db;
- border-radius: 8px;
- color: #6b7280;
- transition: all 0.2s ease;
- z-index: 1;
- cursor: pointer;
- }
- .data-btn:hover {
- background: #10b981;
- border-color: #10b981;
- color: white;
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
- }
- .data-btn svg {
- width: 16px;
- height: 16px;
- }
- .device-card:hover {
- transform: translateY(-4px);
- box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.15);
- border-color: #10b981;
- }
- .device-card.warning {
- border-color: #f59e0b;
- background: linear-gradient(to right, rgba(245, 158, 11, 0.05), rgba(245, 158, 11, 0.02));
- }
- .device-card.offline {
- opacity: 0.8;
- }
- .device-header {
- margin-bottom: 0.5rem;
- padding-right: 3rem; /* 为右上角数据按钮留出空间 */
- }
- .device-title-row {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- flex-wrap: wrap;
- }
- .device-name {
- font-weight: 600;
- color: #1f2937;
- font-size: 1.125rem;
- }
- .device-status {
- font-size: 0.75rem;
- padding: 4px 12px;
- border-radius: 12px;
- font-weight: 500;
- }
- .device-status.online {
- background-color: rgba(16, 185, 129, 0.1);
- color: #10b981;
- border: 1px solid rgba(16, 185, 129, 0.2);
- }
- .device-status.warning {
- background-color: rgba(245, 158, 11, 0.1);
- color: #f59e0b;
- border: 1px solid rgba(245, 158, 11, 0.2);
- }
- .device-status.offline {
- background-color: rgba(239, 68, 68, 0.1);
- color: #ef4444;
- border: 1px solid rgba(239, 68, 68, 0.2);
- }
- .device-location {
- font-size: 0.875rem;
- color: #6b7280;
- margin-bottom: 1.5rem;
- }
- .device-metrics {
- margin-bottom: 1.5rem;
- flex: 1;
- }
- .metrics-container {
- position: relative;
- min-height: 80px;
- overflow: hidden;
- }
- .metric-pair {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 2rem;
- width: 100%;
- }
- .metric {
- text-align: left;
- }
- .metric-label {
- font-size: 0.875rem;
- color: #6b7280;
- margin-bottom: 0.25rem;
- font-weight: 500;
- }
- .metric-value {
- font-size: 1.875rem;
- font-weight: 600;
- color: #1f2937;
- display: flex;
- align-items: baseline;
- }
- .metric-value span {
- font-size: 1.25rem;
- margin-left: 0.25rem;
- color: #6b7280;
- font-weight: 400;
- }
- .metric-value.metric-warning {
- color: #ef4444;
- }
- .metric-value.metric-alert {
- color: #ef4444;
- }
- /* 机场翻牌动画效果 */
- .flip-enter-active,
- .flip-leave-active {
- transition: all 0.4s ease-in-out;
- transform-style: preserve-3d;
- }
- .flip-enter {
- transform: perspective(600px) rotateX(90deg);
- opacity: 0;
- }
- .flip-enter-to {
- transform: perspective(600px) rotateX(0deg);
- opacity: 1;
- }
- .flip-leave {
- transform: perspective(600px) rotateX(0deg);
- opacity: 1;
- }
- .flip-leave-to {
- transform: perspective(600px) rotateX(-90deg);
- opacity: 0;
- }
- .device-card.offline .metric-value {
- color: #9ca3af;
- }
- .device-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-top: auto;
- }
- .last-update {
- font-size: 0.875rem;
- color: #6b7280;
- }
- .last-update.text-warning {
- color: #ef4444;
- }
- .last-update.text-offline {
- color: #9ca3af;
- }
- .detail-btn {
- background: transparent;
- border: 1px solid #d1d5db;
- color: #1f2937;
- padding: 8px 16px;
- border-radius: 8px;
- font-size: 0.875rem;
- cursor: pointer;
- transition: all 0.2s ease;
- font-weight: 500;
- }
- .detail-btn:hover {
- border-color: #10b981;
- background-color: rgba(16, 185, 129, 0.1);
- color: #059669;
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
- }
- .detail-btn.warning {
- border-color: #ef4444;
- color: #ef4444;
- }
- .detail-btn.warning:hover {
- background-color: #ef4444;
- color: white;
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
- }
- /* 单个摄像头全屏模式 */
- .single-camera-fullscreen {
- position: fixed !important;
- top: 0 !important;
- left: 0 !important;
- width: 100vw !important;
- height: 100vh !important;
- background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important;
- z-index: 10000 !important;
- display: flex !important;
- margin: 0 !important;
- padding: 0 !important;
- }
- .video-area {
- flex: 1 !important;
- min-width: 0 !important;
- display: flex !important;
- flex-direction: column !important;
- position: relative !important;
- }
- .fullscreen-video-container {
- flex: 1;
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- background: linear-gradient(135deg, #f0fdf4 0%, #f8fafc 100%);
- }
- /* 左上角摄像头名称 */
- .camera-title-overlay {
- position: absolute !important;
- top: 20px !important;
- left: 20px !important;
- z-index: 10001 !important;
- background: rgba(255, 255, 255, 0.95) !important;
- padding: 12px 20px !important;
- border-radius: 12px !important;
- backdrop-filter: blur(8px) !important;
- border: 1px solid rgba(229, 231, 235, 0.8) !important;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1) !important;
- }
- .camera-title-text {
- color: #1f2937;
- font-size: 1.25rem;
- font-weight: 600;
- margin: 0;
- text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
- letter-spacing: 0.5px;
- }
- /* 右上角关闭按钮 */
- .close-button-overlay {
- position: absolute !important;
- top: 20px !important;
- right: 20px !important;
- z-index: 10001 !important;
- }
- .fullscreen-video-preview {
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- background: linear-gradient(135deg, #f0fdf4 0%, #f8fafc 100%);
- }
- .fullscreen-camera-icon {
- width: 120px;
- height: 120px;
- color: #10b981;
- opacity: 0.6;
- }
- .single-camera-fullscreen .control-panel {
- width: 280px;
- background: white;
- border-left: 1px solid #e5e7eb;
- padding: 1.5rem;
- overflow-y: auto;
- height: 100vh;
- display: flex;
- flex-direction: column;
- box-shadow: -4px 0 6px -1px rgba(0, 0, 0, 0.1);
- }
- .control-sections {
- display: flex;
- flex-direction: column;
- gap: 1.25rem;
- flex: 1;
- justify-content: flex-start;
- }
- .control-section {
- background: transparent;
- }
- .control-title {
- font-size: 0.875rem;
- font-weight: 600;
- margin-bottom: 0.75rem;
- color: #1f2937;
- text-align: left;
- }
- /* 云台控制按钮网格 */
- .ptz-controls {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 0.5rem;
- }
- .ptz-btn {
- aspect-ratio: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- background: white;
- border: 1px solid #d1d5db;
- color: #6b7280;
- border-radius: 8px;
- font-size: 1.25rem;
- transition: all 0.2s ease;
- cursor: pointer;
- min-height: 44px;
- }
- .ptz-btn:hover {
- background: #10b981;
- border-color: #10b981;
- color: white;
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
- }
- .ptz-btn:active {
- transform: scale(0.95);
- }
- /* 统一所有控制按钮的样式 */
- .zoom-btn, .playback-btn, .other-btn {
- aspect-ratio: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 44px;
- }
- .zoom-btn svg, .playback-btn svg, .other-btn svg {
- width: 16px !important;
- height: 16px !important;
- }
- /* 变倍控制 */
- .zoom-controls {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 0.5rem;
- }
- /* 预置位按钮网格 */
- .preset-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 0.5rem;
- }
- .preset-btn {
- padding: 0.75rem;
- background: white;
- border: 1px solid #d1d5db;
- color: #6b7280;
- border-radius: 8px;
- transition: all 0.2s ease;
- text-align: center;
- cursor: pointer;
- font-size: 0.875rem;
- min-height: 44px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .preset-btn:hover {
- background: #10b981;
- border-color: #10b981;
- color: white;
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
- }
- /* 回放控制 */
- .playback-controls {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 0.5rem;
- }
- /* 其他控制 */
- .other-controls {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 0.5rem;
- }
- /* 网格全屏模式 */
- .grid-fullscreen-mode {
- position: fixed !important;
- top: 0 !important;
- left: 0 !important;
- width: 100vw !important;
- height: 100vh !important;
- background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important;
- z-index: 10000 !important;
- display: flex !important;
- flex-direction: column !important;
- padding: 1rem !important;
- }
- .fullscreen-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 1rem 0;
- border-bottom: 1px solid #e5e7eb;
- margin-bottom: 1rem;
- gap: 2rem;
- }
- .fullscreen-title {
- display: flex;
- align-items: baseline;
- gap: 1rem;
- }
- .fullscreen-title-text {
- font-size: 1.5rem;
- font-weight: 600;
- color: #1f2937;
- margin: 0;
- }
- .fullscreen-subtitle {
- font-size: 0.875rem;
- color: #6b7280;
- }
- .fullscreen-close-btn {
- width: 40px;
- height: 40px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: white;
- border: 1px solid #d1d5db;
- border-radius: 8px;
- color: #6b7280;
- transition: all 0.2s ease;
- cursor: pointer;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
- .fullscreen-close-btn:hover {
- background: #10b981;
- color: white;
- border-color: #10b981;
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
- }
- /* 全屏分页控制样式 */
- .fullscreen-pagination {
- display: flex;
- align-items: center;
- gap: 1rem;
- flex: 1;
- justify-content: center;
- }
- .fullscreen-nav-btn {
- width: 36px;
- height: 36px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: white;
- border: 1px solid #d1d5db;
- border-radius: 8px;
- color: #6b7280;
- transition: all 0.2s ease;
- cursor: pointer;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
- .fullscreen-nav-btn:hover:not(:disabled) {
- background: #10b981;
- color: white;
- border-color: #10b981;
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
- }
- .fullscreen-nav-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- .fullscreen-page-indicator {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.5rem;
- color: #6b7280;
- font-size: 0.875rem;
- }
- .fullscreen-page-dots {
- display: flex;
- gap: 0.5rem;
- align-items: center;
- }
- .page-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background-color: #d1d5db;
- cursor: pointer;
- transition: all 0.2s ease;
- }
- .page-dot.active {
- background-color: #10b981;
- transform: scale(1.2);
- }
- .page-dot:hover {
- background-color: #6b7280;
- }
- /* 全屏控制区域 */
- .fullscreen-controls {
- display: flex;
- align-items: center;
- gap: 1rem;
- }
- /* 键盘快捷键提示 */
- .keyboard-hints {
- display: flex;
- gap: 1rem;
- align-items: center;
- }
- .hint-item {
- display: flex;
- align-items: center;
- gap: 0.25rem;
- font-size: 0.75rem;
- color: #6b7280;
- }
- .hint-item kbd {
- background: white;
- border: 1px solid #d1d5db;
- border-radius: 4px;
- color: #374151;
- font-size: 0.75rem;
- padding: 2px 6px;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
- font-family: monospace;
- min-width: 20px;
- text-align: center;
- }
- .fullscreen-video-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- grid-template-rows: repeat(2, 1fr);
- gap: 1rem;
- flex: 1;
- height: calc(100vh - 120px);
- }
- .fullscreen-video-card {
- position: relative;
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- overflow: hidden;
- transition: all 0.3s ease;
- display: flex;
- flex-direction: column;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
- .fullscreen-video-card:hover {
- border-color: #10b981;
- transform: translateY(-4px);
- box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.15);
- }
- .fullscreen-camera-btn {
- position: absolute;
- top: 12px;
- right: 12px;
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(255, 255, 255, 0.9);
- border: 1px solid rgba(229, 231, 235, 0.8);
- border-radius: 8px;
- color: #6b7280;
- transition: all 0.2s ease;
- z-index: 10;
- cursor: pointer;
- backdrop-filter: blur(8px);
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
- .fullscreen-camera-btn:hover {
- background: #10b981;
- color: white;
- border-color: #10b981;
- }
- .fullscreen-video-preview {
- flex: 1;
- position: relative;
- background: linear-gradient(135deg, #f0fdf4 0%, #f8fafc 100%);
- display: flex;
- flex-direction: column;
- }
- .fullscreen-camera-placeholder {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .fullscreen-camera-icon {
- width: 64px;
- height: 64px;
- color: #10b981;
- opacity: 0.6;
- }
- .fullscreen-video-info {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- background: linear-gradient(to top, rgba(248, 250, 252, 0.95), rgba(248, 250, 252, 0.8), rgba(248, 250, 252, 0.4), transparent);
- padding: 0.75rem;
- display: flex;
- justify-content: space-between;
- align-items: flex-end;
- z-index: 5;
- backdrop-filter: blur(8px);
- border-top: 1px solid rgba(229, 231, 235, 0.3);
- }
- .fullscreen-video-details {
- flex: 1;
- }
- .fullscreen-video-name {
- font-weight: 600;
- margin-bottom: 0.25rem;
- color: #1f2937;
- font-size: 0.875rem;
- text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
- }
- .fullscreen-video-location {
- font-size: 0.75rem;
- color: #6b7280;
- text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
- }
- .fullscreen-video-status {
- font-size: 0.75rem;
- padding: 4px 8px;
- border-radius: 12px;
- font-weight: 500;
- white-space: nowrap;
- backdrop-filter: blur(4px);
- }
- .fullscreen-video-status.online {
- background-color: rgba(16, 185, 129, 0.15);
- color: #059669;
- border: 1px solid rgba(16, 185, 129, 0.4);
- font-weight: 600;
- }
- .fullscreen-video-status.offline {
- background-color: rgba(239, 68, 68, 0.15);
- color: #dc2626;
- border: 1px solid rgba(239, 68, 68, 0.4);
- font-weight: 600;
- }
- /* 从网格全屏进入单摄像头全屏时的处理 */
- .grid-fullscreen-mode .single-camera-fullscreen {
- position: fixed !important;
- z-index: 10001 !important;
- }
- /* 网格全屏模式响应式设计 */
- @media (max-width: 1024px) {
- .fullscreen-video-grid {
- grid-template-columns: repeat(2, 1fr);
- grid-template-rows: repeat(3, 1fr);
- }
- .fullscreen-camera-icon {
- width: 48px;
- height: 48px;
- }
- }
- @media (max-width: 768px) {
- .grid-fullscreen-mode {
- padding: 0.5rem !important;
- }
- .fullscreen-header {
- padding: 0.5rem 0;
- flex-direction: column;
- gap: 1rem;
- }
- .fullscreen-title-text {
- font-size: 1.25rem;
- }
- .fullscreen-pagination {
- order: 2;
- gap: 0.5rem;
- }
- .fullscreen-controls {
- order: 3;
- gap: 0.5rem;
- }
- .keyboard-hints {
- display: none;
- }
- .fullscreen-video-grid {
- grid-template-columns: repeat(1, 1fr);
- grid-template-rows: repeat(6, 1fr);
- gap: 0.5rem;
- height: calc(100vh - 140px);
- }
- .fullscreen-camera-icon {
- width: 40px;
- height: 40px;
- }
- .fullscreen-page-indicator {
- font-size: 0.75rem;
- }
- .fullscreen-nav-btn {
- width: 32px;
- height: 32px;
- }
- }
- /* 响应式设计 */
- @media (max-width: 1280px) {
- .stats-container {
- grid-template-columns: 1fr;
- gap: 1.5rem;
- }
- .category-stats {
- grid-template-columns: repeat(4, 1fr);
- }
- .device-monitor-header {
- flex-direction: column;
- align-items: stretch;
- gap: 1rem;
- }
- .device-monitor-title-section {
- flex-direction: column;
- align-items: stretch;
- gap: 1rem;
- }
- .filter-buttons {
- flex-wrap: wrap;
- }
- .type-filters {
- flex-wrap: wrap;
- }
- }
- @media (max-width: 768px) {
- .control-panel {
- flex-direction: column;
- align-items: stretch;
- gap: 1rem;
- }
- .control-left {
- flex-direction: column;
- gap: 1rem;
- }
- .control-right {
- flex-direction: column;
- align-items: stretch;
- gap: 1rem;
- }
- .form-group {
- justify-content: space-between;
- }
- .stats-container {
- grid-template-columns: 1fr;
- }
- .overview-stats {
- grid-template-columns: repeat(3, 1fr);
- gap: 1rem;
- }
- .category-stats {
- grid-template-columns: repeat(2, 1fr);
- }
- .video-grid {
- grid-template-columns: repeat(2, 1fr);
- }
- .device-monitor-header {
- gap: 0.75rem;
- }
- .device-monitor-title-section {
- gap: 0.75rem;
- }
- .device-monitor-controls {
- flex-direction: column;
- align-items: stretch;
- gap: 0.75rem;
- }
- .filter-btn {
- padding: 0.375rem 0.75rem;
- font-size: 0.8rem;
- }
- .device-pagination {
- justify-content: center;
- }
- .device-grid {
- padding: 0;
- }
- .device-metrics {
- gap: 1rem;
- }
- .metric-value {
- font-size: 1.5rem;
- }
- .metric-value span {
- font-size: 1rem;
- }
- .detail-btn {
- width: 100%;
- justify-content: center;
- text-align: center;
- }
- }
- /* 历史数据模态框样式 */
- .history-modal-backdrop {
- position: fixed !important;
- top: 0 !important; /* 从页面顶部开始覆盖 */
- left: 200px !important; /* 考虑左侧菜单栏宽度 */
- right: 0 !important;
- bottom: 0 !important;
- background-color: rgba(248, 250, 252, 0.85) !important;
- backdrop-filter: blur(8px) !important;
- z-index: 1000 !important;
- display: flex !important;
- align-items: center !important;
- justify-content: center !important;
- padding: 1rem !important;
- padding-top: 84px !important; /* 给顶部留出导航栏空间,但仍然覆盖 */
- opacity: 1 !important;
- transition: opacity 0.3s ease !important;
- box-sizing: border-box !important;
- }
- /* 当侧边栏隐藏时的样式调整 */
- .hideSidebar .history-modal-backdrop {
- left: 54px !important; /* 折叠后的侧边栏宽度 */
- }
- /* 移动端适配 */
- @media (max-width: 1024px) {
- .history-modal-backdrop {
- left: 0 !important;
- padding-top: 50px !important;
- }
- }
- .history-modal-content {
- background-color: white !important;
- border-radius: 16px !important;
- width: 100% !important;
- max-width: 1000px !important;
- max-height: 90vh !important;
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04) !important;
- border: 1px solid #e5e7eb !important;
- overflow: hidden !important;
- display: flex !important;
- flex-direction: column !important;
- }
- .history-modal-header {
- display: flex !important;
- justify-content: space-between !important;
- align-items: center !important;
- padding: 1rem 1.5rem !important;
- border-bottom: 1px solid #e5e7eb !important;
- background: #f8fafc !important;
- }
- .history-modal-title-section {
- display: flex !important;
- align-items: center !important;
- gap: 0.75rem !important;
- }
- .history-modal-title {
- font-size: 1.125rem !important;
- font-weight: 600 !important;
- color: #1f2937 !important;
- margin: 0 !important;
- }
- .history-modal-close {
- background: transparent !important;
- border: none !important;
- color: #6b7280 !important;
- cursor: pointer !important;
- padding: 0.5rem !important;
- transition: all 0.2s ease !important;
- border-radius: 8px !important;
- display: flex !important;
- align-items: center !important;
- justify-content: center !important;
- width: 40px !important;
- height: 40px !important;
- }
- .history-modal-close:hover {
- color: #1f2937 !important;
- background: rgba(16, 185, 129, 0.1) !important;
- }
- .history-modal-close svg {
- width: 20px !important;
- height: 20px !important;
- stroke: currentColor !important;
- fill: none !important;
- }
- .history-modal-controls {
- display: flex !important;
- justify-content: space-between !important;
- align-items: center !important;
- padding: 1rem 1.5rem !important;
- border-bottom: 1px solid #e5e7eb !important;
- background: white !important;
- }
- .time-range-buttons {
- display: flex !important;
- gap: 0.5rem !important;
- }
- .indicator-buttons {
- display: flex !important;
- gap: 0.5rem !important;
- }
- .time-range-btn, .indicator-btn {
- padding: 0.5rem 1rem !important;
- border-radius: 8px !important;
- font-size: 0.875rem !important;
- color: #6b7280 !important;
- background: transparent !important;
- border: 1px solid #d1d5db !important;
- transition: all 0.2s ease !important;
- cursor: pointer !important;
- }
- .time-range-btn:hover, .indicator-btn:hover {
- background: rgba(16, 185, 129, 0.05) !important;
- border-color: #10b981 !important;
- color: #059669 !important;
- }
- .time-range-btn.active, .indicator-btn.active {
- background: #10b981 !important;
- border-color: #10b981 !important;
- color: white !important;
- }
- .history-modal-body {
- flex: 1 !important;
- padding: 1.5rem !important;
- background: white !important;
- overflow: hidden !important;
- }
- .chart-container {
- width: 100% !important;
- height: 400px !important;
- display: flex !important;
- align-items: center !important;
- justify-content: center !important;
- background: transparent !important;
- border-radius: 6px !important;
- }
- /* 响应式设计 */
- @media (max-width: 768px) {
- .history-modal-content {
- margin: 0.5rem !important;
- max-width: calc(100vw - 1rem) !important;
- max-height: calc(100vh - 1rem) !important;
- }
- .history-modal-controls {
- flex-direction: column !important;
- gap: 1rem !important;
- align-items: stretch !important;
- }
- .time-range-buttons, .indicator-buttons {
- flex-wrap: wrap !important;
- justify-content: center !important;
- }
- .time-range-btn, .indicator-btn {
- padding: 0.375rem 0.75rem !important;
- font-size: 0.8rem !important;
- }
- .chart-container {
- height: 300px !important;
- }
- }
- </style>
|