index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. <template>
  2. <div class="c-camera-detail">
  3. <div class="l-flex--row center c-camera-detail__header">
  4. <div class="c-camera-detail__name u-ellipsis"> {{ camera.name }}</div>
  5. <i
  6. class="c-camera-detail__close el-icon-close u-pointer"
  7. @click="close"
  8. />
  9. </div>
  10. <video
  11. ref="video"
  12. class="c-camera-detail__video o-simple-video"
  13. :poster="poster"
  14. autoplay
  15. muted
  16. @play="onVideoPlay"
  17. @pause="onVideoPause"
  18. @waiting="onVideoWaiting"
  19. @playing="onVideoPlaying"
  20. @error="onVideoError"
  21. />
  22. <div class="c-camera-detail__footer has-padding">
  23. <div class="l-flex--row c-sibling-item--v c-video-controls">
  24. <div
  25. v-if="settingsShow"
  26. class="settingB"
  27. @click.stop
  28. >
  29. <template v-if="settingTab">
  30. <div
  31. class="settingT settingsub"
  32. @click="onCloseSettingTab"
  33. >
  34. <i class="el-icon-arrow-left" />{{ settingActive.label }}
  35. </div>
  36. <div class="settingHeight">
  37. <div
  38. v-for="item in videoSettings[settingActive.value]"
  39. :key="item.value"
  40. class="settingT settingsub"
  41. @click="settingClick(item)"
  42. >
  43. <i :class="{ 'el-icon-check': item.active }" />
  44. {{ item.label }}
  45. </div>
  46. </div>
  47. </template>
  48. <template v-else>
  49. <div
  50. v-for="tab in tabs"
  51. :key="tab.value"
  52. class="settingT"
  53. @click="onClickSettings(tab)"
  54. >
  55. {{ tab.label }} <i class="el-icon-arrow-right" />
  56. </div>
  57. </template>
  58. </div>
  59. <div class="l-flex__auto">
  60. <i
  61. v-if="loading"
  62. class="c-video-controls__btn el-icon-loading"
  63. />
  64. <i
  65. v-else
  66. class="c-video-controls__btn has-active u-pointer"
  67. :class="iconClass"
  68. @click="onPlayOrPause"
  69. />
  70. </div>
  71. <i
  72. class="c-video-controls__btn settings el-icon-setting has-active u-pointer"
  73. @click.stop="onSettings"
  74. />
  75. <i
  76. class="c-video-controls__btn el-icon-refresh has-active u-pointer"
  77. @click="onRefresh"
  78. />
  79. </div>
  80. <div
  81. v-if="isTraffic"
  82. class="c-sibling-item--v far c-camera-detail__traffic has-padding"
  83. >
  84. <div
  85. id="main"
  86. class="o-canvas"
  87. />
  88. <div class="c-choose-date">
  89. <div class="c-sibling-item c-choose-date__type u-pointer">
  90. <span
  91. class="c-choose-date__item"
  92. :class="{ active: active === 'hour' }"
  93. @click="onTimeTypeChanged('hour')"
  94. >
  95. 1小时
  96. </span>
  97. <span
  98. class="c-choose-date__item"
  99. :class="{ active: active === 'day' }"
  100. @click="onTimeTypeChanged('day')"
  101. >
  102. 1天
  103. </span>
  104. </div>
  105. <el-date-picker
  106. v-model="dateValue"
  107. class="c-sibling-item far c-choose-date__date u-pointer"
  108. type="date"
  109. value-format="yyyy-MM-dd"
  110. :editable="false"
  111. :clearable="false"
  112. @change="getStatistic"
  113. />
  114. <el-time-select
  115. v-model="timeValue"
  116. class="c-sibling-item c-choose-date__time u-pointer"
  117. :picker-options="timePickerOptions"
  118. :editable="false"
  119. :clearable="false"
  120. @change="getStatistic"
  121. />
  122. </div>
  123. </div>
  124. </div>
  125. </div>
  126. </template>
  127. <script>
  128. import { mapGetters } from 'vuex'
  129. import * as echarts from 'echarts'
  130. import {
  131. getStatistic,
  132. getVideoinfo,
  133. getAvailableParam,
  134. setCamera
  135. } from '@/api/camera'
  136. import {
  137. GATEWAY_CAMERA,
  138. Camera
  139. } from '@/constant'
  140. import { parseTime } from '@/utils'
  141. import playerMixin from '../../player'
  142. export default {
  143. name: 'CameraDetail',
  144. mixins: [playerMixin],
  145. props: {
  146. camera: {
  147. type: Object,
  148. required: true
  149. }
  150. },
  151. data () {
  152. const now = new Date()
  153. return {
  154. active: 'hour', // hour是小时,day是天
  155. settingActive: 'items',
  156. tabs: [
  157. { value: 'items', label: '分辨率' },
  158. { value: 'fps', label: '帧率' },
  159. { value: 'bitRates', label: '码率' }
  160. ],
  161. settingTab: true,
  162. settingsShow: false,
  163. videoSettings: null,
  164. infoData: null,
  165. dateValue: parseTime(now, '{y}-{m}-{d}'),
  166. timeValue: parseTime(now, '{h}:00'),
  167. timePickerOptions: {
  168. start: '00:00',
  169. step: '01:00',
  170. end: '23:00'
  171. }
  172. }
  173. },
  174. computed: {
  175. ...mapGetters(['token']),
  176. isTraffic () {
  177. return this.camera.cameraType === Camera.TRAFFIC
  178. },
  179. iconClass () {
  180. return this.paused || this.needReset ? 'el-icon-video-play' : 'el-icon-video-pause'
  181. }
  182. },
  183. created () {
  184. this.getAvailableParam()
  185. },
  186. mounted () {
  187. this.createPlayer()
  188. if (this.isTraffic) {
  189. this.getStatistic()
  190. window.addEventListener('resize', this.onResize)
  191. }
  192. },
  193. beforeDestroy () {
  194. if (this.isTraffic) {
  195. window.removeEventListener('resize', this.onResize)
  196. }
  197. },
  198. methods: {
  199. close () {
  200. this.destroyPlayer()
  201. this.$emit('close')
  202. },
  203. createPlayer () {
  204. this.playUrl(`${GATEWAY_CAMERA}/${this.camera.identifier}?authorization=${this.token}`)
  205. },
  206. setCamera () {
  207. this.hideSettingsMenu()
  208. setCamera({
  209. deviceId: this.camera.identifier,
  210. ...this.infoData
  211. }).then(
  212. () => {
  213. this.dataInit()
  214. this.destroyPlayer()
  215. this.createPlayer()
  216. },
  217. () => {
  218. this.infoData = { ...this.$infoData }
  219. }
  220. )
  221. },
  222. getAvailableParam () {
  223. if (this.$videoPramsLoading) {
  224. this.$message({
  225. type: 'warning',
  226. message: '配置获取中,请稍后重试'
  227. })
  228. return
  229. }
  230. this.$videoPramsLoading = true
  231. getAvailableParam().then(({ data }) => {
  232. this.videoSettings = {
  233. items: data.itemList.map(item => {
  234. // maxBitRateOptions: 20480
  235. // minBitRateOptions: 3
  236. // resolutionFpsMax: 20
  237. // snHight: 1944
  238. // snWidth: 2592
  239. return {
  240. value: `${item.snWidth}*${item.snHight}`,
  241. label: `${item.snWidth}*${item.snHight}`,
  242. active: false,
  243. ...item
  244. }
  245. }),
  246. fps: [],
  247. bitRates: [],
  248. streamRateType: data.streamRateTypeList
  249. }
  250. this.getVideoinfo()
  251. }).finally(() => {
  252. this.$videoPramsLoading = false
  253. })
  254. },
  255. onSettings () {
  256. if (!this.videoSettings) {
  257. this.getAvailableParam()
  258. return
  259. }
  260. if (!this.infoData) {
  261. this.getVideoinfo()
  262. return
  263. }
  264. this.settingTab = false
  265. if (this.settingsShow) {
  266. this.hideSettingsMenu()
  267. } else {
  268. document.addEventListener('click', this.hideSettingsMenu)
  269. this.settingsShow = true
  270. }
  271. },
  272. hideSettingsMenu () {
  273. if (this.settingsShow) {
  274. this.settingsShow = false
  275. document.addEventListener('click', this.hideSettingsMenu)
  276. }
  277. },
  278. onRefresh () {
  279. this.destroyPlayer()
  280. this.createPlayer()
  281. },
  282. onClickSettings (tab) {
  283. this.settingActive = tab
  284. this.settingTab = true
  285. },
  286. onCloseSettingTab () {
  287. this.settingTab = false
  288. },
  289. getVideoinfo () {
  290. if (this.$videoInfoLoading) {
  291. this.$message({
  292. type: 'warning',
  293. message: '配置获取中,请稍后重试'
  294. })
  295. return
  296. }
  297. this.$videoInfoLoading = true
  298. getVideoinfo(this.camera.identifier).then(({ data }) => {
  299. const { width, hight, frameRate, bitRate } = data
  300. this.infoData = { width, hight, frameRate, bitRate }
  301. this.dataInit()
  302. }).finally(() => {
  303. this.$videoInfoLoading = false
  304. })
  305. },
  306. dataInit () {
  307. const { width, hight, frameRate, bitRate } = this.infoData
  308. const value = `${width}*${hight}`
  309. let target = null
  310. this.videoSettings.items.forEach(item => {
  311. if (item.value === value) {
  312. item.active = true
  313. target = item
  314. } else {
  315. item.active = false
  316. }
  317. })
  318. if (target) {
  319. const fps = []
  320. const max = target.resolutionFpsMax
  321. for (let i = 1; i <= max; i++) {
  322. fps.push({ value: i, label: `${i}fps`, active: i === frameRate })
  323. }
  324. this.videoSettings.fps = fps
  325. const bitRates = []
  326. const minBitRate = target.minBitRateOptions
  327. const maxBitRate = target.maxBitRateOptions
  328. const streamRateType = this.videoSettings.streamRateType
  329. for (let i = 0; i < streamRateType.length; i++) {
  330. const rateType = streamRateType[i]
  331. if (rateType >= minBitRate && rateType <= maxBitRate) {
  332. bitRates.push({ value: rateType, label: `${rateType}kb/s`, active: rateType === bitRate })
  333. }
  334. }
  335. this.videoSettings.bitRates = bitRates
  336. } else {
  337. this.videoSettings.items = [
  338. { label: `${width}*${hight}`, active: true }
  339. ]
  340. this.videoSettings.fps = [
  341. { value: frameRate, label: `${frameRate}fps`, active: true }
  342. ]
  343. this.videoSettings.bitRates = [
  344. { value: bitRate, label: `${bitRate}kb/s`, active: true }
  345. ]
  346. }
  347. },
  348. settingClick (item) {
  349. let value = null
  350. switch (this.settingActive.value) {
  351. case 'items':
  352. value = `${this.infoData.width}*${this.infoData.hight}`
  353. break
  354. case 'fps':
  355. value = this.infoData.frameRate
  356. break
  357. case 'bitRates':
  358. value = this.infoData.bitRate
  359. break
  360. default:
  361. return
  362. }
  363. if (value === item.value) {
  364. return
  365. }
  366. this.$infoData = { ...this.infoData }
  367. switch (this.settingActive.value) {
  368. case 'items':
  369. this.infoData.width = item.snWidth
  370. this.infoData.hight = item.snHight
  371. this.infoData.frameRate = Math.min(item.resolutionFpsMax, this.infoData.frameRate)
  372. this.infoData.bitRate = Math.max(item.minBitRateOptions, Math.min(item.maxBitRateOptions, this.infoData.bitRate))
  373. break
  374. case 'fps':
  375. this.infoData.frameRate = item.value
  376. break
  377. case 'bitRates':
  378. this.infoData.bitRate = item.value
  379. break
  380. default:
  381. break
  382. }
  383. this.setCamera()
  384. },
  385. onTimeTypeChanged (type) {
  386. if (this.active === type) {
  387. return
  388. }
  389. this.active = type
  390. this.getStatistic()
  391. },
  392. getStatistic () {
  393. const endTime = `${this.dateValue} ${this.timeValue}:00`
  394. const date = new Date(endTime)
  395. switch (this.active) {
  396. case 'hour':
  397. date.setHours(date.getHours() - 1)
  398. break
  399. default:
  400. date.setDate(date.getDate() - 1)
  401. break
  402. }
  403. const startTime = parseTime(date, '{y}-{m}-{d} {h}:{i}:{s}')
  404. this.refreshEchart([])
  405. getStatistic({
  406. deviceId: this.camera.identifier,
  407. startTime,
  408. endTime,
  409. pageIndex: 1,
  410. pageSize: 10000
  411. }).then(({ data }) => {
  412. this.refreshEchart(data)
  413. })
  414. },
  415. getEchartData (data) {
  416. const date = new Date(`${this.dateValue} ${this.timeValue}:00`)
  417. const xdata = []
  418. const ydata = []
  419. if (this.active === 'hour') {
  420. date.setHours(date.getHours() - 1)
  421. for (let i = 0; i <= 60; i++) {
  422. const value = parseTime(date, '{y}-{m}-{d} {h}:{i}')
  423. xdata.push(value.split(' ')[1])
  424. ydata.push(data[value] || 0)
  425. date.setMinutes(date.getMinutes() + 1)
  426. }
  427. } else {
  428. date.setDate(date.getDate() - 1)
  429. for (let i = 0; i <= 24; i++) {
  430. const value = parseTime(date, '{y}-{m}-{d} {h}')
  431. xdata.push(`${value.split(' ')[1]}:00`)
  432. ydata.push(data[value] || 0)
  433. date.setHours(date.getHours() + 1)
  434. }
  435. }
  436. return { xdata, ydata }
  437. },
  438. transformEchartData (data) {
  439. const ydata = {}
  440. const isHour = this.active === 'hour'
  441. data.forEach(({ eventTime, insidePeopleNum }) => {
  442. const key = eventTime.slice(0, isHour ? 16 : 13)
  443. if (ydata[key]) {
  444. ydata[key] = Math.max(ydata[key], insidePeopleNum)
  445. } else {
  446. ydata[key] = insidePeopleNum
  447. }
  448. })
  449. return this.getEchartData(ydata)
  450. },
  451. onResize () {
  452. this.$echarts?.resize()
  453. },
  454. refreshEchart (echartsData) {
  455. const { xdata, ydata } = this.transformEchartData(echartsData)
  456. if (!this.$echarts) {
  457. this.$echarts = echarts.init(document.getElementById('main'))
  458. }
  459. this.$echarts?.setOption({
  460. title: {
  461. text: '区域内人数',
  462. textStyle: {
  463. color: '#fff',
  464. fontWeight: 'bold'
  465. }
  466. },
  467. xAxis: {
  468. type: 'category',
  469. data: xdata,
  470. axisLine: {
  471. lineStyle: {
  472. color: '#4779BC'
  473. }
  474. },
  475. axisLabel: {
  476. color: '#A9CEFF'
  477. }
  478. },
  479. yAxis: {
  480. type: 'value',
  481. minInterval: 1,
  482. splitLine: {
  483. lineStyle: {
  484. color: '#4779BC',
  485. type: 'dashed'
  486. }
  487. },
  488. axisLine: {
  489. lineStyle: {
  490. color: '#4779BC'
  491. }
  492. },
  493. axisLabel: {
  494. color: '#A9CEFF'
  495. }
  496. },
  497. grid: {
  498. left: '30',
  499. right: '20',
  500. top: '40',
  501. bottom: '20'
  502. },
  503. series: [
  504. {
  505. data: ydata,
  506. type: 'bar',
  507. showBackground: true,
  508. backgroundStyle: {
  509. color: 'transparent'
  510. },
  511. itemStyle: {
  512. color: 'rgba(0, 191, 208, 0.5)'
  513. },
  514. select: {
  515. itemStyle: {
  516. color: 'rgb(0, 234, 255)'
  517. }
  518. }
  519. }
  520. ],
  521. tooltip: {
  522. formatter: '时间:{b}<br />人流量:{c}'
  523. }
  524. })
  525. }
  526. }
  527. }
  528. </script>
  529. <style lang="scss" scoped>
  530. $theme-blue: #003e90;
  531. .c-camera-detail {
  532. position: relative;
  533. width: 100%;
  534. height: 100%;
  535. overflow: hidden;
  536. &__video {
  537. width: 100%;
  538. height: 100%;
  539. }
  540. &__header {
  541. position: absolute;
  542. top: 0;
  543. left: 0;
  544. width: 100%;
  545. height: 42px;
  546. color: #fff;
  547. font-size: 24px;
  548. text-align: center;
  549. background-color: rgba($theme-blue, 0.8);
  550. z-index: 9;
  551. &::after {
  552. content: "";
  553. position: absolute;
  554. top: 100%;
  555. left: 50%;
  556. width: 700px;
  557. height: 0;
  558. border-top: 16px solid rgba($theme-blue, 0.8);
  559. border-right: 16px solid transparent;
  560. border-bottom: 16px solid transparent;
  561. border-left: 16px solid transparent;
  562. transform: translateX(-50%);
  563. }
  564. }
  565. &__name {
  566. display: inline-block;
  567. width: 600px;
  568. }
  569. &__close {
  570. position: absolute;
  571. top: 50%;
  572. right: 0;
  573. padding: $spacing;
  574. color: #fff;
  575. font-size: 32px;
  576. transform: translateY(-50%);
  577. }
  578. &__footer {
  579. position: absolute;
  580. left: 0;
  581. bottom: 0;
  582. width: 100%;
  583. }
  584. &__traffic {
  585. position: relative;
  586. line-height: 1;
  587. border-radius: $radius--mini;
  588. background-color: rgba($theme-blue, 0.8);
  589. }
  590. }
  591. .c-video-controls {
  592. position: relative;
  593. height: 48px;
  594. padding: 0 32px 0 16px;
  595. border-radius: $radius--mini;
  596. background-color: rgba(#00060d, 0.75);
  597. &__btn {
  598. color: #fff;
  599. font-size: 32px;
  600. &.settings {
  601. margin-right: 32px;
  602. }
  603. }
  604. }
  605. .settingB {
  606. position: absolute;
  607. right: 134px;
  608. bottom: 48px;
  609. padding: 10px 0px;
  610. color: #fff;
  611. background-color: rgba(#000, 0.85);
  612. transform: translateX(50%);
  613. .settingT {
  614. position: relative;
  615. padding: 0px 16px;
  616. width: 180px;
  617. height: 40px;
  618. line-height: 40px;
  619. cursor: pointer;
  620. &:hover {
  621. background-color: rgba(#454545, 0.85);
  622. }
  623. i {
  624. position: absolute;
  625. top: 13px;
  626. right: 16px;
  627. }
  628. }
  629. .settingHeight {
  630. max-height: 250px;
  631. overflow: auto;
  632. }
  633. .settingsub {
  634. width: 140px;
  635. padding-left: 42px;
  636. i {
  637. position: absolute;
  638. left: 16px;
  639. }
  640. }
  641. }
  642. .c-choose-date {
  643. position: absolute;
  644. right: $spacing;
  645. top: $spacing;
  646. color: #fff;
  647. &__type {
  648. display: inline-block;
  649. height: 24px;
  650. line-height: 24px;
  651. border-radius: $radius--mini;
  652. background-color: #4478bc;
  653. }
  654. &__item {
  655. display: inline-block;
  656. width: 50px;
  657. font-size: 14px;
  658. text-align: center;
  659. border-radius: $radius--mini;
  660. background-color: #4478bc;
  661. &.active {
  662. background: #0096ff;
  663. }
  664. }
  665. &__date {
  666. width: 130px;
  667. }
  668. &__time {
  669. width: 80px;
  670. }
  671. ::v-deep input {
  672. height: 24px;
  673. padding-right: 10px;
  674. color: #fff;
  675. font-size: 14px;
  676. line-height: 24px;
  677. background-color: transparent;
  678. }
  679. ::v-deep .el-input__icon {
  680. line-height: 24px;
  681. }
  682. }
  683. .o-canvas {
  684. width: 100%;
  685. height: 200px;
  686. }
  687. </style>