detail-camera.vue 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238
  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
  9. class="status-tag"
  10. :class="deviceInfo.status === 'online' ? 'status-online' : 'status-offline'"
  11. >
  12. <view class="status-dot" :class="{'offline-dot': deviceInfo.status === 'offline'}"></view>
  13. {{ deviceInfo.status === 'online' ? '在线' : '离线' }}
  14. </view>
  15. </view>
  16. <view class="refresh-btn" :class="{'refreshing': isRefreshing}" @tap="refreshData">
  17. <image src="/static/icons/refresh_icon.png" mode="aspectFit" style="width: 22px; height: 22px;"></image>
  18. </view>
  19. </view>
  20. <view class="device-meta-row">
  21. <view class="device-meta-item">
  22. <view class="meta-icon">
  23. <image src="/static/icons/device_icon.png" mode="aspectFit" style="width: 36rpx; height: 36rpx;"></image>
  24. </view>
  25. <text class="meta-label">设备编号:</text>
  26. <text class="meta-value">{{ deviceInfo.deviceId }}</text>
  27. </view>
  28. <view class="device-meta-item">
  29. <view class="meta-icon">
  30. <image src="/static/icons/location_icon.png" mode="aspectFit" 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;"></image>
  38. </view>
  39. <text class="meta-label">最近更新:</text>
  40. <text class="meta-value">{{ deviceInfo.lastUpdate }}</text>
  41. </view>
  42. </view>
  43. </view>
  44. <!-- 视频预览区域 -->
  45. <view class="video-section">
  46. <view class="video-container" :class="{'fullscreen-mode': isFullscreen}">
  47. <image v-if="!isPlaying" src="/static/images/video-placeholder.jpg" mode="aspectFill" class="video-placeholder"></image>
  48. <!-- 使用Jessibuca组件替换原来的video标签 -->
  49. <Jessibuca v-if="isPlaying" ref="jessibucaRef" :videoUrl="deviceInfo.streamUrl" :hasAudio="true" />
  50. <!-- 视频控制层 -->
  51. <view class="video-controls">
  52. <view class="control-row top-controls">
  53. <view class="signal-indicator">
  54. <image src="/static/icons/signal_icon.png" mode="aspectFit" style="width: 16px; height: 16px;"></image>
  55. <text class="signal-text">信号良好</text>
  56. </view>
  57. <view class="fullscreen-button" @click="toggleFullscreen">
  58. <image src="/static/icons/resize_icon.png" mode="aspectFit" style="width: 20px; height: 20px;"></image>
  59. </view>
  60. </view>
  61. <view class="control-row center-controls">
  62. <view v-if="!isPlaying" class="play-button" @click="togglePlayState">
  63. <image src="/static/icons/play_icon.png" mode="aspectFit" style="width: 32px; height: 32px;"></image>
  64. </view>
  65. <view v-else class="center-button-container" @click="togglePlayState">
  66. <view class="pause-icon">
  67. <image src="/static/icons/pause_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  68. </view>
  69. </view>
  70. </view>
  71. <view class="control-row bottom-controls">
  72. <view class="video-time">{{ currentTime }}</view>
  73. </view>
  74. </view>
  75. </view>
  76. </view>
  77. <!-- 云台控制区域 -->
  78. <view class="ptz-section">
  79. <view class="section-title">云台控制</view>
  80. <view class="ptz-container">
  81. <!-- 圆形云台控制 -->
  82. <view class="ptz-circle-container">
  83. <!-- 上箭头 -->
  84. <view class="ptz-arrow ptz-up" @touchstart="controlPTZ('up', true)" @touchend="controlPTZ('up', false)">
  85. <image src="/static/icons/arrow_up_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  86. </view>
  87. <!-- 左箭头 -->
  88. <view class="ptz-arrow ptz-left" @touchstart="controlPTZ('left', true)" @touchend="controlPTZ('left', false)">
  89. <image src="/static/icons/arrow_left_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  90. </view>
  91. <!-- 中心拍照按钮 -->
  92. <view class="ptz-center" @click="takeScreenshot">
  93. <image src="/static/icons/camera_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  94. </view>
  95. <!-- 右箭头 -->
  96. <view class="ptz-arrow ptz-right" @touchstart="controlPTZ('right', true)" @touchend="controlPTZ('right', false)">
  97. <image src="/static/icons/arrow_right_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  98. </view>
  99. <!-- 下箭头 -->
  100. <view class="ptz-arrow ptz-down" @touchstart="controlPTZ('down', true)" @touchend="controlPTZ('down', false)">
  101. <image src="/static/icons/arrow_down_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  102. </view>
  103. </view>
  104. <!-- 底部操作按钮 -->
  105. <view class="ptz-bottom-controls">
  106. <view class="ptz-bottom-button" @touchstart="controlZoom('out', true)" @touchend="controlZoom('out', false)">
  107. <image src="/static/icons/zoom01_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  108. </view>
  109. <view class="ptz-bottom-button" @click="resetPTZ">
  110. <image src="/static/icons/resetPTZ_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  111. </view>
  112. <view class="ptz-bottom-button" @touchstart="controlZoom('in', true)" @touchend="controlZoom('in', false)">
  113. <image src="/static/icons/zoom02_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  114. </view>
  115. </view>
  116. </view>
  117. </view>
  118. <!-- 快捷功能按钮 -->
  119. <view class="quick-actions">
  120. <view class="action-button" @click="toggleVoiceIntercom">
  121. <view class="action-icon">
  122. <image src="/static/icons/Voice_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  123. </view>
  124. <text class="action-text">语音对讲</text>
  125. </view>
  126. <view class="action-button" @click="toggleMute">
  127. <view class="action-icon">
  128. <image v-if="isMuted" src="/static/icons/muted_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  129. <image v-else src="/static/icons/unmuted_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  130. </view>
  131. <text class="action-text">{{ isMuted ? '取消静音' : '静音' }}</text>
  132. </view>
  133. <view class="action-button" @click="navigateToHistory">
  134. <view class="action-icon">
  135. <image src="/static/icons/action_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  136. </view>
  137. <text class="action-text">视频回看</text>
  138. </view>
  139. </view>
  140. <!-- 告警信息列表 -->
  141. <view class="alerts-section">
  142. <view class="section-title">
  143. <text>告警信息</text>
  144. <view class="alert-badge" v-if="getUnhandledAlerts.length > 0">{{ getUnhandledAlerts.length }}</view>
  145. </view>
  146. <view class="alerts-list" v-if="getUnhandledAlerts.length > 0">
  147. <view
  148. v-for="(item, index) in getUnhandledAlerts"
  149. :key="index"
  150. class="alert-item"
  151. :class="{
  152. 'alert-urgent': item.level === 'high',
  153. 'alert-warning': item.level === 'medium',
  154. 'alert-info': item.level === 'low'
  155. }"
  156. >
  157. <view class="alert-item-icon">
  158. <image v-if="item.level === 'high'" src="/static/icons/warning_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  159. <image v-else-if="item.level === 'medium'" src="/static/icons/info_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  160. <image v-else src="/static/icons/success_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  161. </view>
  162. <view class="alert-item-info">
  163. <text class="alert-item-type">{{ item.type }}</text>
  164. <text class="alert-item-level">
  165. {{ item.level === 'high' ? '紧急' : item.level === 'medium' ? '警告' : '提示' }}
  166. </text>
  167. </view>
  168. <view class="alert-item-time">{{ item.time }}</view>
  169. </view>
  170. </view>
  171. <view v-else class="empty-alert">
  172. <text class="empty-text">暂无告警信息</text>
  173. </view>
  174. </view>
  175. </view>
  176. </template>
  177. <script>
  178. // 导入Jessibuca组件
  179. import Jessibuca from '@/components/common/jessibuca.vue'
  180. export default {
  181. components: {
  182. Jessibuca
  183. },
  184. data() {
  185. return {
  186. deviceInfo: {
  187. deviceId: 'DEV1001',
  188. name: '监控设备-1',
  189. status: 'online',
  190. location: '西区B2地块',
  191. lastUpdate: '5分钟前',
  192. streamUrl: 'ws://121.4.16.100:6080/rtp/34020000001110000001_34020000001320000012.live.flv',
  193. alertCount: 3
  194. },
  195. isPlaying: false,
  196. isMuted: false,
  197. isRecording: false,
  198. isFullscreen: false,
  199. isVoiceActive: false,
  200. isGridView: false,
  201. isZoomMode: false,
  202. currentTime: '14:30:25',
  203. // 模拟历史录像数据
  204. recordHistory: [
  205. { id: 1, startTime: '今天 12:30', duration: '00:15:30', url: '' },
  206. { id: 2, startTime: '今天 10:15', duration: '00:05:22', url: '' },
  207. { id: 3, startTime: '昨天 18:45', duration: '00:30:10', url: '' },
  208. { id: 4, startTime: '昨天 14:20', duration: '00:10:05', url: '' }
  209. ],
  210. // 模拟告警数据
  211. alertHistory: [
  212. { id: 1, time: '今天 13:05', type: '移动侦测', status: '未处理', level: 'high' },
  213. { id: 2, time: '今天 09:30', type: '信号异常', status: '未处理', level: 'medium' },
  214. { id: 3, time: '昨天 15:45', type: '设备状态', status: '已处理', level: 'low' },
  215. { id: 4, time: '昨天 10:20', type: '遮挡告警', status: '未处理', level: 'low' }
  216. ],
  217. videoContext: null,
  218. isRefreshing: false
  219. }
  220. },
  221. computed: {
  222. // 获取所有未处理的告警
  223. getUnhandledAlerts() {
  224. return this.alertHistory.filter(alert => alert.status === '未处理');
  225. }
  226. },
  227. mounted() {
  228. // 设置页面标题
  229. uni.setNavigationBarTitle({
  230. title: this.deviceInfo.name
  231. })
  232. // 模拟更新时间
  233. this.startTimeUpdate()
  234. // 加载Jessibuca的JS库
  235. this.loadJessibucaScript()
  236. // 监听全屏状态变化
  237. this.setupFullscreenListener()
  238. // 添加键盘事件监听
  239. this.setupKeyboardListener()
  240. },
  241. beforeUnmount() {
  242. // 移除全屏状态监听
  243. this.removeFullscreenListener()
  244. // 移除键盘事件监听
  245. this.removeKeyboardListener()
  246. },
  247. onLoad(options) {
  248. // 如果有传入设备ID,则获取设备信息
  249. if (options && options.id) {
  250. this.fetchDeviceInfo(options.id)
  251. }
  252. },
  253. methods: {
  254. // 加载Jessibuca脚本
  255. loadJessibucaScript() {
  256. const script = document.createElement('script')
  257. script.src = '/static/js/jessibuca/jessibuca.js'
  258. script.onload = () => {
  259. console.log('Jessibuca 脚本加载成功')
  260. }
  261. script.onerror = (error) => {
  262. console.error('Jessibuca 脚本加载失败:', error)
  263. }
  264. document.head.appendChild(script)
  265. },
  266. // 获取设备信息
  267. fetchDeviceInfo(deviceId) {
  268. // 这里应该是API请求,暂时用模拟数据
  269. console.log('获取设备信息:', deviceId)
  270. // 模拟异步获取数据
  271. setTimeout(() => {
  272. // 实际应该是API请求结果
  273. }, 500)
  274. },
  275. // 播放/暂停切换
  276. togglePlayState() {
  277. if (!this.isPlaying) {
  278. // 从未播放状态切换到播放状态
  279. this.isPlaying = true;
  280. // 短暂延迟确保Jessibuca组件已加载
  281. setTimeout(() => {
  282. // 振动反馈
  283. uni.vibrateShort();
  284. }, 300);
  285. } else {
  286. // 从播放状态切换到暂停状态
  287. if (this.$refs.jessibucaRef) {
  288. this.$refs.jessibucaRef.pause();
  289. }
  290. this.isPlaying = false;
  291. // 在页面上显示暂停状态
  292. uni.showToast({
  293. title: '视频已暂停',
  294. icon: 'none',
  295. duration: 1500
  296. });
  297. }
  298. },
  299. // 静音切换
  300. toggleMute() {
  301. this.isMuted = !this.isMuted
  302. if (this.$refs.jessibucaRef) {
  303. if (this.isMuted) {
  304. this.$refs.jessibucaRef.mute()
  305. } else {
  306. this.$refs.jessibucaRef.cancelMute()
  307. }
  308. }
  309. },
  310. // 全屏切换
  311. toggleFullscreen() {
  312. if (!this.isPlaying) {
  313. // 如果视频未播放,先开始播放
  314. this.togglePlayState();
  315. // 延迟执行全屏操作,等待视频元素加载
  316. setTimeout(() => {
  317. this.setFullscreen(true);
  318. }, 500);
  319. } else {
  320. this.setFullscreen(!this.isFullscreen);
  321. }
  322. },
  323. // 设置全屏状态
  324. setFullscreen(fullscreen) {
  325. this.isFullscreen = fullscreen;
  326. // 更新CSS状态
  327. if (this.isFullscreen) {
  328. // 如果是移动设备,尝试使用系统全屏
  329. const ua = navigator.userAgent.toLowerCase();
  330. const isMobile = /mobile|android|iphone|ipad/.test(ua);
  331. if (isMobile && this.$refs.jessibucaRef) {
  332. // 在移动设备上使用Jessibuca的全屏API
  333. this.$refs.jessibucaRef.fullscreenSwich();
  334. } else {
  335. // 适配屏幕尺寸
  336. setTimeout(() => {
  337. if (this.$refs.jessibucaRef) {
  338. this.$refs.jessibucaRef.resize();
  339. }
  340. }, 300);
  341. }
  342. // 锁定屏幕方向为横屏
  343. if (window.screen && window.screen.orientation && window.screen.orientation.lock) {
  344. window.screen.orientation.lock('landscape').catch(err => {
  345. console.error('无法锁定屏幕方向:', err);
  346. });
  347. }
  348. } else {
  349. // 退出全屏状态
  350. if (this.$refs.jessibucaRef) {
  351. // 在某些情况下需要先调用Jessibuca的退出全屏
  352. if (this.$refs.jessibucaRef.isFullscreen()) {
  353. this.$refs.jessibucaRef.fullscreenSwich();
  354. }
  355. // 重置视频大小
  356. setTimeout(() => {
  357. if (this.$refs.jessibucaRef) {
  358. this.$refs.jessibucaRef.resize();
  359. }
  360. }, 300);
  361. }
  362. // 解除屏幕方向锁定
  363. if (window.screen && window.screen.orientation && window.screen.orientation.unlock) {
  364. window.screen.orientation.unlock();
  365. }
  366. }
  367. },
  368. // 切换九宫格视图
  369. toggleGridView() {
  370. this.isGridView = !this.isGridView
  371. uni.showToast({
  372. title: this.isGridView ? '切换到多画面模式' : '切换到单画面模式',
  373. icon: 'none'
  374. })
  375. },
  376. // 截图
  377. takeScreenshot() {
  378. if (this.$refs.jessibucaRef && this.isPlaying) {
  379. this.$refs.jessibucaRef.screenshot();
  380. uni.showToast({
  381. title: '截图已保存',
  382. icon: 'success'
  383. });
  384. } else {
  385. uni.showToast({
  386. title: '请先播放视频',
  387. icon: 'none'
  388. });
  389. }
  390. },
  391. // 语音对讲
  392. toggleVoiceIntercom() {
  393. this.isVoiceActive = !this.isVoiceActive
  394. if (this.isVoiceActive) {
  395. uni.showToast({
  396. title: '语音对讲已开启',
  397. icon: 'none'
  398. })
  399. } else {
  400. uni.showToast({
  401. title: '语音对讲已关闭',
  402. icon: 'none'
  403. })
  404. }
  405. },
  406. // 云台控制
  407. controlPTZ(direction, isStart) {
  408. // 模拟云台控制
  409. if (isStart) {
  410. console.log(`开始控制云台: ${direction}`)
  411. uni.vibrateShort() // 短震动反馈
  412. } else {
  413. console.log(`停止控制云台: ${direction}`)
  414. }
  415. },
  416. // 云台复位
  417. resetPTZ() {
  418. console.log('云台复位')
  419. uni.vibrateShort() // 短震动反馈
  420. uni.showToast({
  421. title: '云台已复位',
  422. icon: 'none'
  423. })
  424. },
  425. // 变焦控制
  426. controlZoom(type, isStart) {
  427. // 模拟变焦控制
  428. if (isStart) {
  429. console.log(`开始控制变焦: ${type}`)
  430. uni.vibrateShort() // 短震动反馈
  431. } else {
  432. console.log(`停止控制变焦: ${type}`)
  433. }
  434. },
  435. // 跳转到历史视频页面
  436. navigateToHistory() {
  437. uni.navigateTo({
  438. url: `/pages/video/history?deviceId=${this.deviceInfo.deviceId}`
  439. })
  440. },
  441. // 更新时间
  442. startTimeUpdate() {
  443. // 模拟时间更新
  444. setInterval(() => {
  445. const now = new Date()
  446. const hours = String(now.getHours()).padStart(2, '0')
  447. const minutes = String(now.getMinutes()).padStart(2, '0')
  448. const seconds = String(now.getSeconds()).padStart(2, '0')
  449. this.currentTime = `${hours}:${minutes}:${seconds}`
  450. }, 1000)
  451. },
  452. // 设置全屏状态监听
  453. setupFullscreenListener() {
  454. this.fullscreenChangeHandler = () => {
  455. const isFullscreen = !!(
  456. document.fullscreenElement ||
  457. document.mozFullScreenElement ||
  458. document.webkitFullscreenElement ||
  459. document.msFullscreenElement
  460. );
  461. if (this.isFullscreen !== isFullscreen) {
  462. this.isFullscreen = isFullscreen;
  463. }
  464. };
  465. document.addEventListener('fullscreenchange', this.fullscreenChangeHandler);
  466. document.addEventListener('webkitfullscreenchange', this.fullscreenChangeHandler);
  467. document.addEventListener('mozfullscreenchange', this.fullscreenChangeHandler);
  468. document.addEventListener('MSFullscreenChange', this.fullscreenChangeHandler);
  469. },
  470. // 移除全屏状态监听
  471. removeFullscreenListener() {
  472. if (this.fullscreenChangeHandler) {
  473. document.removeEventListener('fullscreenchange', this.fullscreenChangeHandler);
  474. document.removeEventListener('webkitfullscreenchange', this.fullscreenChangeHandler);
  475. document.removeEventListener('mozfullscreenchange', this.fullscreenChangeHandler);
  476. document.removeEventListener('MSFullscreenChange', this.fullscreenChangeHandler);
  477. }
  478. },
  479. // 设置键盘监听
  480. setupKeyboardListener() {
  481. this.keydownHandler = (event) => {
  482. // ESC键退出全屏
  483. if (event.key === 'Escape' && this.isFullscreen) {
  484. this.setFullscreen(false);
  485. }
  486. // F键切换全屏
  487. if (event.key === 'f' || event.key === 'F') {
  488. this.toggleFullscreen();
  489. event.preventDefault();
  490. }
  491. };
  492. document.addEventListener('keydown', this.keydownHandler);
  493. },
  494. // 移除键盘监听
  495. removeKeyboardListener() {
  496. if (this.keydownHandler) {
  497. document.removeEventListener('keydown', this.keydownHandler);
  498. }
  499. }
  500. }
  501. }
  502. </script>
  503. <style>
  504. /* 基础样式 */
  505. .container {
  506. display: flex;
  507. flex-direction: column;
  508. min-height: 100vh;
  509. background-color: #F8FCF9;
  510. padding-bottom: 30rpx;
  511. }
  512. /* 设备头部样式 */
  513. .device-header {
  514. background-color: #FFFFFF;
  515. border-radius: 20rpx;
  516. padding: 30rpx;
  517. margin: 30rpx 30rpx 30rpx;
  518. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  519. border: 1rpx solid rgba(210, 237, 217, 0.5);
  520. }
  521. .device-info-row {
  522. display: flex;
  523. justify-content: space-between;
  524. align-items: center;
  525. margin-bottom: 28rpx;
  526. }
  527. .device-name-container {
  528. display: flex;
  529. flex-direction: column;
  530. align-items: flex-start;
  531. }
  532. .device-name {
  533. font-size: 36rpx;
  534. color: #333333;
  535. font-weight: 600;
  536. margin-bottom: 12rpx;
  537. }
  538. .status-tag {
  539. padding: 6rpx 16rpx 6rpx 28rpx;
  540. border-radius: 30rpx;
  541. font-size: 24rpx;
  542. font-weight: 500;
  543. flex-shrink: 0;
  544. position: relative;
  545. overflow: hidden;
  546. }
  547. .status-online {
  548. background-color: rgba(76, 175, 80, 0.1);
  549. color: #3BB44A;
  550. }
  551. .status-offline {
  552. background-color: rgba(245, 108, 108, 0.1);
  553. color: #F56C6C;
  554. padding-left: 28rpx;
  555. }
  556. .status-dot {
  557. position: absolute;
  558. width: 6rpx;
  559. height: 6rpx;
  560. background-color: #3BB44A;
  561. border-radius: 50%;
  562. top: 50%;
  563. left: 12rpx;
  564. transform: translateY(-50%);
  565. box-shadow: 0 0 4rpx rgba(76, 175, 80, 0.8);
  566. animation: blink 1.5s infinite;
  567. display: inline-block;
  568. }
  569. .status-dot.offline-dot {
  570. background-color: #F56C6C;
  571. box-shadow: 0 0 4rpx rgba(245, 108, 108, 0.8);
  572. }
  573. @keyframes blink {
  574. 0% {
  575. opacity: 0.4;
  576. }
  577. 50% {
  578. opacity: 1;
  579. }
  580. 100% {
  581. opacity: 0.4;
  582. }
  583. }
  584. .device-meta-row {
  585. display: flex;
  586. flex-direction: column;
  587. background-color: #F9FCFA;
  588. padding: 20rpx 24rpx;
  589. border-radius: 16rpx;
  590. border: 1rpx solid rgba(210, 237, 217, 0.8);
  591. }
  592. .device-meta-item {
  593. display: flex;
  594. align-items: center;
  595. font-size: 28rpx;
  596. margin-top: 18rpx;
  597. }
  598. .device-meta-item:first-child {
  599. margin-top: 0;
  600. }
  601. .meta-icon {
  602. width: 36rpx;
  603. height: 36rpx;
  604. display: flex;
  605. align-items: center;
  606. justify-content: center;
  607. margin-right: 12rpx;
  608. }
  609. .meta-label {
  610. color: #777777;
  611. min-width: 140rpx;
  612. font-size: 28rpx;
  613. }
  614. .meta-value {
  615. color: #333333;
  616. flex: 1;
  617. font-size: 28rpx;
  618. font-weight: 500;
  619. }
  620. /* 视频预览区域 */
  621. .video-section {
  622. margin: 0 30rpx 20rpx;
  623. position: relative;
  624. z-index: 1;
  625. }
  626. .video-container {
  627. position: relative;
  628. width: 100%;
  629. height: 420rpx; /* 16:9比例 */
  630. background-color: #000000;
  631. border-radius: 16rpx;
  632. overflow: hidden;
  633. box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.1);
  634. transition: all 0.3s ease;
  635. }
  636. .video-container.fullscreen-mode {
  637. position: fixed;
  638. top: 0;
  639. left: 0;
  640. width: 100vw;
  641. height: 100vh;
  642. margin: 0;
  643. z-index: 9999;
  644. border-radius: 0;
  645. }
  646. .video-container.fullscreen-mode .video-controls {
  647. padding: 30rpx;
  648. }
  649. .video-container.fullscreen-mode .top-controls,
  650. .video-container.fullscreen-mode .bottom-controls {
  651. opacity: 0;
  652. transition: opacity 0.3s ease;
  653. }
  654. .video-container.fullscreen-mode:hover .top-controls,
  655. .video-container.fullscreen-mode:hover .bottom-controls {
  656. opacity: 1;
  657. }
  658. .video-player, .video-placeholder {
  659. width: 100%;
  660. height: 100%;
  661. object-fit: contain;
  662. }
  663. .video-controls {
  664. position: absolute;
  665. top: 0;
  666. left: 0;
  667. width: 100%;
  668. height: 100%;
  669. display: flex;
  670. flex-direction: column;
  671. justify-content: space-between;
  672. padding: 20rpx;
  673. box-sizing: border-box;
  674. 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%);
  675. }
  676. .control-row {
  677. display: flex;
  678. justify-content: space-between;
  679. align-items: center;
  680. width: 100%;
  681. }
  682. .top-controls {
  683. height: 80rpx;
  684. }
  685. .center-controls {
  686. height: 120rpx;
  687. justify-content: center;
  688. align-items: center;
  689. }
  690. .bottom-controls {
  691. height: 80rpx;
  692. }
  693. .signal-indicator {
  694. display: flex;
  695. align-items: center;
  696. color: #FFFFFF;
  697. font-size: 24rpx;
  698. background-color: rgba(0, 0, 0, 0.5);
  699. padding: 8rpx 16rpx;
  700. border-radius: 30rpx;
  701. }
  702. .signal-indicator svg {
  703. margin-right: 8rpx;
  704. }
  705. .signal-text {
  706. font-weight: 500;
  707. }
  708. .fullscreen-button {
  709. width: 60rpx;
  710. height: 60rpx;
  711. display: flex;
  712. align-items: center;
  713. justify-content: center;
  714. color: #FFFFFF;
  715. background-color: rgba(0, 0, 0, 0.5);
  716. border-radius: 50%;
  717. transition: all 0.2s;
  718. }
  719. .fullscreen-button:active {
  720. background-color: rgba(76, 175, 80, 0.7);
  721. transform: scale(0.9);
  722. }
  723. .video-time {
  724. color: #FFFFFF;
  725. font-size: 26rpx;
  726. background-color: rgba(0, 0, 0, 0.5);
  727. padding: 6rpx 16rpx;
  728. border-radius: 30rpx;
  729. font-weight: 500;
  730. }
  731. .play-button {
  732. width: 100rpx;
  733. height: 100rpx;
  734. border-radius: 50%;
  735. background-color: rgba(255, 255, 255, 0.9);
  736. display: flex;
  737. align-items: center;
  738. justify-content: center;
  739. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.5);
  740. transition: all 0.2s;
  741. transform: scale(1);
  742. }
  743. .play-button:active {
  744. transform: scale(0.92);
  745. background-color: rgba(255, 255, 255, 1);
  746. }
  747. .center-button-container {
  748. width: 100%;
  749. height: 100%;
  750. display: flex;
  751. align-items: center;
  752. justify-content: center;
  753. }
  754. .pause-icon {
  755. width: 80rpx;
  756. height: 80rpx;
  757. border-radius: 50%;
  758. background-color: rgba(0, 0, 0, 0.5);
  759. display: flex;
  760. align-items: center;
  761. justify-content: center;
  762. opacity: 0;
  763. transition: opacity 0.3s;
  764. }
  765. .center-button-container:active .pause-icon {
  766. opacity: 1;
  767. background-color: rgba(76, 175, 80, 0.7);
  768. }
  769. /* 云台控制区域 */
  770. .ptz-section {
  771. margin: 0 30rpx 20rpx;
  772. background-color: #FFFFFF;
  773. border-radius: 20rpx;
  774. padding: 24rpx;
  775. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
  776. }
  777. .section-title {
  778. font-size: 30rpx;
  779. font-weight: 600;
  780. color: #333333;
  781. margin-bottom: 20rpx;
  782. padding: 0 10rpx;
  783. display: flex;
  784. align-items: center;
  785. }
  786. .alert-badge {
  787. background-color: #F56C6C;
  788. color: #FFFFFF;
  789. font-size: 22rpx;
  790. border-radius: 30rpx;
  791. padding: 2rpx 12rpx;
  792. margin-left: 12rpx;
  793. font-weight: normal;
  794. min-width: 32rpx;
  795. text-align: center;
  796. }
  797. .ptz-container {
  798. display: flex;
  799. flex-direction: column;
  800. align-items: center;
  801. padding: 20rpx 0;
  802. }
  803. .ptz-circle-container {
  804. position: relative;
  805. width: 300rpx;
  806. height: 300rpx;
  807. margin: 20rpx 0;
  808. }
  809. .ptz-arrow {
  810. position: absolute;
  811. width: 70rpx;
  812. height: 70rpx;
  813. display: flex;
  814. align-items: center;
  815. justify-content: center;
  816. background-color: #F0F9F0;
  817. border-radius: 50%;
  818. color: #3BB44A;
  819. font-size: 36rpx;
  820. transition: all 0.2s;
  821. box-shadow: 0 2rpx 10rpx rgba(76, 175, 80, 0.15);
  822. }
  823. .ptz-up {
  824. top: 0;
  825. left: 50%;
  826. transform: translateX(-50%);
  827. }
  828. .ptz-down {
  829. bottom: 0;
  830. left: 50%;
  831. transform: translateX(-50%);
  832. }
  833. .ptz-left {
  834. left: 0;
  835. top: 50%;
  836. transform: translateY(-50%);
  837. }
  838. .ptz-right {
  839. right: 0;
  840. top: 50%;
  841. transform: translateY(-50%);
  842. }
  843. .ptz-center {
  844. position: absolute;
  845. top: 50%;
  846. left: 50%;
  847. transform: translate(-50%, -50%);
  848. width: 90rpx;
  849. height: 90rpx;
  850. display: flex;
  851. align-items: center;
  852. justify-content: center;
  853. background-color: #F0F9F0;
  854. border-radius: 50%;
  855. color: #3BB44A;
  856. font-size: 36rpx;
  857. transition: all 0.2s;
  858. box-shadow: 0 2rpx 10rpx rgba(76, 175, 80, 0.15);
  859. }
  860. .ptz-arrow:active, .ptz-center:active {
  861. background-color: #3BB44A;
  862. transform-origin: center;
  863. }
  864. .ptz-arrow:active svg path, .ptz-center:active svg path {
  865. fill: #FFFFFF;
  866. }
  867. .ptz-up:active {
  868. transform: scale(0.92) translateX(-50%);
  869. }
  870. .ptz-down:active {
  871. transform: scale(0.92) translateX(-50%);
  872. }
  873. .ptz-left:active {
  874. transform: scale(0.92) translateY(-50%);
  875. }
  876. .ptz-right:active {
  877. transform: scale(0.92) translateY(-50%);
  878. }
  879. .ptz-center:active {
  880. transform: translate(-50%, -50%) scale(0.92);
  881. }
  882. .ptz-bottom-controls {
  883. display: flex;
  884. justify-content: space-between;
  885. align-items: center;
  886. width: 300rpx;
  887. margin-top: 30rpx;
  888. }
  889. .ptz-bottom-button {
  890. width: 80rpx;
  891. height: 80rpx;
  892. border-radius: 50%;
  893. background-color: #F0F9F0;
  894. display: flex;
  895. align-items: center;
  896. justify-content: center;
  897. color: #3BB44A;
  898. font-size: 32rpx;
  899. font-weight: 500;
  900. transition: all 0.2s;
  901. box-shadow: 0 2rpx 10rpx rgba(76, 175, 80, 0.1);
  902. }
  903. .ptz-bottom-button:active {
  904. background-color: #3BB44A;
  905. color: #FFFFFF;
  906. transform: scale(0.92);
  907. }
  908. .ptz-bottom-button:active svg path {
  909. fill: #FFFFFF;
  910. }
  911. /* 快捷功能按钮 */
  912. .quick-actions {
  913. display: flex;
  914. justify-content: space-evenly;
  915. margin: 0 30rpx 20rpx;
  916. background-color: #FFFFFF;
  917. border-radius: 20rpx;
  918. padding: 24rpx 30rpx;
  919. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
  920. }
  921. .action-button {
  922. display: flex;
  923. flex-direction: column;
  924. align-items: center;
  925. width: 160rpx;
  926. }
  927. .action-icon {
  928. width: 100rpx;
  929. height: 100rpx;
  930. border-radius: 50%;
  931. background-color: #F0F9F0;
  932. display: flex;
  933. align-items: center;
  934. justify-content: center;
  935. color: #3BB44A;
  936. margin-bottom: 12rpx;
  937. transition: all 0.2s;
  938. box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.1);
  939. }
  940. .action-icon:active {
  941. background-color: #3BB44A;
  942. transform: scale(0.92);
  943. }
  944. .action-icon:active svg path {
  945. fill: #FFFFFF;
  946. }
  947. .action-text {
  948. font-size: 24rpx;
  949. color: #666666;
  950. text-align: center;
  951. }
  952. /* 告警信息列表 */
  953. .alerts-section {
  954. margin: 0 30rpx 30rpx;
  955. background-color: #FFFFFF;
  956. border-radius: 20rpx;
  957. padding: 30rpx 24rpx;
  958. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
  959. border: 1rpx solid rgba(210, 237, 217, 0.5);
  960. }
  961. .section-title {
  962. display: flex;
  963. justify-content: space-between;
  964. align-items: center;
  965. font-size: 32rpx;
  966. font-weight: 600;
  967. color: #333333;
  968. margin-bottom: 24rpx;
  969. padding-bottom: 16rpx;
  970. border-bottom: 2rpx solid #f0f0f0;
  971. }
  972. .alert-badge {
  973. background-color: #F56C6C;
  974. color: #FFFFFF;
  975. font-size: 22rpx;
  976. border-radius: 30rpx;
  977. padding: 2rpx 12rpx;
  978. margin-left: 12rpx;
  979. font-weight: normal;
  980. min-width: 32rpx;
  981. text-align: center;
  982. }
  983. .alerts-list {
  984. display: flex;
  985. flex-direction: column;
  986. }
  987. .alert-item {
  988. display: flex;
  989. align-items: center;
  990. padding: 24rpx 20rpx;
  991. border-radius: 12rpx;
  992. margin-bottom: 16rpx;
  993. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  994. position: relative;
  995. border: 1rpx solid transparent;
  996. transition: transform 0.2s ease;
  997. }
  998. .alert-item:active {
  999. transform: scale(0.98);
  1000. }
  1001. .alert-urgent {
  1002. background-color: #FEF3F3;
  1003. border-left: 4rpx solid #F56C6C;
  1004. border-color: rgba(245, 108, 108, 0.2);
  1005. }
  1006. .alert-warning {
  1007. background-color: #FFF8E6;
  1008. border-left: 4rpx solid #E6A23C;
  1009. border-color: rgba(230, 162, 60, 0.2);
  1010. }
  1011. .alert-info {
  1012. background-color: #F2FAF5;
  1013. border-left: 4rpx solid #67C23A;
  1014. border-color: rgba(103, 194, 58, 0.2);
  1015. }
  1016. .alert-item-icon {
  1017. width: 50rpx;
  1018. height: 50rpx;
  1019. display: flex;
  1020. align-items: center;
  1021. justify-content: center;
  1022. margin-right: 16rpx;
  1023. }
  1024. .alert-item-info {
  1025. flex: 1;
  1026. display: flex;
  1027. flex-direction: column;
  1028. }
  1029. .alert-item-type {
  1030. font-size: 28rpx;
  1031. color: #333333;
  1032. font-weight: 500;
  1033. margin-bottom: 8rpx;
  1034. }
  1035. .alert-item-level {
  1036. font-size: 24rpx;
  1037. color: #999999;
  1038. }
  1039. .alert-item-time {
  1040. font-size: 24rpx;
  1041. color: #999999;
  1042. margin-left: 16rpx;
  1043. min-width: 100rpx;
  1044. text-align: right;
  1045. }
  1046. .empty-alert {
  1047. padding: 60rpx 0;
  1048. display: flex;
  1049. justify-content: center;
  1050. align-items: center;
  1051. }
  1052. .empty-text {
  1053. font-size: 28rpx;
  1054. color: #999999;
  1055. }
  1056. /* 刷新按钮样式 */
  1057. .refresh-btn {
  1058. width: 48rpx;
  1059. height: 48rpx;
  1060. background-color: rgba(76, 175, 80, 0.1);
  1061. border-radius: 50%;
  1062. display: flex;
  1063. align-items: center;
  1064. justify-content: center;
  1065. padding: 12rpx;
  1066. transition: transform 0.3s ease;
  1067. }
  1068. .refresh-btn:active {
  1069. transform: rotate(180deg);
  1070. }
  1071. .refresh-btn.refreshing {
  1072. animation: spin 1.2s linear infinite;
  1073. }
  1074. @keyframes spin {
  1075. 0% {
  1076. transform: rotate(0deg);
  1077. }
  1078. 100% {
  1079. transform: rotate(360deg);
  1080. }
  1081. }
  1082. </style>