detail-camera.vue 29 KB

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