Merge remote-tracking branch 'origin/master'

This commit is contained in:
haozq 2024-04-01 17:44:06 +08:00
commit fb7698c01c
9 changed files with 516 additions and 52 deletions

View File

@ -26,7 +26,9 @@
"echarts": "4.2.1",
"element-ui": "2.15.13",
"file-saver": "2.0.1",
"flv.js": "^1.6.2",
"fuse.js": "3.4.4",
"jquery": "^3.7.1",
"js-cookie": "2.2.0",
"jsonlint": "1.6.3",
"jszip": "3.2.1",
@ -35,10 +37,11 @@
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"qs": "^6.11.2",
"qs": "^6.12.0",
"screenfull": "4.2.0",
"script-loader": "0.7.2",
"sortablejs": "1.8.4",
"video.js": "^8.10.0",
"vue": "2.6.10",
"vue-count-to": "1.0.13",
"vue-router": "3.0.2",

View File

@ -0,0 +1,26 @@
import request from '@/utils/videoRequest'
import qs from 'qs'
import $ from 'jquery'
// 视频监控相关操作
export function handleVideoGet(url, data) {
return request({
url,
method: 'get',
params: data
})
}
export function handleVideoPost(url, data, scb) {
$.ajax({
type: 'post',
url: url,
data: data,
traditional: true,
dataType: 'json',
async: true,
complete: function(res) {
scb && scb(res)
}
})
}

View File

