detail-camera.vue 28 KB

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