detail-camera.vue 26 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051
  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>
  17. <view class="device-meta-row">
  18. <view class="device-meta-item">
  19. <view class="meta-icon">
  20. <image src="/static/icons/device_icon.png" mode="aspectFit" style="width: 36rpx; height: 36rpx;"></image>
  21. </view>
  22. <text class="meta-label">设备编号:</text>
  23. <text class="meta-value">{{ deviceInfo.deviceId }}</text>
  24. </view>
  25. <view class="device-meta-item">
  26. <view class="meta-icon">
  27. <image src="/static/icons/location_icon.png" mode="aspectFit" style="width: 36rpx; height: 36rpx;"></image>
  28. </view>
  29. <text class="meta-label">安装位置:</text>
  30. <text class="meta-value">{{ deviceInfo.location }}</text>
  31. </view>
  32. <view class="device-meta-item">
  33. <view class="meta-icon">
  34. <image src="/static/icons/clock_icon.png" mode="aspectFit" style="width: 36rpx; height: 36rpx;"></image>
  35. </view>
  36. <text class="meta-label">最近更新:</text>
  37. <text class="meta-value">{{ deviceInfo.lastUpdate }}</text>
  38. </view>
  39. </view>
  40. </view>
  41. <!-- 视频预览区域 -->
  42. <view class="video-section">
  43. <view class="video-container">
  44. <image v-if="!isPlaying" src="/static/images/video-placeholder.jpg" mode="aspectFill" class="video-placeholder"></image>
  45. <video
  46. v-else
  47. id="videoPlayer"
  48. :src="deviceInfo.streamUrl"
  49. class="video-player"
  50. object-fit="cover"
  51. autoplay
  52. :controls="false"
  53. :show-center-play-btn="false"
  54. :show-fullscreen-btn="false"
  55. :show-play-btn="false"
  56. :enable-progress-gesture="false"
  57. @error="handleVideoError"
  58. ></video>
  59. <!-- 视频控制层 -->
  60. <view class="video-controls">
  61. <view class="control-row top-controls">
  62. <view class="signal-indicator">
  63. <image src="/static/icons/signal_icon.png" mode="aspectFit" style="width: 16px; height: 16px;"></image>
  64. <text class="signal-text">信号良好</text>
  65. </view>
  66. <view class="fullscreen-button" @click="toggleFullscreen">
  67. <image src="/static/icons/resize_icon.png" mode="aspectFit" style="width: 20px; height: 20px;"></image>
  68. </view>
  69. </view>
  70. <view class="control-row center-controls">
  71. <view v-if="!isPlaying" class="play-button" @click="togglePlayState">
  72. <image src="/static/icons/play_icon.png" mode="aspectFit" style="width: 32px; height: 32px;"></image>
  73. </view>
  74. <view v-else class="center-button-container" @click="togglePlayState">
  75. <view class="pause-icon">
  76. <image src="/static/icons/pause_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  77. </view>
  78. </view>
  79. </view>
  80. <view class="control-row bottom-controls">
  81. <view class="video-time">{{ currentTime }}</view>
  82. </view>
  83. </view>
  84. </view>
  85. </view>
  86. <!-- 云台控制区域 -->
  87. <view class="ptz-section">
  88. <view class="section-title">云台控制</view>
  89. <view class="ptz-container">
  90. <!-- 圆形云台控制 -->
  91. <view class="ptz-circle-container">
  92. <!-- 上箭头 -->
  93. <view class="ptz-arrow ptz-up" @touchstart="controlPTZ('up', true)" @touchend="controlPTZ('up', false)">
  94. <image src="/static/icons/arrow_up_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  95. </view>
  96. <!-- 左箭头 -->
  97. <view class="ptz-arrow ptz-left" @touchstart="controlPTZ('left', true)" @touchend="controlPTZ('left', false)">
  98. <image src="/static/icons/arrow_left_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  99. </view>
  100. <!-- 中心拍照按钮 -->
  101. <view class="ptz-center" @click="takeScreenshot">
  102. <image src="/static/icons/camera_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  103. </view>
  104. <!-- 右箭头 -->
  105. <view class="ptz-arrow ptz-right" @touchstart="controlPTZ('right', true)" @touchend="controlPTZ('right', false)">
  106. <image src="/static/icons/arrow_right_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  107. </view>
  108. <!-- 下箭头 -->
  109. <view class="ptz-arrow ptz-down" @touchstart="controlPTZ('down', true)" @touchend="controlPTZ('down', false)">
  110. <image src="/static/icons/arrow_down_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  111. </view>
  112. </view>
  113. <!-- 底部操作按钮 -->
  114. <view class="ptz-bottom-controls">
  115. <view class="ptz-bottom-button" @touchstart="controlZoom('out', true)" @touchend="controlZoom('out', false)">
  116. <image src="/static/icons/zoom01_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  117. </view>
  118. <view class="ptz-bottom-button" @click="resetPTZ">
  119. <image src="/static/icons/resetPTZ_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  120. </view>
  121. <view class="ptz-bottom-button" @touchstart="controlZoom('in', true)" @touchend="controlZoom('in', false)">
  122. <image src="/static/icons/zoom02_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  123. </view>
  124. </view>
  125. </view>
  126. </view>
  127. <!-- 快捷功能按钮 -->
  128. <view class="quick-actions">
  129. <view class="action-button" @click="toggleVoiceIntercom">
  130. <view class="action-icon">
  131. <image src="/static/icons/Voice_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  132. </view>
  133. <text class="action-text">语音对讲</text>
  134. </view>
  135. <view class="action-button" @click="toggleMute">
  136. <view class="action-icon">
  137. <image v-if="isMuted" src="/static/icons/muted_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  138. <image v-else src="/static/icons/unmuted_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  139. </view>
  140. <text class="action-text">{{ isMuted ? '取消静音' : '静音' }}</text>
  141. </view>
  142. <view class="action-button" @click="navigateToHistory">
  143. <view class="action-icon">
  144. <image src="/static/icons/action_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  145. </view>
  146. <text class="action-text">视频回看</text>
  147. </view>
  148. </view>
  149. <!-- 告警信息列表 -->
  150. <view class="alerts-section">
  151. <view class="section-title">
  152. <text>告警信息</text>
  153. <view class="alert-badge" v-if="getUnhandledAlerts.length > 0">{{ getUnhandledAlerts.length }}</view>
  154. </view>
  155. <view class="alerts-list" v-if="getUnhandledAlerts.length > 0">
  156. <view
  157. v-for="(item, index) in getUnhandledAlerts"
  158. :key="index"
  159. class="alert-item"
  160. :class="{
  161. 'alert-urgent': item.level === 'high',
  162. 'alert-warning': item.level === 'medium',
  163. 'alert-info': item.level === 'low'
  164. }"
  165. >
  166. <view class="alert-item-icon">
  167. <image v-if="item.level === 'high'" src="/static/icons/warning_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  168. <image v-else-if="item.level === 'medium'" src="/static/icons/info_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  169. <image v-else src="/static/icons/success_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
  170. </view>
  171. <view class="alert-item-info">
  172. <text class="alert-item-type">{{ item.type }}</text>
  173. <text class="alert-item-level">
  174. {{ item.level === 'high' ? '紧急' : item.level === 'medium' ? '警告' : '提示' }}
  175. </text>
  176. </view>
  177. <view class="alert-item-time">{{ item.time }}</view>
  178. </view>
  179. </view>
  180. <view v-else class="empty-alert">
  181. <text class="empty-text">暂无告警信息</text>
  182. </view>
  183. </view>
  184. </view>
  185. </template>
  186. <script>
  187. export default {
  188. data() {
  189. return {
  190. deviceInfo: {
  191. deviceId: 'DEV1001',
  192. name: '监控设备-1',
  193. status: 'online',
  194. location: '西区B2地块',
  195. lastUpdate: '5分钟前',
  196. streamUrl: 'https://demo-rtsp-server-2h4n.onrender.com/stream.mp4',
  197. alertCount: 3
  198. },
  199. isPlaying: false,
  200. isMuted: false,
  201. isRecording: false,
  202. isFullscreen: false,
  203. isVoiceActive: false,
  204. isGridView: false,
  205. isZoomMode: false,
  206. currentTime: '14:30:25',
  207. // 模拟历史录像数据
  208. recordHistory: [
  209. { id: 1, startTime: '今天 12:30', duration: '00:15:30', url: '' },
  210. { id: 2, startTime: '今天 10:15', duration: '00:05:22', url: '' },
  211. { id: 3, startTime: '昨天 18:45', duration: '00:30:10', url: '' },
  212. { id: 4, startTime: '昨天 14:20', duration: '00:10:05', url: '' }
  213. ],
  214. // 模拟告警数据
  215. alertHistory: [
  216. { id: 1, time: '今天 13:05', type: '移动侦测', status: '未处理', level: 'high' },
  217. { id: 2, time: '今天 09:30', type: '信号异常', status: '未处理', level: 'medium' },
  218. { id: 3, time: '昨天 15:45', type: '设备状态', status: '已处理', level: 'low' },
  219. { id: 4, time: '昨天 10:20', type: '遮挡告警', status: '未处理', level: 'low' }
  220. ],
  221. videoContext: null
  222. }
  223. },
  224. computed: {
  225. // 获取所有未处理的告警
  226. getUnhandledAlerts() {
  227. return this.alertHistory.filter(alert => alert.status === '未处理');
  228. }
  229. },
  230. onReady() {
  231. // 获取视频实例
  232. this.videoContext = uni.createVideoContext('videoPlayer')
  233. // 设置页面标题
  234. uni.setNavigationBarTitle({
  235. title: this.deviceInfo.name
  236. })
  237. // 模拟更新时间
  238. this.startTimeUpdate()
  239. },
  240. onLoad(options) {
  241. // 如果有传入设备ID,则获取设备信息
  242. if (options && options.id) {
  243. this.fetchDeviceInfo(options.id)
  244. }
  245. },
  246. methods: {
  247. // 获取设备信息
  248. fetchDeviceInfo(deviceId) {
  249. // 这里应该是API请求,暂时用模拟数据
  250. console.log('获取设备信息:', deviceId)
  251. // 模拟异步获取数据
  252. setTimeout(() => {
  253. // 实际应该是API请求结果
  254. }, 500)
  255. },
  256. // 播放/暂停切换
  257. togglePlayState() {
  258. if (!this.isPlaying) {
  259. // 从未播放状态切换到播放状态
  260. this.isPlaying = true;
  261. // 短暂延迟确保视频元素已加载
  262. setTimeout(() => {
  263. if (this.videoContext) {
  264. this.videoContext.play();
  265. // 振动反馈
  266. uni.vibrateShort();
  267. }
  268. }, 300);
  269. } else {
  270. // 从播放状态切换到暂停状态
  271. if (this.videoContext) {
  272. this.videoContext.pause();
  273. // 在页面上显示暂停状态
  274. uni.showToast({
  275. title: '视频已暂停',
  276. icon: 'none',
  277. duration: 1500
  278. });
  279. }
  280. }
  281. },
  282. // 静音切换
  283. toggleMute() {
  284. this.isMuted = !this.isMuted
  285. if (this.videoContext) {
  286. if (this.isMuted) {
  287. this.videoContext.mute()
  288. } else {
  289. this.videoContext.unmute()
  290. }
  291. }
  292. },
  293. // 全屏切换
  294. toggleFullscreen() {
  295. if (!this.isPlaying) {
  296. // 如果视频未播放,先开始播放
  297. this.togglePlayState();
  298. // 延迟执行全屏操作,等待视频元素加载
  299. setTimeout(() => {
  300. if (this.videoContext) {
  301. this.videoContext.requestFullScreen();
  302. this.isFullscreen = true;
  303. }
  304. }, 500);
  305. } else if (this.videoContext) {
  306. if (!this.isFullscreen) {
  307. this.videoContext.requestFullScreen();
  308. } else {
  309. this.videoContext.exitFullScreen();
  310. }
  311. this.isFullscreen = !this.isFullscreen;
  312. }
  313. },
  314. // 切换九宫格视图
  315. toggleGridView() {
  316. this.isGridView = !this.isGridView
  317. uni.showToast({
  318. title: this.isGridView ? '切换到多画面模式' : '切换到单画面模式',
  319. icon: 'none'
  320. })
  321. },
  322. // 截图
  323. takeScreenshot() {
  324. // 模拟截图功能
  325. uni.showLoading({
  326. title: '截图中...'
  327. })
  328. setTimeout(() => {
  329. uni.hideLoading()
  330. uni.showToast({
  331. title: '截图已保存',
  332. icon: 'success'
  333. })
  334. }, 1000)
  335. },
  336. // 语音对讲
  337. toggleVoiceIntercom() {
  338. this.isVoiceActive = !this.isVoiceActive
  339. if (this.isVoiceActive) {
  340. uni.showToast({
  341. title: '语音对讲已开启',
  342. icon: 'none'
  343. })
  344. } else {
  345. uni.showToast({
  346. title: '语音对讲已关闭',
  347. icon: 'none'
  348. })
  349. }
  350. },
  351. // 云台控制
  352. controlPTZ(direction, isStart) {
  353. // 模拟云台控制
  354. if (isStart) {
  355. console.log(`开始控制云台: ${direction}`)
  356. uni.vibrateShort() // 短震动反馈
  357. } else {
  358. console.log(`停止控制云台: ${direction}`)
  359. }
  360. },
  361. // 云台复位
  362. resetPTZ() {
  363. console.log('云台复位')
  364. uni.vibrateShort() // 短震动反馈
  365. uni.showToast({
  366. title: '云台已复位',
  367. icon: 'none'
  368. })
  369. },
  370. // 变焦控制
  371. controlZoom(type, isStart) {
  372. // 模拟变焦控制
  373. if (isStart) {
  374. console.log(`开始控制变焦: ${type}`)
  375. uni.vibrateShort() // 短震动反馈
  376. } else {
  377. console.log(`停止控制变焦: ${type}`)
  378. }
  379. },
  380. // 切换缩放模式
  381. toggleZoom() {
  382. this.isZoomMode = !this.isZoomMode
  383. uni.showToast({
  384. title: this.isZoomMode ? '进入缩放模式' : '退出缩放模式',
  385. icon: 'none'
  386. })
  387. },
  388. // 添加预设位
  389. addPreset() {
  390. uni.showModal({
  391. title: '添加预设位',
  392. content: '是否保存当前位置为预设位?',
  393. success: (res) => {
  394. if (res.confirm) {
  395. uni.showToast({
  396. title: '预设位已保存',
  397. icon: 'success'
  398. })
  399. }
  400. }
  401. })
  402. },
  403. // 处理告警点击
  404. handleAlert(item) {
  405. uni.navigateTo({
  406. url: `/pages/alerts/alert-detail?alertId=${item.id}&deviceId=${this.deviceInfo.deviceId}`
  407. })
  408. },
  409. // 跳转到历史视频页面
  410. navigateToHistory() {
  411. uni.navigateTo({
  412. url: `/pages/video/history?deviceId=${this.deviceInfo.deviceId}`
  413. })
  414. },
  415. // 处理视频错误
  416. handleVideoError(e) {
  417. console.error('视频播放错误:', e)
  418. uni.showToast({
  419. title: '视频播放出错,请稍后再试',
  420. icon: 'none'
  421. })
  422. },
  423. // 更新时间
  424. startTimeUpdate() {
  425. // 模拟时间更新
  426. setInterval(() => {
  427. const now = new Date()
  428. const hours = String(now.getHours()).padStart(2, '0')
  429. const minutes = String(now.getMinutes()).padStart(2, '0')
  430. const seconds = String(now.getSeconds()).padStart(2, '0')
  431. this.currentTime = `${hours}:${minutes}:${seconds}`
  432. }, 1000)
  433. }
  434. }
  435. }
  436. </script>
  437. <style>
  438. /* 基础样式 */
  439. .container {
  440. display: flex;
  441. flex-direction: column;
  442. min-height: 100vh;
  443. background-color: #F8FCF9;
  444. padding-bottom: 30rpx;
  445. }
  446. /* 设备头部样式 */
  447. .device-header {
  448. background-color: #FFFFFF;
  449. border-radius: 20rpx;
  450. padding: 26rpx 30rpx 30rpx;
  451. margin: 20rpx 30rpx 20rpx;
  452. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  453. }
  454. .device-info-row {
  455. display: flex;
  456. justify-content: space-between;
  457. align-items: flex-start;
  458. margin-bottom: 24rpx;
  459. }
  460. .device-name-container {
  461. display: flex;
  462. flex-direction: column;
  463. align-items: flex-start;
  464. }
  465. .device-name {
  466. font-size: 34rpx;
  467. color: #333333;
  468. font-weight: 600;
  469. margin-bottom: 10rpx;
  470. }
  471. .status-tag {
  472. padding: 4rpx 12rpx 4rpx 24rpx;
  473. border-radius: 30rpx;
  474. font-size: 22rpx;
  475. font-weight: 500;
  476. flex-shrink: 0;
  477. position: relative;
  478. overflow: hidden;
  479. }
  480. .status-online {
  481. background-color: rgba(76, 175, 80, 0.1);
  482. color: #3BB44A;
  483. }
  484. .status-offline {
  485. background-color: rgba(245, 108, 108, 0.1);
  486. color: #F56C6C;
  487. padding-left: 24rpx;
  488. }
  489. .status-dot {
  490. position: absolute;
  491. width: 6rpx;
  492. height: 6rpx;
  493. background-color: #3BB44A;
  494. border-radius: 50%;
  495. top: 50%;
  496. left: 12rpx;
  497. transform: translateY(-50%);
  498. box-shadow: 0 0 4rpx rgba(76, 175, 80, 0.8);
  499. animation: blink 1.5s infinite;
  500. display: inline-block;
  501. }
  502. .status-dot.offline-dot {
  503. background-color: #F56C6C;
  504. box-shadow: 0 0 4rpx rgba(245, 108, 108, 0.8);
  505. }
  506. @keyframes blink {
  507. 0% {
  508. opacity: 0.4;
  509. }
  510. 50% {
  511. opacity: 1;
  512. }
  513. 100% {
  514. opacity: 0.4;
  515. }
  516. }
  517. .device-meta-row {
  518. display: flex;
  519. flex-direction: column;
  520. }
  521. .device-meta-item {
  522. display: flex;
  523. align-items: center;
  524. font-size: 26rpx;
  525. margin-top: 18rpx;
  526. }
  527. .meta-icon {
  528. width: 36rpx;
  529. height: 36rpx;
  530. display: flex;
  531. align-items: center;
  532. justify-content: center;
  533. margin-right: 8rpx;
  534. }
  535. .meta-label {
  536. color: #999999;
  537. min-width: 140rpx;
  538. font-size: 26rpx;
  539. }
  540. .meta-value {
  541. color: #333333;
  542. flex: 1;
  543. font-size: 26rpx;
  544. }
  545. /* 视频预览区域 */
  546. .video-section {
  547. margin: 0 30rpx 20rpx;
  548. }
  549. .video-container {
  550. position: relative;
  551. width: 100%;
  552. height: 420rpx; /* 16:9比例 */
  553. background-color: #000000;
  554. border-radius: 16rpx;
  555. overflow: hidden;
  556. box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.1);
  557. }
  558. .video-player, .video-placeholder {
  559. width: 100%;
  560. height: 100%;
  561. }
  562. .video-controls {
  563. position: absolute;
  564. top: 0;
  565. left: 0;
  566. width: 100%;
  567. height: 100%;
  568. display: flex;
  569. flex-direction: column;
  570. justify-content: space-between;
  571. padding: 20rpx;
  572. box-sizing: border-box;
  573. 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%);
  574. }
  575. .control-row {
  576. display: flex;
  577. justify-content: space-between;
  578. align-items: center;
  579. width: 100%;
  580. }
  581. .top-controls {
  582. height: 80rpx;
  583. }
  584. .center-controls {
  585. height: 120rpx;
  586. justify-content: center;
  587. align-items: center;
  588. }
  589. .bottom-controls {
  590. height: 80rpx;
  591. }
  592. .signal-indicator {
  593. display: flex;
  594. align-items: center;
  595. color: #FFFFFF;
  596. font-size: 24rpx;
  597. background-color: rgba(0, 0, 0, 0.5);
  598. padding: 8rpx 16rpx;
  599. border-radius: 30rpx;
  600. }
  601. .signal-indicator svg {
  602. margin-right: 8rpx;
  603. }
  604. .signal-text {
  605. font-weight: 500;
  606. }
  607. .fullscreen-button {
  608. width: 60rpx;
  609. height: 60rpx;
  610. display: flex;
  611. align-items: center;
  612. justify-content: center;
  613. color: #FFFFFF;
  614. background-color: rgba(0, 0, 0, 0.5);
  615. border-radius: 50%;
  616. transition: all 0.2s;
  617. }
  618. .fullscreen-button:active {
  619. background-color: rgba(76, 175, 80, 0.7);
  620. transform: scale(0.9);
  621. }
  622. .video-time {
  623. color: #FFFFFF;
  624. font-size: 26rpx;
  625. background-color: rgba(0, 0, 0, 0.5);
  626. padding: 6rpx 16rpx;
  627. border-radius: 30rpx;
  628. font-weight: 500;
  629. }
  630. .play-button {
  631. width: 100rpx;
  632. height: 100rpx;
  633. border-radius: 50%;
  634. background-color: rgba(255, 255, 255, 0.9);
  635. display: flex;
  636. align-items: center;
  637. justify-content: center;
  638. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.5);
  639. transition: all 0.2s;
  640. transform: scale(1);
  641. }
  642. .play-button:active {
  643. transform: scale(0.92);
  644. background-color: rgba(255, 255, 255, 1);
  645. }
  646. .center-button-container {
  647. width: 100%;
  648. height: 100%;
  649. display: flex;
  650. align-items: center;
  651. justify-content: center;
  652. }
  653. .pause-icon {
  654. width: 80rpx;
  655. height: 80rpx;
  656. border-radius: 50%;
  657. background-color: rgba(0, 0, 0, 0.5);
  658. display: flex;
  659. align-items: center;
  660. justify-content: center;
  661. opacity: 0;
  662. transition: opacity 0.3s;
  663. }
  664. .center-button-container:active .pause-icon {
  665. opacity: 1;
  666. background-color: rgba(76, 175, 80, 0.7);
  667. }
  668. /* 云台控制区域 */
  669. .ptz-section {
  670. margin: 0 30rpx 20rpx;
  671. background-color: #FFFFFF;
  672. border-radius: 20rpx;
  673. padding: 24rpx;
  674. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
  675. }
  676. .section-title {
  677. font-size: 30rpx;
  678. font-weight: 600;
  679. color: #333333;
  680. margin-bottom: 20rpx;
  681. padding: 0 10rpx;
  682. display: flex;
  683. align-items: center;
  684. }
  685. .alert-badge {
  686. background-color: #F56C6C;
  687. color: #FFFFFF;
  688. font-size: 22rpx;
  689. border-radius: 30rpx;
  690. padding: 2rpx 12rpx;
  691. margin-left: 12rpx;
  692. font-weight: normal;
  693. min-width: 32rpx;
  694. text-align: center;
  695. }
  696. .ptz-container {
  697. display: flex;
  698. flex-direction: column;
  699. align-items: center;
  700. padding: 20rpx 0;
  701. }
  702. .ptz-circle-container {
  703. position: relative;
  704. width: 300rpx;
  705. height: 300rpx;
  706. margin: 20rpx 0;
  707. }
  708. .ptz-arrow {
  709. position: absolute;
  710. width: 70rpx;
  711. height: 70rpx;
  712. display: flex;
  713. align-items: center;
  714. justify-content: center;
  715. background-color: #F0F9F0;
  716. border-radius: 50%;
  717. color: #3BB44A;
  718. font-size: 36rpx;
  719. transition: all 0.2s;
  720. box-shadow: 0 2rpx 10rpx rgba(76, 175, 80, 0.15);
  721. }
  722. .ptz-up {
  723. top: 0;
  724. left: 50%;
  725. transform: translateX(-50%);
  726. }
  727. .ptz-down {
  728. bottom: 0;
  729. left: 50%;
  730. transform: translateX(-50%);
  731. }
  732. .ptz-left {
  733. left: 0;
  734. top: 50%;
  735. transform: translateY(-50%);
  736. }
  737. .ptz-right {
  738. right: 0;
  739. top: 50%;
  740. transform: translateY(-50%);
  741. }
  742. .ptz-center {
  743. position: absolute;
  744. top: 50%;
  745. left: 50%;
  746. transform: translate(-50%, -50%);
  747. width: 90rpx;
  748. height: 90rpx;
  749. display: flex;
  750. align-items: center;
  751. justify-content: center;
  752. background-color: #F0F9F0;
  753. border-radius: 50%;
  754. color: #3BB44A;
  755. font-size: 36rpx;
  756. transition: all 0.2s;
  757. box-shadow: 0 2rpx 10rpx rgba(76, 175, 80, 0.15);
  758. }
  759. .ptz-arrow:active, .ptz-center:active {
  760. background-color: #3BB44A;
  761. transform-origin: center;
  762. }
  763. .ptz-arrow:active svg path, .ptz-center:active svg path {
  764. fill: #FFFFFF;
  765. }
  766. .ptz-up:active {
  767. transform: scale(0.92) translateX(-50%);
  768. }
  769. .ptz-down:active {
  770. transform: scale(0.92) translateX(-50%);
  771. }
  772. .ptz-left:active {
  773. transform: scale(0.92) translateY(-50%);
  774. }
  775. .ptz-right:active {
  776. transform: scale(0.92) translateY(-50%);
  777. }
  778. .ptz-center:active {
  779. transform: translate(-50%, -50%) scale(0.92);
  780. }
  781. .ptz-bottom-controls {
  782. display: flex;
  783. justify-content: space-between;
  784. align-items: center;
  785. width: 300rpx;
  786. margin-top: 30rpx;
  787. }
  788. .ptz-bottom-button {
  789. width: 80rpx;
  790. height: 80rpx;
  791. border-radius: 50%;
  792. background-color: #F0F9F0;
  793. display: flex;
  794. align-items: center;
  795. justify-content: center;
  796. color: #3BB44A;
  797. font-size: 32rpx;
  798. font-weight: 500;
  799. transition: all 0.2s;
  800. box-shadow: 0 2rpx 10rpx rgba(76, 175, 80, 0.1);
  801. }
  802. .ptz-bottom-button:active {
  803. background-color: #3BB44A;
  804. color: #FFFFFF;
  805. transform: scale(0.92);
  806. }
  807. .ptz-bottom-button:active svg path {
  808. fill: #FFFFFF;
  809. }
  810. /* 快捷功能按钮 */
  811. .quick-actions {
  812. display: flex;
  813. justify-content: space-evenly;
  814. margin: 0 30rpx 20rpx;
  815. background-color: #FFFFFF;
  816. border-radius: 20rpx;
  817. padding: 24rpx 30rpx;
  818. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
  819. }
  820. .action-button {
  821. display: flex;
  822. flex-direction: column;
  823. align-items: center;
  824. width: 160rpx;
  825. }
  826. .action-icon {
  827. width: 100rpx;
  828. height: 100rpx;
  829. border-radius: 50%;
  830. background-color: #F0F9F0;
  831. display: flex;
  832. align-items: center;
  833. justify-content: center;
  834. color: #3BB44A;
  835. margin-bottom: 12rpx;
  836. transition: all 0.2s;
  837. box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.1);
  838. }
  839. .action-icon:active {
  840. background-color: #3BB44A;
  841. transform: scale(0.92);
  842. }
  843. .action-icon:active svg path {
  844. fill: #FFFFFF;
  845. }
  846. .action-text {
  847. font-size: 24rpx;
  848. color: #666666;
  849. text-align: center;
  850. }
  851. /* 告警信息列表 */
  852. .alerts-section {
  853. margin: 0 30rpx;
  854. background-color: #FFFFFF;
  855. border-radius: 20rpx;
  856. padding: 24rpx;
  857. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
  858. }
  859. .alerts-list {
  860. display: flex;
  861. flex-direction: column;
  862. }
  863. .alert-item {
  864. display: flex;
  865. align-items: center;
  866. padding: 24rpx 20rpx;
  867. border-radius: 12rpx;
  868. margin-bottom: 16rpx;
  869. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  870. position: relative;
  871. }
  872. .alert-urgent {
  873. background-color: #FEF3F3;
  874. border-left: 4rpx solid #F56C6C;
  875. }
  876. .alert-warning {
  877. background-color: #FFF8E6;
  878. border-left: 4rpx solid #E6A23C;
  879. }
  880. .alert-info {
  881. background-color: #F2FAF5;
  882. border-left: 4rpx solid #67C23A;
  883. }
  884. .alert-item-icon {
  885. width: 50rpx;
  886. height: 50rpx;
  887. display: flex;
  888. align-items: center;
  889. justify-content: center;
  890. margin-right: 16rpx;
  891. }
  892. .alert-item-info {
  893. flex: 1;
  894. display: flex;
  895. flex-direction: column;
  896. }
  897. .alert-item-type {
  898. font-size: 28rpx;
  899. color: #333333;
  900. font-weight: 500;
  901. margin-bottom: 8rpx;
  902. }
  903. .alert-item-level {
  904. font-size: 24rpx;
  905. color: #999999;
  906. }
  907. .alert-item-time {
  908. font-size: 24rpx;
  909. color: #999999;
  910. margin-left: 16rpx;
  911. min-width: 100rpx;
  912. text-align: right;
  913. }
  914. .empty-alert {
  915. padding: 60rpx 0;
  916. display: flex;
  917. justify-content: center;
  918. align-items: center;
  919. }
  920. .empty-text {
  921. font-size: 28rpx;
  922. color: #999999;
  923. }
  924. </style>