|
|
@@ -0,0 +1,531 @@
|
|
|
+<template>
|
|
|
+ <div class="cam-detail-container">
|
|
|
+ <el-row :gutter="16">
|
|
|
+ <el-col :md="17" :sm="24">
|
|
|
+ <el-card class="video-main-card" shadow="never">
|
|
|
+ <div slot="header" class="card-header">
|
|
|
+ <div class="title-info">
|
|
|
+ <i class="el-icon-video-camera"></i>
|
|
|
+ <span class="cam-name">{{ camDetail ? camDetail.camName : '摄像头监控' }}</span>
|
|
|
+ <el-tag v-if="camDetail" :type="camDetail.onLive ? 'danger' : 'info'" size="mini" effect="dark" class="live-tag">
|
|
|
+ {{ camDetail.onLive ? '• LIVE 直播' : '回放中' }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="header-actions">
|
|
|
+ <el-button icon="el-icon-share" type="primary" size="mini" round @click="onShareCam">分享</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="video-content-wrapper">
|
|
|
+ <div class="video-container">
|
|
|
+ <video
|
|
|
+ id="videoElement"
|
|
|
+ controls
|
|
|
+ autoplay
|
|
|
+ muted
|
|
|
+ playsinline
|
|
|
+ webkit-playsinline
|
|
|
+ class="video-element"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <el-col :md="7" :sm="24">
|
|
|
+ <el-card class="record-sidebar-card" shadow="never">
|
|
|
+ <div slot="header" class="sidebar-header">
|
|
|
+ <div class="sidebar-title">
|
|
|
+ <i class="el-icon-collection"></i>
|
|
|
+ <span>历史录像</span>
|
|
|
+ </div>
|
|
|
+ <el-button type="primary" icon="el-icon-date" size="mini" plain @click="onSelectDate">切换日期</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="record-list-info">
|
|
|
+ <div class="date-display">
|
|
|
+ <i class="el-icon-time"></i>
|
|
|
+ <span>{{ getYearMonthDay(calendarDate) }}</span>
|
|
|
+ <small>的监控档案</small>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-table
|
|
|
+ :data="dataList"
|
|
|
+ size="small"
|
|
|
+ height="460px"
|
|
|
+ class="custom-table"
|
|
|
+ :header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
|
|
|
+ >
|
|
|
+ <el-table-column prop="startTime" label="开始时间" width="90">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <span class="time-text">{{ scope.row.startTime }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="duration" label="时长" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-tag size="mini" type="info" plain>{{ scope.row.duration }}s</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="80" align="right">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ circle
|
|
|
+ size="mini"
|
|
|
+ icon="el-icon-caret-right"
|
|
|
+ @click="handlePlay(scope.row.recordId)"
|
|
|
+ ></el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <div class="sidebar-footer">
|
|
|
+ <el-button type="warning" icon="el-icon-magic-stick" size="medium" class="full-width-btn" @click="onButtonSubmit">提交</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-dialog
|
|
|
+ title="选择监控日期"
|
|
|
+ append-to-body
|
|
|
+ :visible.sync="showCalenderDialog"
|
|
|
+ width="600px"
|
|
|
+ custom-class="custom-calendar-dialog"
|
|
|
+ >
|
|
|
+ <div class="calendar-legend">
|
|
|
+ <span class="legend-item"><i class="dot active"></i> 有录像</span>
|
|
|
+ <span class="legend-item"><i class="dot"></i> 无数据</span>
|
|
|
+ </div>
|
|
|
+ <el-calendar v-model="calendarDate">
|
|
|
+ <div slot="dateCell" slot-scope="{ date, data }" class="custom-date-cell" @click="handleCellClick(date, data)">
|
|
|
+ <span :class="{ 'has-record': dealMyDate(data.day) }">
|
|
|
+ {{ data.day.split("-").slice(2).join() }}
|
|
|
+ </span>
|
|
|
+ <div v-if="dealMyDate(data.day)" class="record-dot"></div>
|
|
|
+ </div>
|
|
|
+ </el-calendar>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <el-dialog
|
|
|
+ title="分享摄像头权限"
|
|
|
+ :visible.sync="showCreateShareDialog"
|
|
|
+ width="420px"
|
|
|
+ center
|
|
|
+ >
|
|
|
+ <div class="share-dialog-body">
|
|
|
+ <el-form ref="createAlbumForm" :model="createShareForm" label-position="top">
|
|
|
+ <el-form-item label="授权给联系人">
|
|
|
+ <div class="user-check-list">
|
|
|
+ <el-checkbox-group v-model="createShareForm.shareToList">
|
|
|
+ <el-checkbox v-for="user in userContactList" :key="user.userIdStr" :label="user.username" border size="small" class="user-checkbox" />
|
|
|
+ </el-checkbox-group>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div slot="footer" class="dialog-footer">
|
|
|
+ <el-button type="primary" block @click="createShare" style="width: 100%">确认分享</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import flvjs from 'flv.js'
|
|
|
+
|
|
|
+import { createShare, getCamDetail, getCamList, getRecordByMonth, getRecordUrl, submitActivity } from '@/api/disk'
|
|
|
+import { getUserContact } from '@/api/user'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'CamDetail',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ calendarDate: new Date(),
|
|
|
+ dateMap: new Map(),
|
|
|
+ query: {
|
|
|
+ camId: '',
|
|
|
+ yearMonth: '',
|
|
|
+ yearMonthDay: ''
|
|
|
+ },
|
|
|
+ dataList: [],
|
|
|
+ showCalenderDialog: false,
|
|
|
+ camDetail: null,
|
|
|
+ camRecordDetail: null,
|
|
|
+ // ****************************************************************************************************************
|
|
|
+ showCreateShareDialog: false,
|
|
|
+ userContactList: [],
|
|
|
+ createShareForm: {
|
|
|
+ albumType: 1,
|
|
|
+ albumId: null,
|
|
|
+ shareToList: []
|
|
|
+ },
|
|
|
+ flvPlayer: null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ $route() {
|
|
|
+ this.$router.go()
|
|
|
+ },
|
|
|
+ calendarDate: {
|
|
|
+ handler(newValue, oldValue) {
|
|
|
+ const oldMonth = this.getYearMonth(oldValue)
|
|
|
+ const newMonth = this.getYearMonth(newValue)
|
|
|
+ if (oldMonth !== newMonth) {
|
|
|
+ this.query.yearMonth = this.getYearMonth(newValue)
|
|
|
+ getRecordByMonth(this.query).then(resp => {
|
|
|
+ if (resp.code === 0) {
|
|
|
+ this.dateMap.clear()
|
|
|
+ for (const item of resp.data) {
|
|
|
+ const date1 = new Date(item)
|
|
|
+ const dayStr = this.getYearMonthDay(date1)
|
|
|
+ this.dateMap.set(dayStr, date1)
|
|
|
+ }
|
|
|
+ // 对 calendarDate 赋值重新渲染日历, 目的是调用 dealMyDate 处理当月的日期
|
|
|
+ this.calendarDate = new Date(newMonth)
|
|
|
+ }
|
|
|
+ }).catch(error => {
|
|
|
+ this.$message.error(error.message)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ document.title = '摄像头监控'
|
|
|
+ this.query.camId = this.$route.query.camId
|
|
|
+ if (this.query.camId) {
|
|
|
+ this.getData()
|
|
|
+ } else {
|
|
|
+ this.$message.error('参数错误')
|
|
|
+ this.goBack()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ getData() {
|
|
|
+ getCamDetail(this.query).then(resp => {
|
|
|
+ if (resp.code === 0) {
|
|
|
+ this.camDetail = resp.data
|
|
|
+ if (this.camDetail.onLive) {
|
|
|
+ this.initVideoPlayer(this.camDetail.liveUrl, true)
|
|
|
+ } else {
|
|
|
+ this.initVideoPlayer(this.camDetail.url, false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ goBack() {
|
|
|
+ this.$router.go(-1)
|
|
|
+ },
|
|
|
+ handleCellClick(date, data) {
|
|
|
+ this.showCalenderDialog = false
|
|
|
+ this.query.yearMonthDay = this.getYearMonthDay(date)
|
|
|
+ getCamDetail(this.query).then(resp => {
|
|
|
+ if (resp.code === 0) {
|
|
|
+ const respData = resp.data
|
|
|
+ this.camDetail = respData
|
|
|
+ this.dataList = respData.dayRecords
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 渲染日历时会处理每个日期
|
|
|
+ dealMyDate(val) {
|
|
|
+ return this.dateMap.get(val) !== undefined
|
|
|
+ },
|
|
|
+ getYearMonth(date) {
|
|
|
+ const year = date.getFullYear().toString().padStart(4, '0')
|
|
|
+ const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
|
|
+ // const day = date.getDate().toString().padStart(2, '0')
|
|
|
+ // const hour = date.getHours().toString().padStart(2, '0')
|
|
|
+ // const minute = date.getMinutes().toString().padStart(2, '0')
|
|
|
+ // const second = date.getSeconds().toString().padStart(2, '0')
|
|
|
+ // 2023-02-16 08:25:05
|
|
|
+ // console.log(`${year}-${month}-${day} ${hour}:${minute}:${second}`)
|
|
|
+ return year + '-' + month
|
|
|
+ },
|
|
|
+ getYearMonthDay(date) {
|
|
|
+ const year = date.getFullYear().toString().padStart(4, '0')
|
|
|
+ const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
|
|
+ const day = date.getDate().toString().padStart(2, '0')
|
|
|
+ return year + '-' + month + '-' + day
|
|
|
+ },
|
|
|
+ handlePlay(recordId) {
|
|
|
+ getRecordUrl(recordId).then(resp => {
|
|
|
+ if (resp.code === 0) {
|
|
|
+ this.camRecordDetail = resp.data
|
|
|
+ this.initVideoPlayer(this.camRecordDetail.url, false)
|
|
|
+ } else {
|
|
|
+ this.$message.error(resp.msg)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ initVideoPlayer(videoUrl, live) {
|
|
|
+ var videoElement = document.getElementById('videoElement')
|
|
|
+ if (!live) {
|
|
|
+ videoElement.src = videoUrl
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!flvjs.isSupported()) {
|
|
|
+ this.$message.error('not support flv')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.flvPlayer = flvjs.createPlayer({
|
|
|
+ type: 'flv',
|
|
|
+ isLive: true,
|
|
|
+ url: videoUrl,
|
|
|
+ duration: 0,
|
|
|
+ filesize: 0,
|
|
|
+ enableStashBuffer: false,
|
|
|
+ hasAudio: true,
|
|
|
+ hasVideo: true
|
|
|
+ })
|
|
|
+ this.flvPlayer.attachMediaElement(videoElement)
|
|
|
+ this.flvPlayer.load()
|
|
|
+ this.flvPlayer.play()
|
|
|
+
|
|
|
+ videoElement.addEventListener('progress', () => {
|
|
|
+ const end = this.flvPlayer.buffered.end(0) // 获取当前buffered值(缓冲区末尾)
|
|
|
+ const delta = end - this.flvPlayer.currentTime // 获取buffered与当前播放位置的差值
|
|
|
+ // 延迟过大,通过跳帧的方式更新视频
|
|
|
+ if (delta > 10 || delta < 0) {
|
|
|
+ this.flvPlayer.currentTime = this.flvPlayer.buffered.end(0) - 1
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 追帧
|
|
|
+ if (delta > 1) {
|
|
|
+ videoElement.playbackRate = 1.1
|
|
|
+ } else {
|
|
|
+ videoElement.playbackRate = 1
|
|
|
+ }
|
|
|
+ })
|
|
|
+ // 点击播放按钮后,更新视频
|
|
|
+ videoElement.addEventListener('play', () => {
|
|
|
+ if (this.flvPlayer.buffered.length > 0) {
|
|
|
+ const end = this.flvPlayer.buffered.end(0) - 1
|
|
|
+ this.flvPlayer.currentTime = end
|
|
|
+ }
|
|
|
+ })
|
|
|
+ // 网页重新激活后,更新视频
|
|
|
+ window.onfocus = () => {
|
|
|
+ const end = this.flvPlayer.buffered.end(0) - 1
|
|
|
+ this.flvPlayer.currentTime = end
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onSelectDate() {
|
|
|
+ this.showCalenderDialog = true
|
|
|
+ },
|
|
|
+ onButtonSubmit() {
|
|
|
+ this.$confirm('确认提交?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(() => {
|
|
|
+ submitActivity().then(resp => {
|
|
|
+ this.$message.info(resp.msg)
|
|
|
+ }).catch(error => {
|
|
|
+ this.$message.error(error.message)
|
|
|
+ })
|
|
|
+ }).catch(() => {
|
|
|
+ this.$message({
|
|
|
+ type: 'info',
|
|
|
+ message: '已取消'
|
|
|
+ })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ onShareCam() {
|
|
|
+ this.createShareForm.albumId = this.camDetail.camId
|
|
|
+ getUserContact(1).then(resp => {
|
|
|
+ if (resp.code === 0) {
|
|
|
+ this.userContactList = resp.data
|
|
|
+ this.showCreateShareDialog = true
|
|
|
+ } else {
|
|
|
+ this.$message.error(resp.msg)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ createShare() {
|
|
|
+ createShare(this.createShareForm).then(resp => {
|
|
|
+ this.$message.info(resp.msg)
|
|
|
+ }).finally(() => {
|
|
|
+ this.showCreateShareDialog = false
|
|
|
+ this.createShareForm = {
|
|
|
+ albumType: 1,
|
|
|
+ albumId: null,
|
|
|
+ shareToList: []
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.cam-detail-container {
|
|
|
+ padding: 16px;
|
|
|
+ background-color: #f0f2f5;
|
|
|
+ min-height: 100vh;
|
|
|
+}
|
|
|
+
|
|
|
+/* 播放器卡片美化 */
|
|
|
+.video-main-card {
|
|
|
+ border-radius: 8px;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.title-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.title-info i {
|
|
|
+ font-size: 20px;
|
|
|
+ color: #409eff;
|
|
|
+ margin-right: 8px;
|
|
|
+}
|
|
|
+.cam-name {
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 16px;
|
|
|
+ margin-right: 12px;
|
|
|
+}
|
|
|
+.live-tag {
|
|
|
+ letter-spacing: 1px;
|
|
|
+}
|
|
|
+
|
|
|
+.video-content-wrapper {
|
|
|
+ background-color: #000;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.video-container {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ padding-top: 56.25%; /* 16:9 Aspect Ratio */
|
|
|
+}
|
|
|
+.video-element {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
+}
|
|
|
+
|
|
|
+/* 侧边栏美化 */
|
|
|
+.record-sidebar-card {
|
|
|
+ border-radius: 8px;
|
|
|
+ border: none;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+.sidebar-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.sidebar-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+.sidebar-title i {
|
|
|
+ margin-right: 6px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+.date-display {
|
|
|
+ padding: 12px;
|
|
|
+ background: #fdf6ec;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ color: #e6a23c;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+.date-display small {
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+.time-text {
|
|
|
+ font-family: 'Monaco', 'Courier New', monospace;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+.sidebar-footer {
|
|
|
+ margin-top: 16px;
|
|
|
+}
|
|
|
+.full-width-btn {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+/* 日历组件美化 */
|
|
|
+.custom-calendar-dialog >>> .el-dialog__body {
|
|
|
+ padding: 0 20px 20px 20px;
|
|
|
+}
|
|
|
+.calendar-legend {
|
|
|
+ text-align: right;
|
|
|
+ font-size: 12px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+.legend-item {
|
|
|
+ margin-left: 15px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+.dot {
|
|
|
+ display: inline-block;
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #dcdfe6;
|
|
|
+}
|
|
|
+.dot.active {
|
|
|
+ background: #f56c6c;
|
|
|
+}
|
|
|
+
|
|
|
+.custom-date-cell {
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+.has-record {
|
|
|
+ color: #f56c6c;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+.record-dot {
|
|
|
+ width: 4px;
|
|
|
+ height: 4px;
|
|
|
+ background: #f56c6c;
|
|
|
+ border-radius: 50%;
|
|
|
+ margin-top: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 分享列表美化 */
|
|
|
+.user-check-list {
|
|
|
+ max-height: 200px;
|
|
|
+ overflow-y: auto;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+.user-checkbox {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ margin-left: 0 !important;
|
|
|
+ margin-right: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表格滚动条美化 */
|
|
|
+.custom-table >>> .el-table__body-wrapper::-webkit-scrollbar {
|
|
|
+ width: 6px;
|
|
|
+}
|
|
|
+.custom-table >>> .el-table__body-wrapper::-webkit-scrollbar-thumb {
|
|
|
+ background-color: #ddd;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+</style>
|