detail-camera.vue 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616
  1. <template>
  2. <view class="container">
  3. <!-- 设备头部信息区域 -->
  4. <view class="device-header">
  5. <view class="device-info-row">
  6. <view class="device-name-container">
  7. <text class="device-name">{{ deviceInfo.name }}</text>
  8. <view class="status-tag" :class="deviceInfo.status === 1 ? 'status-online' : 'status-offline'">
  9. <view class="status-dot" :class="{'offline-dot': deviceInfo.status === 1}"></view>
  10. {{ deviceInfo.status === 1 ? '在线' : '离线' }}
  11. </view>
  12. </view>
  13. <view class="refresh-btn" :class="{'refreshing': isRefreshing}" @tap="refreshData">
  14. <image src="/static/icons/refresh_icon.png" mode="aspectFit" style="width: 22px; height: 22px;">
  15. </image>
  16. </view>
  17. </view>
  18. <view class="device-meta-row">
  19. <view class="device-meta-item">
  20. <view class="meta-icon">
  21. <image src="/static/icons/device_icon.png" mode="aspectFit"
  22. style="width: 36rpx; height: 36rpx;"></image>
  23. </view>
  24. <text class="meta-label">设备编号:</text>
  25. <text class="meta-value">{{ deviceInfo.deviceId }}</text>
  26. </view>
  27. <view class="device-meta-item">
  28. <view class="meta-icon">
  29. <image src="/static/icons/location_icon.png" mode="aspectFit"
  30. style="width: 36rpx; height: 36rpx;"></image>
  31. </view>
  32. <text class="meta-label">安装位置:</text>
  33. <text class="meta-value">{{ deviceInfo.location }}</text>
  34. </view>
  35. <view class="device-meta-item">
  36. <view class="meta-icon">
  37. <image src="/static/icons/clock_icon.png" mode="aspectFit" style="width: 36rpx; height: 36rpx;">
  38. </image>
  39. </view>
  40. <text class="meta-label">最近更新:</text>
  41. <text class="meta-value">{{ formatDate(deviceInfo.lastUpdate) }}</text>
  42. </view>
  43. </view>
  44. </view>
  45. <!-- 视频预览区域 -->
  46. <view class="video-section">
  47. <view class="video-container" :class="{'fullscreen-mode': isFullscreen}">
  48. <image v-if="!isPlaying" src="/static/images/video-placeholder.jpg" mode="aspectFill"
  49. class="video-placeholder"></image>
  50. <!-- 使用跨平台视频播放组件 -->
  51. <!-- #ifdef H5 -->
  52. <view v-if="isPlaying" class="h5-video-wrapper">
  53. <Jessibuca ref="jessibucaRef" :videoUrl="getH5StreamUrl" :hasAudio="true" @error="onVideoError" />
  54. </view>
  55. <!-- #endif -->
  56. <!-- #ifdef MP-WEIXIN -->
  57. <live-player v-if="isPlaying" id="videoPlayer" :src="getMiniProgramStreamUrl" mode="live"
  58. :autoplay="true" :muted="isMuted" object-fit="contain" @statechange="onStateChange"
  59. @error="onVideoError" @fullscreenchange="onFullscreenChange" class="video-player"></live-player>
  60. <!-- #endif -->
  61. <!-- 视频控制层 -->
  62. <view class="video-controls">
  63. <view class="control-row top-controls">
  64. <view class="signal-indicator">
  65. <image src="/static/icons/signal_icon.png" mode="aspectFit"
  66. style="width: 16px; height: 16px;"></image>
  67. <text class="signal-text">信号良好</text>
  68. </view>
  69. <view class="fullscreen-button" @click="toggleFullscreen">
  70. <image src="/static/icons/resize_icon.png" mode="aspectFit"
  71. style="width: 20px; height: 20px;"></image>
  72. </view>
  73. </view>
  74. <view class="control-row center-controls">
  75. <view v-if="!isPlaying" class="play-button" @click="togglePlayState">
  76. <image src="/static/icons/play_icon.png" mode="aspectFit"
  77. style="width: 32px; height: 32px;"></image>
  78. </view>
  79. <view v-else class="center-button-container" @click="togglePlayState">
  80. <view class="pause-icon">
  81. <image src="/static/icons/pause_icon.png" mode="aspectFit"
  82. style="width: 24px; height: 24px;"></image>
  83. </view>
  84. </view>
  85. </view>
  86. <view class="control-row bottom-controls">
  87. <view class="video-time">{{ currentTime }}</view>
  88. </view>
  89. </view>
  90. </view>
  91. </view>
  92. <!-- 云台控制区域 -->
  93. <!-- <view class="ptz-section">
  94. <view class="section-title">云台控制</view>
  95. <view class="ptz-container">
  96. // 圆形云台控制
  97. <view class="ptz-circle-container">
  98. 上箭头
  99. <view class="ptz-arrow ptz-up" @touchstart="controlPTZ('up', true)" @touchend="controlPTZ('up', false)">
  100. <image src="/static/icons/arrow_up_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  101. </view>
  102. // 左箭头
  103. <view class="ptz-arrow ptz-left" @touchstart="controlPTZ('left', true)" @touchend="controlPTZ('left', false)">
  104. <image src="/static/icons/arrow_left_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  105. </view>
  106. 中心拍照按钮
  107. <view class="ptz-center" @click="takeScreenshot">
  108. <image src="/static/icons/camera_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  109. </view>
  110. 右箭头
  111. <view class="ptz-arrow ptz-right" @touchstart="controlPTZ('right', true)" @touchend="controlPTZ('right', false)">
  112. <image src="/static/icons/arrow_right_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  113. </view>
  114. 下箭头
  115. <view class="ptz-arrow ptz-down" @touchstart="controlPTZ('down', true)" @touchend="controlPTZ('down', false)">
  116. <image src="/static/icons/arrow_down_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  117. </view>
  118. </view>
  119. 底部操作按钮
  120. <view class="ptz-bottom-controls">
  121. <view class="ptz-bottom-button" @touchstart="controlZoom('out', true)" @touchend="controlZoom('out', false)">
  122. <image src="/static/icons/zoom01_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  123. </view>
  124. <view class="ptz-bottom-button" @click="resetPTZ">
  125. <image src="/static/icons/resetPTZ_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  126. </view>
  127. <view class="ptz-bottom-button" @touchstart="controlZoom('in', true)" @touchend="controlZoom('in', false)">
  128. <image src="/static/icons/zoom02_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  129. </view>
  130. </view>
  131. </view>
  132. </view> -->
  133. <!-- 快捷功能按钮 -->
  134. <!-- <view class="quick-actions">
  135. <view class="action-button" @click="toggleVoiceIntercom">
  136. <view class="action-icon">
  137. <image src="/static/icons/Voice_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  138. </view>
  139. <text class="action-text">语音对讲</text>
  140. </view>
  141. <view class="action-button" @click="toggleMute">
  142. <view class="action-icon">
  143. <image v-if="isMuted" src="/static/icons/muted_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  144. <image v-else src="/static/icons/unmuted_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  145. </view>
  146. <text class="action-text">{{ isMuted ? '取消静音' : '静音' }}</text>
  147. </view>
  148. <view class="action-button" @click="navigateToHistory">
  149. <view class="action-icon">
  150. <image src="/static/icons/action_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  151. </view>
  152. <text class="action-text">视频回看</text>
  153. </view>
  154. </view> -->
  155. <!-- 告警信息列表 -->
  156. <view class="alerts-section">
  157. <view class="section-title">
  158. <text>告警信息</text>
  159. <view class="alert-badge" v-if="getUnhandledAlerts.length > 0">{{ getUnhandledAlerts.length }}</view>
  160. </view>
  161. <view class="alerts-list" v-if="getUnhandledAlerts.length > 0">
  162. <view v-for="(item, index) in getUnhandledAlerts.slice(0, 3)" :key="index" class="alert-item" :class="{
  163. 'alert-urgent': item.level === 3,
  164. 'alert-warning': item.level === 2,
  165. 'alert-info': item.level === 1
  166. }">
  167. <view class="alert-item-icon">
  168. <image v-if="item.level === 3" src="/static/icons/warning_icon.png" mode="aspectFit"
  169. style="width: 24px; height: 24px;"></image>
  170. <image v-else-if="item.level === 2" src="/static/icons/info_icon.png" mode="aspectFit"
  171. style="width: 24px; height: 24px;"></image>
  172. <image v-else src="/static/icons/success_icon.png" mode="aspectFit"
  173. style="width: 24px; height: 24px;"></image>
  174. </view>
  175. <view class="alert-item-info">
  176. <text class="alert-item-type">{{ item.type }}</text>
  177. <text class="alert-item-level">
  178. {{ item.level === 3 ? '紧急' : item.level === 2 ? '警告' : '提示' }}
  179. </text>
  180. </view>
  181. <view class="alert-item-time">{{ formatSmartTime(item.time) }}</view>
  182. </view>
  183. </view>
  184. <view v-else class="empty-alert">
  185. <text class="empty-text">暂无告警信息</text>
  186. </view>
  187. </view>
  188. </view>
  189. </template>
  190. <script>
  191. import {
  192. getDeviceCollectorDetail,
  193. getChannels,
  194. playStart,
  195. pause
  196. } from "@/api/services/device.js";
  197. import {
  198. formatSmartTime,
  199. formatDate,
  200. getFormattedTime
  201. } from '@/utils/dateUtils'
  202. import {
  203. isPlayableInMiniProgram,
  204. buildPlatformStreamUrls
  205. } from '@/utils/media-utils'
  206. import config from '@/config/config'
  207. // 导入Jessibuca组件
  208. // #ifdef H5
  209. import Jessibuca from '@/components/common/jessibuca.vue'
  210. // #endif
  211. export default {
  212. components: {
  213. // #ifdef H5
  214. Jessibuca
  215. // #endif
  216. },
  217. data() {
  218. return {
  219. deviceInfo: {
  220. deviceId: '',
  221. name: '设备加载中...',
  222. status: '',
  223. location: '正在获取位置...',
  224. lastUpdate: '',
  225. deviceType: 'weather', // 默认类型,会根据API返回更新
  226. deviceTypeId: null,
  227. streamUrl: '',
  228. channelId: null, // 当前通道ID
  229. originalStreamUrl: 'ws://121.4.16.100:6080/rtp/34020000001110000001_34020000001320000012.live.flv',
  230. },
  231. isPlaying: false,
  232. isMuted: false,
  233. isRecording: false,
  234. isFullscreen: false,
  235. isVoiceActive: false,
  236. isGridView: false,
  237. isZoomMode: false,
  238. currentTime: '14:30:25',
  239. // 模拟历史录像数据
  240. recordHistory: [{
  241. id: 1,
  242. startTime: '今天 12:30',
  243. duration: '00:15:30',
  244. url: ''
  245. },
  246. {
  247. id: 2,
  248. startTime: '今天 10:15',
  249. duration: '00:05:22',
  250. url: ''
  251. },
  252. {
  253. id: 3,
  254. startTime: '昨天 18:45',
  255. duration: '00:30:10',
  256. url: ''
  257. },
  258. {
  259. id: 4,
  260. startTime: '昨天 14:20',
  261. duration: '00:10:05',
  262. url: ''
  263. }
  264. ],
  265. // 模拟告警数据
  266. alertHistory: [],
  267. livePlayerContext: null, // 小程序视频上下文
  268. isRefreshing: false
  269. }
  270. },
  271. computed: {
  272. // 获取所有未处理的告警
  273. getUnhandledAlerts() {
  274. return this.alertHistory.filter(alert => alert.status === 0);
  275. },
  276. // 获取H5环境使用的流地址
  277. getH5StreamUrl() {
  278. // 首先尝试使用设备返回的流URL
  279. // if (this.deviceInfo.streamUrl) {
  280. // return this.deviceInfo.streamUrl;
  281. // }
  282. // 如果没有设备特定的URL,则使用默认原始流
  283. return this.deviceInfo.originalStreamUrl || config.streamServer.wsFlvServer;
  284. },
  285. // 获取小程序环境使用的流地址
  286. getMiniProgramStreamUrl() {
  287. // 获取合适的小程序流地址
  288. const {
  289. streamServer
  290. } = config;
  291. // 如果有设备特定的RTMP流,优先使用
  292. if (this.deviceInfo.rtmpUrl) {
  293. return this.deviceInfo.rtmpUrl;
  294. }
  295. // 其次尝试使用HLS流
  296. if (this.deviceInfo.hlsUrl) {
  297. return this.deviceInfo.hlsUrl;
  298. }
  299. // 最后使用配置的默认流
  300. return streamServer.rtmpServer || streamServer.hlsServer;
  301. },
  302. },
  303. onReady() {
  304. // 设置页面标题
  305. uni.setNavigationBarTitle({
  306. title: this.deviceInfo.name
  307. })
  308. // 模拟更新时间
  309. this.startTimeUpdate()
  310. // #ifdef MP-WEIXIN
  311. // 创建小程序视频上下文
  312. this.livePlayerContext = uni.createLivePlayerContext('videoPlayer', this)
  313. // #endif
  314. // #ifdef H5
  315. // 加载Jessibuca的JS库
  316. this.loadJessibucaScript()
  317. // #endif
  318. // 监听全屏状态变化
  319. this.setupFullscreenListener()
  320. // 添加键盘事件监听
  321. this.setupKeyboardListener()
  322. // 初始化流地址
  323. this.initStreamUrl()
  324. },
  325. beforeDestroy() {
  326. console.log("停止点播");
  327. // 停止点播
  328. pause(this.deviceInfo.deviceId, this.deviceInfo.channelId).then(res => {
  329. if (res[1].data.code !== 0) {
  330. console.error('暂停失败:', res.message)
  331. // uni.showToast({
  332. // title: '暂停失败: ' + res.message,
  333. // icon: 'none'
  334. // })
  335. } else {
  336. console.log('视频已暂停')
  337. // uni.showToast({
  338. // title: '视频已暂停',
  339. // icon: 'none',
  340. // duration: 1500
  341. // })
  342. }
  343. }).catch(err => {
  344. console.error('暂停请求错误:', err)
  345. })
  346. },
  347. onLoad(options) {
  348. uni.$once('passDeviceData', (data) => {
  349. console.log('接收到数据', data);
  350. // 如果有传入设备ID,则获取设备信息
  351. if (data && data.deviceId) {
  352. this.deviceInfo.deviceId = data.deviceId;
  353. this.deviceInfo.location = data.fieldName;
  354. this.deviceInfo.name = data.deviceName;
  355. this.deviceInfo.status = data.status;
  356. this.deviceInfo.deviceTypeId = data.deviceTypeId || '';
  357. // 加载设备详情
  358. this.fetchDeviceInfo();
  359. this.queryChannels();
  360. }
  361. });
  362. },
  363. methods: {
  364. formatDate,
  365. formatSmartTime,
  366. // 加载Jessibuca脚本
  367. loadJessibucaScript() {
  368. const script = document.createElement('script')
  369. script.src = '/static/js/jessibuca/jessibuca.js'
  370. script.onload = () => {
  371. console.log('Jessibuca 脚本加载成功')
  372. }
  373. script.onerror = (error) => {
  374. console.error('Jessibuca 脚本加载失败:', error)
  375. }
  376. document.head.appendChild(script)
  377. },
  378. // 获取设备信息
  379. fetchDeviceInfo(deviceId) {
  380. // 这里应该是API请求,暂时用模拟数据
  381. console.log('获取设备信息:', deviceId)
  382. getDeviceCollectorDetail(this.deviceInfo.deviceId)
  383. .then(res => {
  384. if (res.data.data && res.data.code === 200) {
  385. const detail = res.data.data;
  386. this.deviceInfo.lastUpdate = getFormattedTime()
  387. // 更新页面标题
  388. uni.setNavigationBarTitle({
  389. title: this.deviceInfo.name
  390. });
  391. // 更新告警信息
  392. if (detail.alertRecordList && detail.alertRecordList.length > 0) {
  393. this.alertHistory = detail.alertRecordList.map(alert => ({
  394. id: alert.alertId,
  395. time: alert.alertTime,
  396. type: alert.alertContent,
  397. status: alert.processStatus,
  398. level: alert.alertLevel
  399. }));
  400. }
  401. }
  402. })
  403. },
  404. // 根据设备id获取通道列表
  405. queryChannels() {
  406. getChannels(this.deviceInfo.deviceId)
  407. .then(response => {
  408. console.log('获取通道列表:', response);
  409. const res = response[1].data
  410. if (res.code === 0 && res.data.total > 0) {
  411. const channels = res.data.list;
  412. this.deviceInfo.channelId = channels[0].deviceId; // 默认选择第一个通道
  413. playStart(this.deviceInfo.deviceId, this.deviceInfo.channelId).then(res => {
  414. if (res[1].data.code !== 0) {
  415. console.error('播放开始失败:', res.message);
  416. uni.showToast({
  417. title: '播放失败: ' + res.message,
  418. icon: 'none'
  419. });
  420. return;
  421. }
  422. console.log('播放开始:', res);
  423. this.deviceInfo.originalStreamUrl = res[1].data.data.ws_flv || this.deviceInfo
  424. .originalStreamUrl;
  425. }).catch(err => {
  426. console.error('播放开始失败:', err);
  427. })
  428. } else {
  429. uni.showToast({
  430. title: '获取通道列表失败: ' + res.message,
  431. icon: 'none'
  432. });
  433. return;
  434. }
  435. })
  436. .catch(err => {
  437. console.error('获取通道列表错误:', err);
  438. });
  439. },
  440. // 播放/暂停切换
  441. togglePlayState() {
  442. if (!this.isPlaying) {
  443. // 从未播放状态切换到播放状态
  444. this.isPlaying = true
  445. // #ifdef MP-WEIXIN
  446. // 小程序环境中,短暂延迟确保组件已加载
  447. setTimeout(() => {
  448. if (this.livePlayerContext) {
  449. this.livePlayerContext.play({
  450. success: () => {
  451. console.log('小程序视频播放成功')
  452. uni.vibrateShort()
  453. },
  454. fail: (err) => {
  455. console.error('小程序视频播放失败:', err)
  456. uni.showToast({
  457. title: '视频播放失败',
  458. icon: 'none'
  459. })
  460. }
  461. })
  462. }
  463. }, 300)
  464. // #endif
  465. // #ifdef H5
  466. // 浏览器环境中使用Jessibuca
  467. setTimeout(() => {
  468. playStart(this.deviceInfo.deviceId, this.deviceInfo.channelId).then(res => {
  469. if (res[1].data.code !== 0) {
  470. console.error('播放开始失败:', res.message);
  471. uni.showToast({
  472. title: '播放失败: ' + res.message,
  473. icon: 'none'
  474. });
  475. return;
  476. }
  477. console.log('播放开始:', res[1]);
  478. this.deviceInfo.originalStreamUrl = res[1].data.data.ws_flv || this.deviceInfo
  479. .originalStreamUrl;
  480. }).catch(err => {
  481. console.error('播放开始失败:', err);
  482. })
  483. uni.vibrateShort()
  484. }, 300)
  485. // #endif
  486. } else {
  487. // 从播放状态切换到暂停状态
  488. // #ifdef H5
  489. if (this.$refs.jessibucaRef) {
  490. this.$refs.jessibucaRef.pause() // 使用组件的暂定
  491. }
  492. // #endif
  493. // #ifdef MP-WEIXIN
  494. if (this.livePlayerContext) {
  495. this.livePlayerContext.pause()
  496. }
  497. // #endif
  498. this.isPlaying = false
  499. // 在页面上显示暂停状态
  500. uni.showToast({
  501. title: '视频已暂停',
  502. icon: 'none',
  503. duration: 1500
  504. })
  505. }
  506. },
  507. // 静音切换
  508. toggleMute() {
  509. this.isMuted = !this.isMuted
  510. // #ifdef H5
  511. if (this.$refs.jessibucaRef) {
  512. if (this.isMuted) {
  513. this.$refs.jessibucaRef.mute()
  514. } else {
  515. this.$refs.jessibucaRef.cancelMute()
  516. }
  517. }
  518. // #endif
  519. // #ifdef MP-WEIXIN
  520. // 小程序环境中直接通过属性绑定控制静音
  521. // #endif
  522. },
  523. // 全屏切换
  524. toggleFullscreen() {
  525. if (!this.isPlaying) {
  526. // 如果视频未播放,先开始播放
  527. this.togglePlayState()
  528. // 延迟执行全屏操作,等待视频元素加载
  529. setTimeout(() => {
  530. this.setFullscreen(true)
  531. }, 500)
  532. } else {
  533. this.setFullscreen(!this.isFullscreen)
  534. }
  535. },
  536. // 设置全屏状态
  537. setFullscreen(fullscreen) {
  538. this.isFullscreen = fullscreen
  539. // #ifdef H5
  540. // 浏览器环境
  541. if (this.isFullscreen) {
  542. // 如果是移动设备,尝试使用系统全屏
  543. const ua = navigator.userAgent.toLowerCase()
  544. const isMobile = /mobile|android|iphone|ipad/.test(ua)
  545. if (isMobile && this.$refs.jessibucaRef) {
  546. // 在移动设备上使用Jessibuca的全屏API
  547. this.$refs.jessibucaRef.fullscreenSwich()
  548. } else {
  549. // 适配屏幕尺寸
  550. setTimeout(() => {
  551. if (this.$refs.jessibucaRef) {
  552. this.$refs.jessibucaRef.resize()
  553. }
  554. }, 300)
  555. }
  556. // 锁定屏幕方向为横屏
  557. if (window.screen && window.screen.orientation && window.screen.orientation.lock) {
  558. window.screen.orientation.lock('landscape').catch(err => {
  559. console.error('无法锁定屏幕方向:', err)
  560. })
  561. }
  562. } else {
  563. // 退出全屏状态
  564. if (this.$refs.jessibucaRef) {
  565. // 在某些情况下需要先调用Jessibuca的退出全屏
  566. if (this.$refs.jessibucaRef.isFullscreen()) {
  567. this.$refs.jessibucaRef.fullscreenSwich()
  568. }
  569. // 重置视频大小
  570. setTimeout(() => {
  571. if (this.$refs.jessibucaRef) {
  572. this.$refs.jessibucaRef.resize()
  573. }
  574. }, 300)
  575. }
  576. // 解除屏幕方向锁定
  577. if (window.screen && window.screen.orientation && window.screen.orientation.unlock) {
  578. window.screen.orientation.unlock()
  579. }
  580. }
  581. // #endif
  582. // #ifdef MP-WEIXIN
  583. // 小程序环境
  584. if (this.livePlayerContext) {
  585. if (this.isFullscreen) {
  586. this.livePlayerContext.requestFullScreen({
  587. direction: 90, // 横屏
  588. success: () => {
  589. console.log('进入全屏模式成功')
  590. },
  591. fail: (err) => {
  592. console.error('进入全屏模式失败:', err)
  593. }
  594. })
  595. } else {
  596. this.livePlayerContext.exitFullScreen({
  597. success: () => {
  598. console.log('退出全屏模式成功')
  599. },
  600. fail: (err) => {
  601. console.error('退出全屏模式失败:', err)
  602. }
  603. })
  604. }
  605. }
  606. // #endif
  607. },
  608. // 截图
  609. takeScreenshot() {
  610. // #ifdef H5
  611. if (this.$refs.jessibucaRef && this.isPlaying) {
  612. this.$refs.jessibucaRef.screenshot()
  613. uni.showToast({
  614. title: '截图已保存',
  615. icon: 'success'
  616. })
  617. } else {
  618. uni.showToast({
  619. title: '请先播放视频',
  620. icon: 'none'
  621. })
  622. }
  623. // #endif
  624. // #ifdef MP-WEIXIN
  625. if (this.livePlayerContext && this.isPlaying) {
  626. this.livePlayerContext.snapshot({
  627. success: (res) => {
  628. console.log('截图成功:', res.tempImagePath)
  629. // 可以保存到相册或做其他处理
  630. uni.saveImageToPhotosAlbum({
  631. filePath: res.tempImagePath,
  632. success: () => {
  633. uni.showToast({
  634. title: '截图已保存到相册',
  635. icon: 'success'
  636. })
  637. },
  638. fail: (err) => {
  639. console.error('保存截图失败:', err)
  640. uni.showToast({
  641. title: '保存截图失败',
  642. icon: 'none'
  643. })
  644. }
  645. })
  646. },
  647. fail: (err) => {
  648. console.error('截图失败:', err)
  649. uni.showToast({
  650. title: '截图失败',
  651. icon: 'none'
  652. })
  653. }
  654. })
  655. } else {
  656. uni.showToast({
  657. title: '请先播放视频',
  658. icon: 'none'
  659. })
  660. }
  661. // #endif
  662. },
  663. // 小程序播放器状态变化处理
  664. onStateChange(e) {
  665. console.log('播放器状态变化:', e.detail)
  666. const state = e.detail.code
  667. // 根据状态码处理
  668. switch (state) {
  669. case 2001: // 已连接服务器
  670. console.log('已连接服务器')
  671. break
  672. case 2002: // 已连接服务器,开始拉流
  673. console.log('开始拉流')
  674. break
  675. case 2003: // 网络接收到首个视频帧
  676. console.log('网络接收到首个视频帧')
  677. break
  678. case 2004: // 视频播放开始
  679. console.log('视频播放开始')
  680. break
  681. case 2005: // 视频播放进度
  682. console.log('视频播放进度')
  683. break
  684. case 2006: // 视频播放结束
  685. console.log('视频播放结束')
  686. this.isPlaying = false
  687. break
  688. case 2007: // 视频播放Loading
  689. console.log('视频播放Loading')
  690. break
  691. case 2008: // 解码器启动
  692. console.log('解码器启动')
  693. break
  694. case 2009: // 视频分辨率改变
  695. console.log('视频分辨率改变')
  696. break
  697. case -2301: // 网络断连,且重新连接亦不能恢复,播放器已停止
  698. console.error('网络断连,且重新连接亦不能恢复,播放器已停止')
  699. this.isPlaying = false
  700. uni.showToast({
  701. title: '网络断连,请重试',
  702. icon: 'none'
  703. })
  704. break
  705. case -2302: // 获取加速拉流地址失败
  706. console.error('获取加速拉流地址失败')
  707. break
  708. case 2101: // 当前视频帧解码失败
  709. console.error('当前视频帧解码失败')
  710. break
  711. case 2102: // 当前音频帧解码失败
  712. console.error('当前音频帧解码失败')
  713. break
  714. case 2103: // 网络断连, 已启动自动重连
  715. console.warn('网络断连, 已启动自动重连')
  716. break
  717. case 2104: // 网络断连, 重连中...
  718. console.warn('网络断连, 重连中...')
  719. break
  720. case 2105: // 网络断连, 重连成功
  721. console.log('网络断连, 重连成功')
  722. break
  723. case 2106: // 网络断连, 重连失败
  724. console.error('网络断连, 重连失败')
  725. break
  726. case 2107: // 播放器连接超时
  727. console.error('播放器连接超时')
  728. break
  729. case 2108: // 获取点播文件信息失败
  730. console.error('获取点播文件信息失败')
  731. break
  732. default:
  733. console.log('其他状态:', state)
  734. }
  735. },
  736. // 小程序全屏状态变化
  737. onFullscreenChange(e) {
  738. this.isFullscreen = e.detail.fullScreen
  739. console.log('全屏状态变化:', this.isFullscreen)
  740. },
  741. // 初始化流地址
  742. initStreamUrl() {
  743. const originalUrl = this.deviceInfo.originalStreamUrl
  744. // 使用配置中的流服务器信息
  745. const streamUrls = buildPlatformStreamUrls(originalUrl, {
  746. streamServer: config.streamServer,
  747. fallbackToHls: true
  748. })
  749. // #ifdef H5
  750. this.deviceInfo.streamUrl = streamUrls.h5Url
  751. // #endif
  752. // #ifdef MP-WEIXIN
  753. // 检查是否可以在小程序中播放
  754. if (streamUrls.miniProgramUrl) {
  755. this.deviceInfo.streamUrl = streamUrls.miniProgramUrl
  756. } else {
  757. // 如果没有可用的小程序播放地址,显示提示
  758. uni.showToast({
  759. title: '当前视频流不支持小程序播放',
  760. icon: 'none',
  761. duration: 3000
  762. })
  763. }
  764. // #endif
  765. console.log('初始化流地址:', this.deviceInfo.streamUrl)
  766. },
  767. // 切换九宫格视图
  768. toggleGridView() {
  769. this.isGridView = !this.isGridView
  770. uni.showToast({
  771. title: this.isGridView ? '切换到多画面模式' : '切换到单画面模式',
  772. icon: 'none'
  773. })
  774. },
  775. // 语音对讲
  776. toggleVoiceIntercom() {
  777. this.isVoiceActive = !this.isVoiceActive
  778. if (this.isVoiceActive) {
  779. uni.showToast({
  780. title: '语音对讲已开启',
  781. icon: 'none'
  782. })
  783. } else {
  784. uni.showToast({
  785. title: '语音对讲已关闭',
  786. icon: 'none'
  787. })
  788. }
  789. },
  790. // 云台控制
  791. controlPTZ(direction, isStart) {
  792. // 模拟云台控制
  793. if (isStart) {
  794. console.log(`开始控制云台: ${direction}`)
  795. uni.vibrateShort() // 短震动反馈
  796. } else {
  797. console.log(`停止控制云台: ${direction}`)
  798. }
  799. },
  800. // 云台复位
  801. resetPTZ() {
  802. console.log('云台复位')
  803. uni.vibrateShort() // 短震动反馈
  804. uni.showToast({
  805. title: '云台已复位',
  806. icon: 'none'
  807. })
  808. },
  809. // 变焦控制
  810. controlZoom(type, isStart) {
  811. // 模拟变焦控制
  812. if (isStart) {
  813. console.log(`开始控制变焦: ${type}`)
  814. uni.vibrateShort() // 短震动反馈
  815. } else {
  816. console.log(`停止控制变焦: ${type}`)
  817. }
  818. },
  819. // 切换缩放模式
  820. toggleZoom() {
  821. this.isZoomMode = !this.isZoomMode
  822. uni.showToast({
  823. title: this.isZoomMode ? '进入缩放模式' : '退出缩放模式',
  824. icon: 'none'
  825. })
  826. },
  827. // 添加预设位
  828. addPreset() {
  829. uni.showModal({
  830. title: '添加预设位',
  831. content: '是否保存当前位置为预设位?',
  832. success: (res) => {
  833. if (res.confirm) {
  834. uni.showToast({
  835. title: '预设位已保存',
  836. icon: 'success'
  837. })
  838. }
  839. }
  840. })
  841. },
  842. // 处理告警点击
  843. handleAlert(item) {
  844. uni.navigateTo({
  845. url: `/pages/alerts/alert-detail?alertId=${item.id}&deviceId=${this.deviceInfo.deviceId}`
  846. })
  847. },
  848. // 跳转到历史视频页面
  849. navigateToHistory() {
  850. uni.navigateTo({
  851. url: `/pages/video/history?deviceId=${this.deviceInfo.deviceId}`
  852. })
  853. },
  854. // 视频播放错误处理
  855. onVideoError(e) {
  856. console.error('视频播放错误:', e);
  857. uni.showToast({
  858. title: '视频加载失败,请检查网络连接',
  859. icon: 'none',
  860. duration: 2000
  861. });
  862. // 在H5环境,尝试重新加载或使用备用流
  863. // #ifdef H5
  864. setTimeout(() => {
  865. if (this.isPlaying && this.$refs.jessibucaRef) {
  866. console.log('尝试重新加载视频流');
  867. // 可以尝试使用备用流地址
  868. if (this.deviceInfo.originalStreamUrl !== config.streamServer.wsFlvServer) {
  869. this.deviceInfo.streamUrl = config.streamServer.wsFlvServer;
  870. this.$nextTick(() => {
  871. if (this.$refs.jessibucaRef) {
  872. this.$refs.jessibucaRef.play();
  873. }
  874. });
  875. }
  876. }
  877. }, 3000);
  878. // #endif
  879. },
  880. // 更新时间
  881. startTimeUpdate() {
  882. // 模拟时间更新
  883. setInterval(() => {
  884. const now = new Date()
  885. const hours = String(now.getHours()).padStart(2, '0')
  886. const minutes = String(now.getMinutes()).padStart(2, '0')
  887. const seconds = String(now.getSeconds()).padStart(2, '0')
  888. this.currentTime = `${hours}:${minutes}:${seconds}`
  889. }, 1000)
  890. },
  891. // 设置全屏状态监听
  892. setupFullscreenListener() {
  893. this.fullscreenChangeHandler = () => {
  894. const isFullscreen = !!(
  895. document.fullscreenElement ||
  896. document.mozFullScreenElement ||
  897. document.webkitFullscreenElement ||
  898. document.msFullscreenElement
  899. );
  900. if (this.isFullscreen !== isFullscreen) {
  901. this.isFullscreen = isFullscreen;
  902. }
  903. };
  904. // document.addEventListener('fullscreenchange', this.fullscreenChangeHandler);
  905. // document.addEventListener('webkitfullscreenchange', this.fullscreenChangeHandler);
  906. // document.addEventListener('mozfullscreenchange', this.fullscreenChangeHandler);
  907. // document.addEventListener('MSFullscreenChange', this.fullscreenChangeHandler);
  908. },
  909. // 设置键盘监听
  910. setupKeyboardListener() {
  911. this.keydownHandler = (event) => {
  912. // ESC键退出全屏
  913. if (event.key === 'Escape' && this.isFullscreen) {
  914. this.setFullscreen(false);
  915. }
  916. // F键切换全屏
  917. if (event.key === 'f' || event.key === 'F') {
  918. this.toggleFullscreen();
  919. event.preventDefault();
  920. }
  921. };
  922. // document.addEventListener('keydown', this.keydownHandler);
  923. }
  924. }
  925. }
  926. </script>
  927. <style>
  928. /* 基础样式 */
  929. .container {
  930. display: flex;
  931. flex-direction: column;
  932. min-height: 100vh;
  933. background-color: #F8FCF9;
  934. padding-bottom: 30rpx;
  935. }
  936. /* 设备头部样式 */
  937. .device-header {
  938. background-color: #FFFFFF;
  939. border-radius: 20rpx;
  940. padding: 26rpx 30rpx 30rpx;
  941. margin: 20rpx 30rpx 20rpx;
  942. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  943. }
  944. .device-info-row {
  945. display: flex;
  946. justify-content: space-between;
  947. align-items: flex-start;
  948. margin-bottom: 24rpx;
  949. }
  950. .device-name-container {
  951. display: flex;
  952. flex-direction: column;
  953. align-items: flex-start;
  954. }
  955. .device-name {
  956. font-size: 34rpx;
  957. color: #333333;
  958. font-weight: 600;
  959. margin-bottom: 10rpx;
  960. }
  961. .status-tag {
  962. padding: 4rpx 12rpx 4rpx 24rpx;
  963. border-radius: 30rpx;
  964. font-size: 22rpx;
  965. font-weight: 500;
  966. flex-shrink: 0;
  967. position: relative;
  968. overflow: hidden;
  969. }
  970. .status-online {
  971. background-color: rgba(76, 175, 80, 0.1);
  972. color: #3BB44A;
  973. }
  974. .status-offline {
  975. background-color: rgba(245, 108, 108, 0.1);
  976. color: #F56C6C;
  977. padding-left: 24rpx;
  978. }
  979. .status-dot {
  980. position: absolute;
  981. width: 6rpx;
  982. height: 6rpx;
  983. background-color: #3BB44A;
  984. border-radius: 50%;
  985. top: 50%;
  986. left: 12rpx;
  987. transform: translateY(-50%);
  988. box-shadow: 0 0 4rpx rgba(76, 175, 80, 0.8);
  989. animation: blink 1.5s infinite;
  990. display: inline-block;
  991. }
  992. .status-dot.offline-dot {
  993. background-color: #F56C6C;
  994. box-shadow: 0 0 4rpx rgba(245, 108, 108, 0.8);
  995. }
  996. @keyframes blink {
  997. 0% {
  998. opacity: 0.4;
  999. }
  1000. 50% {
  1001. opacity: 1;
  1002. }
  1003. 100% {
  1004. opacity: 0.4;
  1005. }
  1006. }
  1007. .device-meta-row {
  1008. display: flex;
  1009. flex-direction: column;
  1010. }
  1011. .device-meta-item {
  1012. display: flex;
  1013. align-items: center;
  1014. font-size: 26rpx;
  1015. margin-top: 18rpx;
  1016. }
  1017. .meta-icon {
  1018. width: 36rpx;
  1019. height: 36rpx;
  1020. display: flex;
  1021. align-items: center;
  1022. justify-content: center;
  1023. margin-right: 8rpx;
  1024. }
  1025. .meta-label {
  1026. color: #999999;
  1027. min-width: 140rpx;
  1028. font-size: 26rpx;
  1029. }
  1030. .meta-value {
  1031. color: #333333;
  1032. flex: 1;
  1033. font-size: 26rpx;
  1034. }
  1035. /* 视频预览区域 */
  1036. .video-section {
  1037. margin: 0 30rpx 20rpx;
  1038. position: relative;
  1039. z-index: 1;
  1040. }
  1041. .video-container {
  1042. position: relative;
  1043. width: 100%;
  1044. height: 420rpx;
  1045. /* 16:9比例 */
  1046. background-color: #000000;
  1047. border-radius: 16rpx;
  1048. overflow: hidden;
  1049. box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.1);
  1050. transition: all 0.3s ease;
  1051. }
  1052. .video-container.fullscreen-mode {
  1053. position: fixed;
  1054. top: 0;
  1055. left: 0;
  1056. width: 100vw;
  1057. height: 100vh;
  1058. margin: 0;
  1059. z-index: 9999;
  1060. border-radius: 0;
  1061. }
  1062. .video-container.fullscreen-mode .video-controls {
  1063. padding: 30rpx;
  1064. }
  1065. .video-container.fullscreen-mode .top-controls,
  1066. .video-container.fullscreen-mode .bottom-controls {
  1067. opacity: 0;
  1068. transition: opacity 0.3s ease;
  1069. }
  1070. .video-container.fullscreen-mode:hover .top-controls,
  1071. .video-container.fullscreen-mode:hover .bottom-controls {
  1072. opacity: 1;
  1073. }
  1074. .video-player,
  1075. .video-placeholder {
  1076. width: 100%;
  1077. height: 100%;
  1078. object-fit: contain;
  1079. }
  1080. .video-controls {
  1081. position: absolute;
  1082. top: 0;
  1083. left: 0;
  1084. width: 100%;
  1085. height: 100%;
  1086. display: flex;
  1087. flex-direction: column;
  1088. justify-content: space-between;
  1089. padding: 20rpx;
  1090. box-sizing: border-box;
  1091. background: linear-gradient(to bottom, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0) 30%, rgba(0, 0, 0, 0) 70%, rgba(0, 0, 0, 0.4) 100%);
  1092. }
  1093. .control-row {
  1094. display: flex;
  1095. justify-content: space-between;
  1096. align-items: center;
  1097. width: 100%;
  1098. }
  1099. .top-controls {
  1100. height: 80rpx;
  1101. }
  1102. .center-controls {
  1103. height: 120rpx;
  1104. justify-content: center;
  1105. align-items: center;
  1106. }
  1107. .bottom-controls {
  1108. height: 80rpx;
  1109. }
  1110. .signal-indicator {
  1111. display: flex;
  1112. align-items: center;
  1113. color: #FFFFFF;
  1114. font-size: 24rpx;
  1115. background-color: rgba(0, 0, 0, 0.5);
  1116. padding: 8rpx 16rpx;
  1117. border-radius: 30rpx;
  1118. }
  1119. .signal-indicator svg {
  1120. margin-right: 8rpx;
  1121. }
  1122. .signal-text {
  1123. font-weight: 500;
  1124. }
  1125. .fullscreen-button {
  1126. width: 60rpx;
  1127. height: 60rpx;
  1128. display: flex;
  1129. align-items: center;
  1130. justify-content: center;
  1131. color: #FFFFFF;
  1132. background-color: rgba(0, 0, 0, 0.5);
  1133. border-radius: 50%;
  1134. transition: all 0.2s;
  1135. }
  1136. .fullscreen-button:active {
  1137. background-color: rgba(76, 175, 80, 0.7);
  1138. transform: scale(0.9);
  1139. }
  1140. .video-time {
  1141. color: #FFFFFF;
  1142. font-size: 26rpx;
  1143. background-color: rgba(0, 0, 0, 0.5);
  1144. padding: 6rpx 16rpx;
  1145. border-radius: 30rpx;
  1146. font-weight: 500;
  1147. }
  1148. .play-button {
  1149. width: 100rpx;
  1150. height: 100rpx;
  1151. border-radius: 50%;
  1152. background-color: rgba(255, 255, 255, 0.9);
  1153. display: flex;
  1154. align-items: center;
  1155. justify-content: center;
  1156. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.5);
  1157. transition: all 0.2s;
  1158. transform: scale(1);
  1159. }
  1160. .play-button:active {
  1161. transform: scale(0.92);
  1162. background-color: rgba(255, 255, 255, 1);
  1163. }
  1164. .center-button-container {
  1165. width: 100%;
  1166. height: 100%;
  1167. display: flex;
  1168. align-items: center;
  1169. justify-content: center;
  1170. }
  1171. .pause-icon {
  1172. width: 80rpx;
  1173. height: 80rpx;
  1174. border-radius: 50%;
  1175. background-color: rgba(0, 0, 0, 0.5);
  1176. display: flex;
  1177. align-items: center;
  1178. justify-content: center;
  1179. opacity: 0;
  1180. transition: opacity 0.3s;
  1181. }
  1182. .center-button-container:active .pause-icon {
  1183. opacity: 1;
  1184. background-color: rgba(76, 175, 80, 0.7);
  1185. }
  1186. /* 云台控制区域 */
  1187. .ptz-section {
  1188. margin: 0 30rpx 20rpx;
  1189. background-color: #FFFFFF;
  1190. border-radius: 20rpx;
  1191. padding: 24rpx;
  1192. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
  1193. }
  1194. .section-title {
  1195. font-size: 30rpx;
  1196. font-weight: 600;
  1197. color: #333333;
  1198. margin-bottom: 20rpx;
  1199. padding: 0 10rpx;
  1200. display: flex;
  1201. align-items: center;
  1202. }
  1203. .alert-badge {
  1204. background-color: #F56C6C;
  1205. color: #FFFFFF;
  1206. font-size: 22rpx;
  1207. border-radius: 30rpx;
  1208. padding: 2rpx 12rpx;
  1209. margin-left: 12rpx;
  1210. font-weight: normal;
  1211. min-width: 32rpx;
  1212. text-align: center;
  1213. }
  1214. .ptz-container {
  1215. display: flex;
  1216. flex-direction: column;
  1217. align-items: center;
  1218. padding: 20rpx 0;
  1219. }
  1220. .ptz-circle-container {
  1221. position: relative;
  1222. width: 300rpx;
  1223. height: 300rpx;
  1224. margin: 20rpx 0;
  1225. }
  1226. .ptz-arrow {
  1227. position: absolute;
  1228. width: 70rpx;
  1229. height: 70rpx;
  1230. display: flex;
  1231. align-items: center;
  1232. justify-content: center;
  1233. background-color: #F0F9F0;
  1234. border-radius: 50%;
  1235. color: #3BB44A;
  1236. font-size: 36rpx;
  1237. transition: all 0.2s;
  1238. box-shadow: 0 2rpx 10rpx rgba(76, 175, 80, 0.15);
  1239. }
  1240. .ptz-up {
  1241. top: 0;
  1242. left: 50%;
  1243. transform: translateX(-50%);
  1244. }
  1245. .ptz-down {
  1246. bottom: 0;
  1247. left: 50%;
  1248. transform: translateX(-50%);
  1249. }
  1250. .ptz-left {
  1251. left: 0;
  1252. top: 50%;
  1253. transform: translateY(-50%);
  1254. }
  1255. .ptz-right {
  1256. right: 0;
  1257. top: 50%;
  1258. transform: translateY(-50%);
  1259. }
  1260. .ptz-center {
  1261. position: absolute;
  1262. top: 50%;
  1263. left: 50%;
  1264. transform: translate(-50%, -50%);
  1265. width: 90rpx;
  1266. height: 90rpx;
  1267. display: flex;
  1268. align-items: center;
  1269. justify-content: center;
  1270. background-color: #F0F9F0;
  1271. border-radius: 50%;
  1272. color: #3BB44A;
  1273. font-size: 36rpx;
  1274. transition: all 0.2s;
  1275. box-shadow: 0 2rpx 10rpx rgba(76, 175, 80, 0.15);
  1276. }
  1277. .ptz-arrow:active,
  1278. .ptz-center:active {
  1279. background-color: #3BB44A;
  1280. transform-origin: center;
  1281. }
  1282. .ptz-arrow:active svg path,
  1283. .ptz-center:active svg path {
  1284. fill: #FFFFFF;
  1285. }
  1286. .ptz-up:active {
  1287. transform: scale(0.92) translateX(-50%);
  1288. }
  1289. .ptz-down:active {
  1290. transform: scale(0.92) translateX(-50%);
  1291. }
  1292. .ptz-left:active {
  1293. transform: scale(0.92) translateY(-50%);
  1294. }
  1295. .ptz-right:active {
  1296. transform: scale(0.92) translateY(-50%);
  1297. }
  1298. .ptz-center:active {
  1299. transform: translate(-50%, -50%) scale(0.92);
  1300. }
  1301. .ptz-bottom-controls {
  1302. display: flex;
  1303. justify-content: space-between;
  1304. align-items: center;
  1305. width: 300rpx;
  1306. margin-top: 30rpx;
  1307. }
  1308. .ptz-bottom-button {
  1309. width: 80rpx;
  1310. height: 80rpx;
  1311. border-radius: 50%;
  1312. background-color: #F0F9F0;
  1313. display: flex;
  1314. align-items: center;
  1315. justify-content: center;
  1316. color: #3BB44A;
  1317. font-size: 32rpx;
  1318. font-weight: 500;
  1319. transition: all 0.2s;
  1320. box-shadow: 0 2rpx 10rpx rgba(76, 175, 80, 0.1);
  1321. }
  1322. .ptz-bottom-button:active {
  1323. background-color: #3BB44A;
  1324. color: #FFFFFF;
  1325. transform: scale(0.92);
  1326. }
  1327. .ptz-bottom-button:active svg path {
  1328. fill: #FFFFFF;
  1329. }
  1330. /* 快捷功能按钮 */
  1331. .quick-actions {
  1332. display: flex;
  1333. justify-content: space-evenly;
  1334. margin: 0 30rpx 20rpx;
  1335. background-color: #FFFFFF;
  1336. border-radius: 20rpx;
  1337. padding: 24rpx 30rpx;
  1338. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
  1339. }
  1340. .action-button {
  1341. display: flex;
  1342. flex-direction: column;
  1343. align-items: center;
  1344. width: 160rpx;
  1345. }
  1346. .action-icon {
  1347. width: 100rpx;
  1348. height: 100rpx;
  1349. border-radius: 50%;
  1350. background-color: #F0F9F0;
  1351. display: flex;
  1352. align-items: center;
  1353. justify-content: center;
  1354. color: #3BB44A;
  1355. margin-bottom: 12rpx;
  1356. transition: all 0.2s;
  1357. box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.1);
  1358. }
  1359. .action-icon:active {
  1360. background-color: #3BB44A;
  1361. transform: scale(0.92);
  1362. }
  1363. .action-icon:active svg path {
  1364. fill: #FFFFFF;
  1365. }
  1366. .action-text {
  1367. font-size: 24rpx;
  1368. color: #666666;
  1369. text-align: center;
  1370. }
  1371. /* 告警信息列表 */
  1372. .alerts-section {
  1373. margin: 0 30rpx;
  1374. background-color: #FFFFFF;
  1375. border-radius: 20rpx;
  1376. padding: 24rpx;
  1377. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
  1378. }
  1379. .alerts-list {
  1380. display: flex;
  1381. flex-direction: column;
  1382. }
  1383. .alert-item {
  1384. display: flex;
  1385. align-items: center;
  1386. padding: 24rpx 20rpx;
  1387. border-radius: 12rpx;
  1388. margin-bottom: 16rpx;
  1389. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  1390. position: relative;
  1391. }
  1392. .alert-urgent {
  1393. background-color: #FEF3F3;
  1394. border-left: 4rpx solid #F56C6C;
  1395. }
  1396. .alert-warning {
  1397. background-color: #FFF8E6;
  1398. border-left: 4rpx solid #E6A23C;
  1399. }
  1400. .alert-info {
  1401. background-color: #F2FAF5;
  1402. border-left: 4rpx solid #67C23A;
  1403. }
  1404. .alert-item-icon {
  1405. width: 50rpx;
  1406. height: 50rpx;
  1407. display: flex;
  1408. align-items: center;
  1409. justify-content: center;
  1410. margin-right: 16rpx;
  1411. }
  1412. .alert-item-info {
  1413. flex: 1;
  1414. display: flex;
  1415. flex-direction: column;
  1416. }
  1417. .alert-item-type {
  1418. font-size: 28rpx;
  1419. color: #333333;
  1420. font-weight: 500;
  1421. margin-bottom: 8rpx;
  1422. }
  1423. .alert-item-level {
  1424. font-size: 24rpx;
  1425. color: #999999;
  1426. }
  1427. .alert-item-time {
  1428. font-size: 24rpx;
  1429. color: #999999;
  1430. margin-left: 16rpx;
  1431. min-width: 100rpx;
  1432. text-align: right;
  1433. }
  1434. .empty-alert {
  1435. padding: 60rpx 0;
  1436. display: flex;
  1437. justify-content: center;
  1438. align-items: center;
  1439. }
  1440. .empty-text {
  1441. font-size: 28rpx;
  1442. color: #999999;
  1443. }
  1444. </style>