@ -21,6 +21,10 @@ import './utils/error-log' // error log
import * as filters from './filters' // global filters
import qs from 'qs'
import Video from 'video.js'
import 'video.js/dist/video-js.min.css'
/**
* If you don't want to use mock-server
* you want to use MockJs for mock api
@ -35,7 +39,7 @@ import qs from 'qs'
// }
Vue.use(Element, {
size: Cookies.get('size') || 'medium', // set element-ui default size
size: Cookies.get('size') || 'medium' // set element-ui default size
// locale: enLang // 如果使用中文,无需设置,请删除
})
@ -52,3 +56,5 @@ new Vue({
store,
render: h => h(App)
})
Vue.prototype.$video = Video

View File

@ -0,0 +1,53 @@
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken, removeToken } from '@/utils/auth'
import router from '@/router' // @表示src目录
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 10000
})
service.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers['Authorization'] = getToken()
}
return config
},
error => {
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
response => {
const res = response.data
// console.log(res)
if (res.code === 401) {
removeToken()
MessageBox.confirm('登录已过期', '退出登录', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
router.push({ path: '/login' })
})
return
}
return res
},
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service

View File

@ -41,7 +41,8 @@
<el-table-column label="计划结束时间" align="center" prop="planEndTime" />
<el-table-column label="实际开始时间" align="center" prop="startTime" />
<el-table-column label="实际结束时间" align="center" prop="endTime" />
<el-table-column label="进度占比" align="center" prop="gxWeight" />
<el-table-column label="权重" align="center" prop="gxWeight" />
<el-table-column label="当前进度" align="center" prop="planProgress" />
<el-table-column label="延迟原因" align="center" prop="delaReason" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160">
<template slot-scope="{ row, $index }">
@ -102,6 +103,7 @@
</el-table-column>
<el-table-column :label="`${title}名称`" align="center" prop="gxName" />
<el-table-column label="工序进度" align="center" prop="gxProgress" />
<el-table-column v-if="title === '杆塔'" label="工序名称" align="center" prop="currentGxName" />
<el-table-column label="填报时间" align="center" prop="createTime" />
</el-table>
@ -122,7 +124,7 @@
label-width="120px"
>
<el-form-item label="当前工序" prop="nowGxId">
<el-select v-model="temp.nowGxId" placeholder="当前工序" style="width: 100%">
<el-select v-model="temp.nowGxId" :disabled="processType === '变电'" placeholder="当前工序" style="width: 100%">
<el-option v-for="item in typeList" :key="item.id" :value="item.id" :label="item.name" />
</el-select>
</el-form-item>
@ -288,6 +290,9 @@ export default {
//
handleCreateProcess() {
this.dialogStatus = 'create'
if (this.processType === '变电' && this.temp.gxId) {
this.temp.nowGxId = this.temp.gxId
}
this.dialogFormVisible = true
},
createData() {
@ -303,8 +308,6 @@ export default {
})
this.dialogFormVisible = false
this.getList2()
}).finally(() => {
// this.dialogFormVisible = false
})
}
})

View File

@ -41,7 +41,7 @@
<el-table-column label="边带名称" align="center" prop="bdName" />
<el-table-column label="边带编码" align="center" prop="bdCode" />
<el-table-column label="边带IP" align="center" prop="bdIp" />
<el-table-column label="边带类型名称" align="center" prop="bdTypeName" />
<!-- <el-table-column label="边带类型名称" align="center" prop="bdTypeName" />-->
<el-table-column label="杆塔名称" align="center" prop="gtName" />
<el-table-column label="工程名称" align="center" prop="proName" />
<el-table-column label="绑定时间" align="center" prop="bindTime" />
@ -80,12 +80,12 @@
<el-form-item label="边带IP" prop="bdIp">
<el-input v-model="temp.bdIp" placeholder="边带IP" :maxlength="50" />
</el-form-item>
<el-form-item label="边带类型:" prop="bdType">
<SidebandTypeSelect :bind-value.sync="temp.bdType" />
</el-form-item>
<el-form-item label="设备类型:" prop="typeCode">
<DeviceSelect :bind-value.sync="temp.typeCode" />
</el-form-item>
<!-- <el-form-item label="边带类型:" prop="bdType">-->
<!-- <SidebandTypeSelect :bind-value.sync="temp.bdType" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="设备类型:" prop="typeCode">-->
<!-- <DeviceSelect :bind-value.sync="temp.typeCode" />-->
<!-- </el-form-item>-->
<el-form-item label="标段编码:" prop="bidCode">
<ProjectSelect :bind-type.sync="currentProjectType" :bind-value.sync="temp.bidCode" @change="handleProjectChange" />
</el-form-item>
@ -173,8 +173,8 @@ export default {
bdName: [{ required: true, message: '不能为空', trigger: 'blur' }],
bdCode: [{ required: true, message: '不能为空', trigger: 'blur' }],
bdIp: [{ required: true, message: '不能为空', trigger: 'blur' }],
bdType: [{ required: true, message: '不能为空', trigger: 'change' }],
typeCode: [{ required: true, message: '不能为空', trigger: 'change' }],
// bdType: [{ required: true, message: '', trigger: 'change' }],
// typeCode: [{ required: true, message: '', trigger: 'change' }],
gtId: [{ required: false, message: '不能为空', trigger: 'change' }],
bidCode: [{ required: false, message: '不能为空', trigger: 'change' }]
}

View File

@ -5,76 +5,109 @@
<div class="watch-operate">
<div class="watch-operate__top">
<div class="operate-left">
<!-- <div>-->
<!-- <img :src="operateIcon.videoLocal">-->
<!-- <p>本地录像</p>-->
<!-- </div>-->
<div>
<img :src="operateIcon.videoLocal">
<p>本地录像</p>
</div>
<div>
<img :src="operateIcon.videoRemote">
<p>远程录像</p>
<img :src="operateIcon.videoRemote" @mousedown="handleControl('video')">
<p>录像</p>
</div>
</div>
<div class="operate-center">
<div class="item" />
<div class="item">
<img :src="operateIcon.top">
<img :src="operateIcon.top" @mousedown="handleControl('up')" @mouseup="handleControl('stopTurn')">
</div>
<div class="item" />
<div class="item">
<img :src="operateIcon.left">
<img :src="operateIcon.left" @mousedown="handleControl('left')" @mouseup="handleControl('stopTurn')">
</div>
<div class="item" />
<div class="item">
<img :src="operateIcon.right">
<img :src="operateIcon.right" @mousedown="handleControl('right')" @mouseup="handleControl('stopTurn')">
</div>
<div class="item" />
<div class="item">
<img :src="operateIcon.bottom">
<img :src="operateIcon.bottom" @mousedown="handleControl('down')" @mouseup="handleControl('stopTurn')">
</div>
<div class="item" />
</div>
<div class="operate-right">
<!-- <div>-->
<!-- <img :src="operateIcon.photoLocal">-->
<!-- <p>本地抓拍</p>-->
<!-- </div>-->
<div>
<img :src="operateIcon.photoLocal">
<p>本地抓拍</p>
</div>
<div>
<img :src="operateIcon.photoRemote">
<p>远程抓拍</p>
<img :src="operateIcon.photoRemote" @click="handleControl('capture')">
<p>抓拍</p>
</div>
</div>
</div>
<div class="watch-operate__bottom">
<div>
<img :src="operateIcon.nearFocus">
<p></p>
<img :src="operateIcon.nearFocus" @mousedown="handleControl('near')" @mouseup="handleControl('stopFocus')">
<p></p>
</div>
<div>
<img :src="operateIcon.farFocus">
<p></p>
<img :src="operateIcon.farFocus" @mousedown="handleControl('far')" @mouseup="handleControl('stopFocus')">
<p></p>
</div>
<div>
<img :src="operateIcon.shrink">
<p></p>
<img :src="operateIcon.shrink" @mousedown="handleControl('shrink')" @mouseup="handleControl('stopZoom')">
<p></p>
</div>
<div>
<img :src="operateIcon.amplify">
<p></p>
<img :src="operateIcon.amplify" @mousedown="handleControl('amplify')" @mouseup="handleControl('stopZoom')">
<p></p>
</div>
</div>
<div class="watch-operate__keyboard">
<el-button type="text">快捷键设置</el-button>
<h3 class="item-title">球机当日上线记录 &emsp;&emsp;<a href="#" style="color: #1890ff">快捷键设置</a></h3>
<el-divider />
<div>
<div class="filter-container">
<el-date-picker
v-model="listQuery.workDay"
class="filter-item"
style="width: 50%"
type="date"
value-format="yyyy-MM-dd"
placeholder="选择日期"
/>
<el-button
style="margin-left: 40px"
class="filter-item"
type="primary"
@click="handleFilter"
>
查询
</el-button>
</div>
<el-table
:data="list"
border
fit
highlight-current-row
style="width: 100%"
>
<el-table-column label="开机时间" align="center" prop="upTime" />
<el-table-column label="关机时间" align="center" prop="downTime" />
<el-table-column label="在线时长" align="center" prop="hours" />
</el-table>
</div>
</div>
</div>
<div class="watch-play">
<div id="video-watch" style="pointer-events: none;">
<div id="video-watch" class="video-player-wrap" style="pointer-events: none;">
<video
id="video-target"
id="videoPlayer"
ref="videoPlayer"
class="video-player"
autoplay
controls
preload="none"
width="100%"
style="object-fit: fill"
style="object-fit: fill;"
/>
</div>
</div>
@ -129,9 +162,13 @@ import farFocus from '@/assets/images/video/farFocus.png'
import amplify from '@/assets/images/video/amplify.png'
import shrink from '@/assets/images/video/shrink.png'
import { getRemoteWatchDetail } from '@/api/risk/dailyTask'
import { getRemoteWatchDetail, getRemoteWatchTime } from '@/api/risk/dailyTask'
import _ from 'lodash/fp'
import flvjs from 'flv.js'
import { videoConfig } from '@/views/risk/dailyTask/config/video'
import moment from 'moment'
const tmp = {
bidName: '',
sgStatus: '',
@ -156,6 +193,18 @@ export default {
props: ['componentVisible', 'currentId'],
data() {
return {
//
videoPlayer: null,
startVideoStatus: false,
mediaRecorder: null,
recordedChunks: [],
videoUrl: null,
//
list: [],
listQuery: {
classId: '',
workDay: moment().format('YYYY-MM-DD')
},
detail: _.cloneDeep(tmp),
workerList: [],
operateIcon: {
@ -188,9 +237,14 @@ export default {
visible(val) {
if (val) {
this.getList()
this.initVideo()
this.handleFilter()
}
}
},
mounted() {
// this.initVideo()
},
methods: {
getList() {
getRemoteWatchDetail({ classId: this.currentId }).then(res => {
@ -200,8 +254,194 @@ export default {
})
},
handleCloseModal() {
this.detail = _.cloneDeep(tmp)
this.workerList = []
this.list = []
this.detail = _.cloneDeep(tmp)
this.closeVideo()
},
handleFilter() {
this.listQuery.classId = this.currentId
getRemoteWatchTime(this.listQuery).then(res => {
this.list = res.data
})
},
//
initVideo() {
this.$nextTick(() => {
const videoElement = this.$refs.videoPlayer
const url = `${videoConfig.url}/stream.flv?puid=${videoConfig.puid}&idx=${videoConfig.idx}&stream=${videoConfig.stream}&token=${videoConfig.token}`
try {
this.videoPlayer = flvjs.createPlayer({
type: 'flv',
url: url,
isLive: true,
hasAudio: false
}, {
enableWorker: false,
autoCleanupSourceBuffer: true, //
enableStashBuffer: false,
stashInitialSize: 128, //
statisticsInfoReportInterval: 600
})
this.videoPlayer.attachMediaElement(videoElement)
this.videoPlayer.load()
setTimeout(() => {
this.videoPlayer.play()
}, 200)
} catch (err) {
console.log(err)
}
})
},
//
closeVideo() {
if (this.videoPlayer) {
this.videoPlayer.pause()
this.videoPlayer.unload()
this.videoPlayer.detachMediaElement()
this.videoPlayer.destroy()
}
},
//
getPic() {
const video = this.$refs.videoPlayer
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
const context = canvas.getContext('2d')
context.drawImage(video, 0, 0, canvas.width, canvas.height)
const dataURL = canvas.toDataURL('image/png')
const link = document.createElement('a')
link.download = 'screenshot.png'
link.href = dataURL
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
},
//
handlePhoto() {
this.startVideoStatus = !this.startVideoStatus
if (this.startVideoStatus) { //
this.$message({
showClose: true,
message: '录制中···',
type: 'success',
duration: 2000
})
this.startRecording()
} else { //
this.$message({
showClose: true,
message: '结束录制并下载文件中···',
type: 'success',
duration: 2000
})
this.stopRecording()
}
},
startRecording() {
const videoPlayer = this.$refs.videoPlayer
const stream = videoPlayer.captureStream()
this.mediaRecorder = new MediaRecorder(stream)
this.mediaRecorder.ondataavailable = event => {
if (event.data.size > 0) {
this.recordedChunks.push(event.data)
}
}
this.mediaRecorder.onstop = () => {
const blob = new Blob(this.recordedChunks, { type: 'video/mp4' })
this.recordedChunks = []
this.videoUrl = URL.createObjectURL(blob)
this.downloadVideo() //
}
this.mediaRecorder.start()
},
stopRecording() {
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
this.mediaRecorder.stop()
}
},
downloadVideo() {
const link = document.createElement('a')
link.href = this.videoUrl
link.download = 'recorded_video.mp4'
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
},
//
handleControl(type) {
switch (type) {
case 'up':
videoConfig.handle(type, (res) => {
console.log(res)
})
break
case 'down':
videoConfig.handle(type, (res) => {
console.log(res)
})
break
case 'left':
videoConfig.handle(type, (res) => {
console.log(res)
})
break
case 'right':
videoConfig.handle(type, (res) => {
console.log(res)
})
break
case 'stopTurn':
videoConfig.handle(type, (res) => {
console.log(res)
})
break
case 'near':
videoConfig.handle(type, (res) => {
console.log(res)
})
break
case 'far':
videoConfig.handle(type, (res) => {
console.log(res)
})
break
case 'stopFocus':
videoConfig.handle(type, (res) => {
console.log(res)
})
break
case 'shrink':
videoConfig.handle(type, (res) => {
console.log(res)
})
break
case 'amplify':
videoConfig.handle(type, (res) => {
console.log(res)
})
break
case 'stopZoom':
videoConfig.handle(type, (res) => {
console.log(res)
})
break
case 'capture':
this.getPic()
break
case 'video':
this.handlePhoto()
break
default:
break
}
}
}
}
@ -243,6 +483,7 @@ export default {
box-sizing: border-box;
.operate-left, .operate-right {
width: 15%;
text-align: center;
box-sizing: border-box;
}
.operate-center {
@ -270,13 +511,25 @@ export default {
}
}
.watch-operate__keyboard {
height: 40px;
flex: 1;
box-sizing: border-box;
padding-right: 10px;
}
}
.watch-play {
flex: 1;
height: 100%;
.video-player-wrap {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #000000;
.video-player {
width: 100%;
}
}
}
}
.main {

View File

@ -0,0 +1,124 @@
import { handleVideoPost } from '@/api/video'
// 清新视频监控配置
export const videoConfig = {
url: 'http://220.248.250.31:29605/icvs2',
baseUrl: 'http://220.248.250.31:29605/icvs2',
// baseUrl: '/videoPlayer',
puid: '201115201616581263',
idx: 0,
stream: 0,
token: '123456',
handle: function(type, scb) {
let url = ''
let data = {}
switch (type) {
case 'up':
url = `${this.baseUrl}/PTZ/C_PTZ_Turn?token=${this.token}`
data = {
puid: this.puid,
idx: this.idx,
motion: type
}
break
case 'down':
url = `${this.baseUrl}/PTZ/C_PTZ_Turn?token=${this.token}`
data = {
puid: this.puid,
idx: this.idx,
motion: type
}
break
case 'left':
url = `${this.baseUrl}/PTZ/C_PTZ_Turn?token=${this.token}`
data = {
puid: this.puid,
idx: this.idx,
motion: type
}
break
case 'right':
url = `${this.baseUrl}/PTZ/C_PTZ_Turn?token=${this.token}`
data = {
puid: this.puid,
idx: this.idx,
motion: type
}
break
case 'stopTurn':
url = `${this.baseUrl}/PTZ/C_PTZ_Turn?token=${this.token}`
data = {
puid: this.puid,
idx: this.idx,
motion: 'stop'
}
break
case 'near':
url = `${this.baseUrl}/PTZ/C_PTZ_MakeFocusNear?token=${this.token}`
data = {
puid: this.puid,
idx: this.idx
}
break
case 'far':
url = `${this.baseUrl}/PTZ/C_PTZ_MakeFocusFar?token=${this.token}`
data = {
puid: this.puid,
idx: this.idx
}
break
case 'stopFocus':
url = `${this.baseUrl}/PTZ/C_PTZ_StopFocusMove?token=${this.token}`
data = {
puid: this.puid,
idx: this.idx
}
break
case 'shrink':
url = `${this.baseUrl}/PTZ/C_PTZ_ZoomOutPicture?token=${this.token}`
data = {
puid: this.puid,
idx: this.idx
}
break
case 'amplify':
url = `${this.baseUrl}/PTZ/C_PTZ_ZoomInPicture?token=${this.token}`
data = {
puid: this.puid,
idx: this.idx
}
break
case 'stopZoom':
url = `${this.baseUrl}/PTZ/C_PTZ_StopPictureZoom?token=${this.token}`
data = {
puid: this.puid,
idx: this.idx
}
break
case 'startStorage':
url = `${this.baseUrl}/CSS/C_CSS_StartManualStorage?token=${this.token}`
data = {
puid: this.puid,
idx: this.idx
}
break
case 'endStorage':
url = `${this.baseUrl}/CSS/C_CSS_StopManualStorage?token=${this.token}`
data = {
puid: this.puid,
idx: this.idx
}
break
case 'loadStorage':
url = `${this.baseUrl}/CSS/C_CSS_QueryStorageFiles?token=${this.token}`
data = {
puid: this.puid,
idx: this.idx
}
break
default:
break
}
return handleVideoPost(url, data, scb)
}
}

View File

@ -37,10 +37,6 @@
<el-button class="filter-item" style="margin-left: 10px" type="primary" @click="handleExport">
导出
</el-button>
<el-button class="filter-item" style="margin-left: 10px" type="primary" @click="dialogVisible = true">
监控
</el-button>
</div>
<el-table