Commit 2cf53cfe authored by lei's avatar lei

add:

parent d175353f
...@@ -7,5 +7,7 @@ ENV = 'development' ...@@ -7,5 +7,7 @@ ENV = 'development'
# 若依管理系统/开发环境 # 若依管理系统/开发环境
VUE_APP_BASE_API = '/dev-api' VUE_APP_BASE_API = '/dev-api'
VUE_APP_WS_URL = 'ws://192.168.2.16:8081'
# 路由懒加载 # 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true VUE_CLI_BABEL_TRANSPILE_MODULES = true
...@@ -6,3 +6,4 @@ ENV = 'production' ...@@ -6,3 +6,4 @@ ENV = 'production'
# 若依管理系统/生产环境 # 若依管理系统/生产环境
VUE_APP_BASE_API = '/prod-api' VUE_APP_BASE_API = '/prod-api'
VUE_APP_WS_URL = '/ws-api'
...@@ -21,3 +21,4 @@ selenium-debug.log ...@@ -21,3 +21,4 @@ selenium-debug.log
package-lock.json package-lock.json
yarn.lock yarn.lock
public/video/webrtc-streamer.exe
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit"> <meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <meta
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= webpackConfig.name %></title> <title><%= webpackConfig.name %></title>
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]--> <!-- <script
<style> type="text/javascript"
html, src="<%= BASE_URL %>video/webrtcstreamer.js"
body, ></script>
#app { <script
height: 100%; type="text/javascript"
margin: 0px; src="<%= BASE_URL %>video/adapter.min.js"
padding: 0px; ></script> -->
} <!--[if lt IE 11
.chromeframe { ]><script>
margin: 0.2em 0; window.location.href = "/html/ie.html";
background: #ccc; </script><!
color: #000; [endif]-->
padding: 0.2em 0; <style>
} html,
body,
#loader-wrapper { #app {
position: fixed; height: 100%;
top: 0; margin: 0px;
left: 0; padding: 0px;
width: 100%; }
height: 100%; .chromeframe {
z-index: 999999; margin: 0.2em 0;
} background: #ccc;
color: #000;
#loader { padding: 0.2em 0;
display: block; }
position: relative;
left: 50%; #loader-wrapper {
top: 50%; position: fixed;
width: 150px; top: 0;
height: 150px; left: 0;
margin: -75px 0 0 -75px; width: 100%;
border-radius: 50%; height: 100%;
border: 3px solid transparent; z-index: 999999;
border-top-color: #FFF; }
-webkit-animation: spin 2s linear infinite;
-ms-animation: spin 2s linear infinite; #loader {
-moz-animation: spin 2s linear infinite; display: block;
-o-animation: spin 2s linear infinite; position: relative;
animation: spin 2s linear infinite; left: 50%;
z-index: 1001; top: 50%;
} width: 150px;
height: 150px;
#loader:before { margin: -75px 0 0 -75px;
content: ""; border-radius: 50%;
position: absolute; border: 3px solid transparent;
top: 5px; border-top-color: #fff;
left: 5px; -webkit-animation: spin 2s linear infinite;
right: 5px; -ms-animation: spin 2s linear infinite;
bottom: 5px; -moz-animation: spin 2s linear infinite;
border-radius: 50%; -o-animation: spin 2s linear infinite;
border: 3px solid transparent; animation: spin 2s linear infinite;
border-top-color: #FFF; z-index: 1001;
-webkit-animation: spin 3s linear infinite; }
-moz-animation: spin 3s linear infinite;
-o-animation: spin 3s linear infinite; #loader:before {
-ms-animation: spin 3s linear infinite; content: "";
animation: spin 3s linear infinite; position: absolute;
} top: 5px;
left: 5px;
#loader:after { right: 5px;
content: ""; bottom: 5px;
position: absolute; border-radius: 50%;
top: 15px; border: 3px solid transparent;
left: 15px; border-top-color: #fff;
right: 15px; -webkit-animation: spin 3s linear infinite;
bottom: 15px; -moz-animation: spin 3s linear infinite;
border-radius: 50%; -o-animation: spin 3s linear infinite;
border: 3px solid transparent; -ms-animation: spin 3s linear infinite;
border-top-color: #FFF; animation: spin 3s linear infinite;
-moz-animation: spin 1.5s linear infinite; }
-o-animation: spin 1.5s linear infinite;
-ms-animation: spin 1.5s linear infinite; #loader:after {
-webkit-animation: spin 1.5s linear infinite; content: "";
animation: spin 1.5s linear infinite; position: absolute;
} top: 15px;
left: 15px;
right: 15px;
@-webkit-keyframes spin { bottom: 15px;
0% { border-radius: 50%;
-webkit-transform: rotate(0deg); border: 3px solid transparent;
-ms-transform: rotate(0deg); border-top-color: #fff;
transform: rotate(0deg); -moz-animation: spin 1.5s linear infinite;
} -o-animation: spin 1.5s linear infinite;
100% { -ms-animation: spin 1.5s linear infinite;
-webkit-transform: rotate(360deg); -webkit-animation: spin 1.5s linear infinite;
-ms-transform: rotate(360deg); animation: spin 1.5s linear infinite;
transform: rotate(360deg); }
}
} @-webkit-keyframes spin {
0% {
@keyframes spin { -webkit-transform: rotate(0deg);
0% { -ms-transform: rotate(0deg);
-webkit-transform: rotate(0deg); transform: rotate(0deg);
-ms-transform: rotate(0deg); }
transform: rotate(0deg); 100% {
} -webkit-transform: rotate(360deg);
100% { -ms-transform: rotate(360deg);
-webkit-transform: rotate(360deg); transform: rotate(360deg);
-ms-transform: rotate(360deg); }
transform: rotate(360deg); }
}
} @keyframes spin {
0% {
-webkit-transform: rotate(0deg);
#loader-wrapper .loader-section { -ms-transform: rotate(0deg);
position: fixed; transform: rotate(0deg);
top: 0; }
width: 51%; 100% {
height: 100%; -webkit-transform: rotate(360deg);
background: #7171C6; -ms-transform: rotate(360deg);
z-index: 1000; transform: rotate(360deg);
-webkit-transform: translateX(0); }
-ms-transform: translateX(0); }
transform: translateX(0);
} #loader-wrapper .loader-section {
position: fixed;
#loader-wrapper .loader-section.section-left { top: 0;
left: 0; width: 51%;
} height: 100%;
background: #7171c6;
#loader-wrapper .loader-section.section-right { z-index: 1000;
right: 0; -webkit-transform: translateX(0);
} -ms-transform: translateX(0);
transform: translateX(0);
}
.loaded #loader-wrapper .loader-section.section-left {
-webkit-transform: translateX(-100%); #loader-wrapper .loader-section.section-left {
-ms-transform: translateX(-100%); left: 0;
transform: translateX(-100%); }
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000); #loader-wrapper .loader-section.section-right {
} right: 0;
}
.loaded #loader-wrapper .loader-section.section-right {
-webkit-transform: translateX(100%); .loaded #loader-wrapper .loader-section.section-left {
-ms-transform: translateX(100%); -webkit-transform: translateX(-100%);
transform: translateX(100%); -ms-transform: translateX(-100%);
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000); transform: translateX(-100%);
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000); -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
} transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.loaded #loader {
opacity: 0; .loaded #loader-wrapper .loader-section.section-right {
-webkit-transition: all 0.3s ease-out; -webkit-transform: translateX(100%);
transition: all 0.3s ease-out; -ms-transform: translateX(100%);
} transform: translateX(100%);
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
.loaded #loader-wrapper { transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
visibility: hidden; }
-webkit-transform: translateY(-100%);
-ms-transform: translateY(-100%); .loaded #loader {
transform: translateY(-100%); opacity: 0;
-webkit-transition: all 0.3s 1s ease-out; -webkit-transition: all 0.3s ease-out;
transition: all 0.3s 1s ease-out; transition: all 0.3s ease-out;
} }
.no-js #loader-wrapper { .loaded #loader-wrapper {
display: none; visibility: hidden;
} -webkit-transform: translateY(-100%);
-ms-transform: translateY(-100%);
.no-js h1 { transform: translateY(-100%);
color: #222222; -webkit-transition: all 0.3s 1s ease-out;
} transition: all 0.3s 1s ease-out;
}
#loader-wrapper .load_title {
font-family: 'Open Sans'; .no-js #loader-wrapper {
color: #FFF; display: none;
font-size: 19px; }
width: 100%;
text-align: center; .no-js h1 {
z-index: 9999999999999; color: #222222;
position: absolute; }
top: 60%;
opacity: 1; #loader-wrapper .load_title {
line-height: 30px; font-family: "Open Sans";
} color: #fff;
font-size: 19px;
#loader-wrapper .load_title span { width: 100%;
font-weight: normal; text-align: center;
font-style: italic; z-index: 9999999999999;
font-size: 13px; position: absolute;
color: #FFF; top: 60%;
opacity: 0.5; opacity: 1;
} line-height: 30px;
</style> }
#loader-wrapper .load_title span {
font-weight: normal;
font-style: italic;
font-size: 13px;
color: #fff;
opacity: 0.5;
}
</style>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<div id="loader-wrapper"> <div id="loader-wrapper">
<div id="loader"></div> <div id="loader"></div>
<div class="loader-section section-left"></div> <div class="loader-section section-left"></div>
<div class="loader-section section-right"></div> <div class="loader-section section-right"></div>
<div class="load_title">正在加载系统资源,请耐心等待</div> <div class="load_title">正在加载系统资源,请耐心等待</div>
</div> </div>
</div> </div>
</body> </body>
</html> </html>
This source diff could not be displayed because it is too large. You can view the blob instead.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script type="text/javascript" src="./adapter.min.js"></script>
<script type="text/javascript" src="./webrtcstreamer.js"></script>
</head>
<script>
window.onload = function () {
this.webRtcServer = new WebRtcStreamer("video","http://127.0.0.1:8000/");
webRtcServer.connect(
"rtsp://admin:Gemho120611@192.168.0.15:554/h264/ch1/main/av_stream","rtptransport=tcp&timeout=60"
);
};
window.onbeforeunload = function () {
this.webRtcServer.disconnect();
};
</script>
</head>
<body>
<video id="video" muted playsinline controls></video>
</body>
</html>
var WebRtcStreamer = (function() {
/**
* Interface with WebRTC-streamer API
* @constructor
* @param {string} videoElement - id of the video element tag
* @param {string} srvurl - url of webrtc-streamer (default is current location)
*/
var WebRtcStreamer = function WebRtcStreamer (videoElement, srvurl) {
if (typeof videoElement === "string") {
this.videoElement = document.getElementById(videoElement);
} else {
this.videoElement = videoElement;
}
this.srvurl = srvurl || location.protocol+"//"+window.location.hostname+":"+window.location.port;
this.pc = null;
this.mediaConstraints = { offerToReceiveAudio: true, offerToReceiveVideo: true };
this.iceServers = null;
this.earlyCandidates = [];
}
WebRtcStreamer.prototype._handleHttpErrors = function (response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
/**
* Connect a WebRTC Stream to videoElement
* @param {string} videourl - id of WebRTC video stream
* @param {string} audiourl - id of WebRTC audio stream
* @param {string} options - options of WebRTC call
* @param {string} stream - local stream to send
* @param {string} prefmime - prefered mime
*/
WebRtcStreamer.prototype.connect = function(videourl, audiourl, options, localstream, prefmime) {
this.disconnect();
// getIceServers is not already received
if (!this.iceServers) {
console.log("Get IceServers");
fetch(this.srvurl + "/api/getIceServers")
.then(this._handleHttpErrors)
.then( (response) => (response.json()) )
.then( (response) => this.onReceiveGetIceServers(response, videourl, audiourl, options, localstream, prefmime))
.catch( (error) => this.onError("getIceServers " + error ))
} else {
this.onReceiveGetIceServers(this.iceServers, videourl, audiourl, options, localstream, prefmime);
}
}
/**
* Disconnect a WebRTC Stream and clear videoElement source
*/
WebRtcStreamer.prototype.disconnect = function() {
if (this.videoElement?.srcObject) {
this.videoElement.srcObject.getTracks().forEach(track => {
track.stop()
this.videoElement.srcObject.removeTrack(track);
});
}
if (this.pc) {
fetch(this.srvurl + "/api/hangup?peerid=" + this.pc.peerid)
.then(this._handleHttpErrors)
.catch( (error) => this.onError("hangup " + error ))
try {
this.pc.close();
}
catch (e) {
console.log ("Failure close peer connection:" + e);
}
this.pc = null;
}
}
WebRtcStreamer.prototype.filterPreferredCodec = function(sdp, prefmime) {
const lines = sdp.split('\n');
const [prefkind, prefcodec] = prefmime.toLowerCase().split('/');
let currentMediaType = null;
let sdpSections = [];
let currentSection = [];
// Group lines into sections
lines.forEach(line => {
if (line.startsWith('m=')) {
if (currentSection.length) {
sdpSections.push(currentSection);
}
currentSection = [line];
} else {
currentSection.push(line);
}
});
sdpSections.push(currentSection);
// Process each section
const processedSections = sdpSections.map(section => {
const firstLine = section[0];
if (!firstLine.startsWith('m=' + prefkind)) {
return section.join('\n');
}
// Get payload types for preferred codec
const rtpLines = section.filter(line => line.startsWith('a=rtpmap:'));
const preferredPayloads = rtpLines
.filter(line => line.toLowerCase().includes(prefcodec))
.map(line => line.split(':')[1].split(' ')[0]);
if (preferredPayloads.length === 0) {
return section.join('\n');
}
// Modify m= line to only include preferred payloads
const mLine = firstLine.split(' ');
const newMLine = [...mLine.slice(0,3), ...preferredPayloads].join(' ');
// Filter related attributes
const filteredLines = section.filter(line => {
if (line === firstLine) return false;
if (line.startsWith('a=rtpmap:')) {
return preferredPayloads.some(payload => line.startsWith(`a=rtpmap:${payload}`));
}
if (line.startsWith('a=fmtp:') || line.startsWith('a=rtcp-fb:')) {
return preferredPayloads.some(payload => line.startsWith(`a=${line.split(':')[0].split('a=')[1]}:${payload}`));
}
return true;
});
return [newMLine, ...filteredLines].join('\n');
});
return processedSections.join('\n');
}
/*
* GetIceServers callback
*/
WebRtcStreamer.prototype.onReceiveGetIceServers = function(iceServers, videourl, audiourl, options, stream, prefmime) {
this.iceServers = iceServers;
this.pcConfig = iceServers || {"iceServers": [] };
try {
this.createPeerConnection();
let callurl = this.srvurl + "/api/call?peerid=" + this.pc.peerid + "&url=" + encodeURIComponent(videourl);
if (audiourl) {
callurl += "&audiourl="+encodeURIComponent(audiourl);
}
if (options) {
callurl += "&options="+encodeURIComponent(options);
}
if (stream) {
this.pc.addStream(stream);
}
// clear early candidates
this.earlyCandidates.length = 0;
// create Offer
this.pc.createOffer(this.mediaConstraints).then((sessionDescription) => {
console.log("Create offer:" + JSON.stringify(sessionDescription));
console.log(`video codecs:${Array.from(new Set(RTCRtpReceiver.getCapabilities("video")?.codecs?.map(codec => codec.mimeType)))}`)
console.log(`audio codecs:${Array.from(new Set(RTCRtpReceiver.getCapabilities("audio")?.codecs?.map(codec => codec.mimeType)))}`)
if (prefmime != undefined) {
//set prefered codec
let [prefkind] = prefmime.split('/');
if (prefkind != "video" && prefkind != "audio") {
prefkind = "video";
prefmime = prefkind + "/" + prefmime;
}
console.log("sdp:" + sessionDescription.sdp);
sessionDescription.sdp = this.filterPreferredCodec(sessionDescription.sdp, prefmime);
console.log("sdp:" + sessionDescription.sdp);
}
this.pc.setLocalDescription(sessionDescription)
.then(() => {
fetch(callurl, { method: "POST", body: JSON.stringify(sessionDescription) })
.then(this._handleHttpErrors)
.then( (response) => (response.json()) )
.catch( (error) => this.onError("call " + error ))
.then( (response) => this.onReceiveCall(response) )
.catch( (error) => this.onError("call " + error ))
}, (error) => {
console.log ("setLocalDescription error:" + JSON.stringify(error));
});
}, (error) => {
alert("Create offer error:" + JSON.stringify(error));
});
} catch (e) {
this.disconnect();
alert("connect error: " + e);
}
}
WebRtcStreamer.prototype.getIceCandidate = function() {
fetch(this.srvurl + "/api/getIceCandidate?peerid=" + this.pc.peerid)
.then(this._handleHttpErrors)
.then( (response) => (response.json()) )
.then( (response) => this.onReceiveCandidate(response))
.catch( (error) => this.onError("getIceCandidate " + error ))
}
/*
* create RTCPeerConnection
*/
WebRtcStreamer.prototype.createPeerConnection = function() {
console.log("createPeerConnection config: " + JSON.stringify(this.pcConfig));
this.pc = new RTCPeerConnection(this.pcConfig);
let pc = this.pc;
pc.peerid = Math.random();
pc.onicecandidate = (evt) => this.onIceCandidate(evt);
pc.onaddstream = (evt) => this.onAddStream(evt);
pc.oniceconnectionstatechange = (evt) => {
console.log("oniceconnectionstatechange state: " + pc.iceConnectionState);
if (this.videoElement) {
if (pc.iceConnectionState === "connected") {
this.videoElement.style.opacity = "1.0";
}
else if (pc.iceConnectionState === "disconnected") {
this.videoElement.style.opacity = "0.25";
}
else if ( (pc.iceConnectionState === "failed") || (pc.iceConnectionState === "closed") ) {
this.videoElement.style.opacity = "0.5";
} else if (pc.iceConnectionState === "new") {
this.getIceCandidate();
}
}
}
pc.ondatachannel = function(evt) {
console.log("remote datachannel created:"+JSON.stringify(evt));
evt.channel.onopen = function () {
console.log("remote datachannel open");
this.send("remote channel openned");
}
evt.channel.onmessage = function (event) {
console.log("remote datachannel recv:"+JSON.stringify(event.data));
}
}
try {
let dataChannel = pc.createDataChannel("ClientDataChannel");
dataChannel.onopen = function() {
console.log("local datachannel open");
this.send("local channel openned");
}
dataChannel.onmessage = function(evt) {
console.log("local datachannel recv:"+JSON.stringify(evt.data));
}
} catch (e) {
console.log("Cannor create datachannel error: " + e);
}
console.log("Created RTCPeerConnnection with config: " + JSON.stringify(this.pcConfig) );
return pc;
}
/*
* RTCPeerConnection IceCandidate callback
*/
WebRtcStreamer.prototype.onIceCandidate = function (event) {
if (event.candidate) {
if (this.pc.currentRemoteDescription) {
this.addIceCandidate(this.pc.peerid, event.candidate);
} else {
this.earlyCandidates.push(event.candidate);
}
}
else {
console.log("End of candidates.");
}
}
WebRtcStreamer.prototype.addIceCandidate = function(peerid, candidate) {
fetch(this.srvurl + "/api/addIceCandidate?peerid="+peerid, { method: "POST", body: JSON.stringify(candidate) })
.then(this._handleHttpErrors)
.then( (response) => (response.json()) )
.then( (response) => {console.log("addIceCandidate ok:" + response)})
.catch( (error) => this.onError("addIceCandidate " + error ))
}
/*
* RTCPeerConnection AddTrack callback
*/
WebRtcStreamer.prototype.onAddStream = function(event) {
console.log("Remote track added:" + JSON.stringify(event));
this.videoElement.srcObject = event.stream;
let promise = this.videoElement.play();
if (promise !== undefined) {
promise.catch((error) => {
console.warn("error:"+error);
this.videoElement.setAttribute("controls", true);
});
}
}
/*
* AJAX /call callback
*/
WebRtcStreamer.prototype.onReceiveCall = function(dataJson) {
console.log("offer: " + JSON.stringify(dataJson));
let descr = new RTCSessionDescription(dataJson);
this.pc.setRemoteDescription(descr).then(() => {
console.log ("setRemoteDescription ok");
while (this.earlyCandidates.length) {
let candidate = this.earlyCandidates.shift();
this.addIceCandidate(this.pc.peerid, candidate);
}
this.getIceCandidate()
}
, (error) => {
console.log ("setRemoteDescription error:" + JSON.stringify(error));
});
}
/*
* AJAX /getIceCandidate callback
*/
WebRtcStreamer.prototype.onReceiveCandidate = function(dataJson) {
console.log("candidate: " + JSON.stringify(dataJson));
if (dataJson) {
for (let i=0; i<dataJson.length; i++) {
let candidate = new RTCIceCandidate(dataJson[i]);
console.log("Adding ICE candidate :" + JSON.stringify(candidate) );
this.pc.addIceCandidate(candidate).then( () => { console.log ("addIceCandidate OK"); }
, (error) => { console.log ("addIceCandidate error:" + JSON.stringify(error)); } );
}
this.pc.addIceCandidate();
}
}
/*
* AJAX callback for Error
*/
WebRtcStreamer.prototype.onError = function(status) {
console.log("onError:" + status);
}
return WebRtcStreamer;
})();
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
window.WebRtcStreamer = WebRtcStreamer;
}
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = WebRtcStreamer;
}
...@@ -7,18 +7,61 @@ ...@@ -7,18 +7,61 @@
<script> <script>
import ThemePicker from "@/components/ThemePicker"; import ThemePicker from "@/components/ThemePicker";
import MessageBox from "@/components/MessageBox/index.vue";
export default { export default {
name: "App", name: "App",
components: { ThemePicker }, components: { ThemePicker, MessageBox },
data() {
return {
socket: null, // WebSocket实例
baseWsUrl: process.env.VUE_APP_WS_URL, // WebSocket服务器地址
};
},
metaInfo() { metaInfo() {
return { return {
title: this.$store.state.settings.dynamicTitle && this.$store.state.settings.title, title:
titleTemplate: title => { this.$store.state.settings.dynamicTitle &&
return title ? `${title} - ${process.env.VUE_APP_TITLE}` : process.env.VUE_APP_TITLE this.$store.state.settings.title,
titleTemplate: (title) => {
return title
? `${title} - ${process.env.VUE_APP_TITLE}`
: process.env.VUE_APP_TITLE;
},
};
},
methods: {
showAlarm(data) {
this.$notify({
title: data.alarmMessage,
dangerouslyUseHTMLString: true,
message: this.$createElement("MessageBox", {
props: { message: data },
}),
// duration: 0,
});
},
},
mounted() {
if (this.socket) {
return;
}
// 初始化WebSocket连接
this.socket = new WebSocket(this.baseWsUrl + "/websocket");
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("Received message:", data);
if (data.alarmImageUrl) {
this.showAlarm(data);
} }
};
},
beforeDestroy() {
// 确保在组件销毁时关闭WebSocket连接
if (this.socket) {
this.socket.close();
} }
} },
}; };
</script> </script>
<style scoped> <style scoped>
......
...@@ -32,4 +32,17 @@ export function delAlgorithm(id) { ...@@ -32,4 +32,17 @@ export function delAlgorithm(id) {
url: '/system/algorithmConfig/' + id, url: '/system/algorithmConfig/' + id,
method: 'delete' method: 'delete'
}) })
}
//修改算法配置状态
export function changeAlgorithmStatus(list, type) {
const data = {
list,
type
}
return request({
url: '/system/algorithmConfig/selectAlgorithmConfig',
method: 'post',
data: data
})
} }
\ No newline at end of file
import request from '@/utils/request'
//摄像头离在线数据
export function getCameraOnlineData(query) {
return request({
url: '/system/cameraConfig/cameraOnlineOffline',
method: 'get',
params: query
})
}
//获取视频分析任务离在线数据
export function getVideoAnalysisOnlineData(query) {
return request({
url: '/system/task/taskOnlineOffline',
method: 'get',
params: query
})
}
//统计分析
export function getStatisticsData(query) {
return request({
url: '/system/log/listAlarmLogByTime?startTime=' + query.startTime + '&endTime=' + query.endTime,
method: 'get',
})
}
//获取报警信息统计
export function getAlarmStatisticsData(query) {
return request({
url: '/system/log/alarmOverview',
method: 'get',
})
}
//获取摄像播放地址
export function getVideoPlayUrl(query) {
return request({
url: '/system/cameraConfig/getPlayUrl?cameraId=' + query.cameraId,
method: 'get',
})
}
\ No newline at end of file
...@@ -31,4 +31,12 @@ export function delVideoAnalysisTask(id) { ...@@ -31,4 +31,12 @@ export function delVideoAnalysisTask(id) {
url: '/system/task/' + id, url: '/system/task/' + id,
method: 'delete' method: 'delete'
}) })
}
//抓取截图
export function getVideoAnalysisTaskScreenshot(id) {
return request({
url: '/system/cameraConfig/getImage?cameraId=' + id,
method: 'get'
})
} }
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747879145937" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3645" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M895.8 736.4c0-33.6 7.2-166.2-97.4-271.4-70.4-70.8-160.6-106.8-286.6-112.4V192L128 448l384 256V544.4c80 2.2 124.8 18.2 173.4 40C747.2 612 796 672.4 837 737.6l38.4 62.4H896c0-20.2-0.2-45.8-0.2-63.6z" fill="currentColor" p-id="3646"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747878450842" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2632" xmlns:xlink="http://www.w3.org/1999/xlink" width="200.1953125" height="200"><path d="M358.5 1021.5c-52.93 0-96-43.07-96-96 0-13.12 2.6-25.84 7.74-37.81l0.12-0.27-141.99-184.66-0.34 0.11a95.806 95.806 0 0 1-29.53 4.64c-52.93 0-96-43.07-96-96s43.07-96 96-96c7.56 0 15.09 0.89 22.41 2.64l0.36 0.09 213.67-370.08-0.16-0.25c-9.34-15.12-14.28-32.54-14.28-50.39 0-52.93 43.07-96 96-96 40.36 0 76.66 25.5 90.34 63.46l0.09 0.26 300.27 61.72 0.19-0.26c18.01-25.16 47.21-40.18 78.11-40.18 52.93 0 96 43.07 96 96 0 36.49-20.26 69.36-52.88 85.79l-0.31 0.16 41.76 393.5 0.25 0.12a96.184 96.184 0 0 1 39.11 34.79c9.85 15.41 15.06 33.27 15.06 51.65 0 52.93-43.07 96-96 96-31.18 0-60.53-15.24-78.52-40.76l-0.22-0.31-396.7 138.78-0.05 0.29c-8.2 45.76-47.94 78.97-94.5 78.97z m0-192c34.03 0 64.82 17.44 82.36 46.66l0.21 0.35L832.93 739.4l0.03-0.32c3.81-39.11 31.82-72.49 69.7-83.05l0.41-0.11-40.34-380.14-0.34-0.08c-39.73-9.82-68.86-43.81-72.49-84.56l-0.03-0.37-283.9-58.36-0.15 0.39c-7 17.7-19.01 32.81-34.72 43.69-16.08 11.13-34.96 17.02-54.59 17.02-9.75 0-19.37-1.46-28.59-4.33l-0.38-0.12L176.19 555.1l0.19 0.26c11.86 16.42 18.12 35.83 18.12 56.14 0 17.74-4.88 35.06-14.11 50.11l-0.18 0.29 136.28 177.25 0.37-0.18c13.04-6.28 27.05-9.47 41.64-9.47z" p-id="2633"></path><path d="M416.5 2c40.15 0 76.26 25.37 89.87 63.13l0.19 0.53 0.55 0.11 299.68 61.6 0.64 0.13 0.38-0.53c8.66-12.1 20.19-22.15 33.34-29.06C854.71 90.77 870.06 87 885.5 87c52.66 0 95.5 42.84 95.5 95.5 0 18.03-5.05 35.58-14.6 50.77a95.81 95.81 0 0 1-38 34.57l-0.62 0.31 0.07 0.69 41.7 392.88 0.06 0.55 0.5 0.24c15.91 7.71 29.36 19.68 38.91 34.61 9.8 15.33 14.98 33.09 14.98 51.38 0 52.66-42.84 95.5-95.5 95.5-31.02 0-60.22-15.16-78.11-40.55l-0.43-0.62-0.71 0.25-396.09 138.59-0.55 0.19-0.1 0.57c-3.9 21.77-15.4 41.67-32.39 56.03-17.2 14.54-39.08 22.54-61.62 22.54-52.66 0-95.5-42.84-95.5-95.5 0-13.05 2.59-25.71 7.7-37.61l0.23-0.54-0.36-0.46-141.59-184.16-0.43-0.56-0.67 0.22A95.43 95.43 0 0 1 98.5 707C45.84 707 3 664.16 3 611.5S45.84 516 98.5 516c7.52 0 15.01 0.88 22.29 2.62l0.73 0.17 0.37-0.65 213.33-369.5 0.3-0.52-0.31-0.51c-9.29-15.04-14.2-32.37-14.2-50.12C321 44.84 363.84 2 416.5 2m0 191c-9.7 0-19.27-1.45-28.44-4.31l-0.76-0.24-0.4 0.69-210.98 365.43-0.32 0.56 0.38 0.52C187.77 571.99 194 591.3 194 611.5c0 17.64-4.85 34.88-14.03 49.85l-0.36 0.59 0.42 0.55 135.82 176.65 0.5 0.64 0.73-0.35c12.97-6.26 26.91-9.43 41.42-9.43 16.79 0 33.3 4.42 47.75 12.78a96.095 96.095 0 0 1 34.18 33.63l0.42 0.7 0.77-0.27 391.17-136.87 0.6-0.21 0.06-0.64c1.88-19.33 9.53-37.41 22.11-52.28 12.44-14.71 28.77-25.2 47.22-30.35l0.82-0.23-0.09-0.84-40.26-379.37-0.07-0.7-0.68-0.17c-39.52-9.77-68.5-43.58-72.11-84.12l-0.07-0.74-0.73-0.15-283.12-58.2-0.82-0.17-0.31 0.78c-6.96 17.61-18.91 32.64-34.54 43.46-15.99 11.11-34.77 16.96-54.3 16.96m0-192C363.2 1 320 44.2 320 97.5c0 18.58 5.26 35.93 14.35 50.65l-213.33 369.5A96.669 96.669 0 0 0 98.5 515C45.2 515 2 558.2 2 611.5S45.2 708 98.5 708a96.5 96.5 0 0 0 29.69-4.66l141.59 184.15c-5 11.66-7.78 24.51-7.78 38.01 0 53.3 43.2 96.5 96.5 96.5 47.46 0 86.91-34.25 94.99-79.39l396.08-138.59C867.04 828.81 895.88 845 928.5 845c53.3 0 96.5-43.2 96.5-96.5 0-38.22-22.23-71.26-54.46-86.88l-41.7-392.88C960.37 252.86 982 220.21 982 182.5c0-53.3-43.2-96.5-96.5-96.5-32.37 0-61.01 15.94-78.51 40.39l-299.68-61.6C493.91 27.59 458.31 1 416.5 1z m0 193c40.77 0 75.63-25.28 89.77-61.03l283.12 58.2c3.69 41.4 33.52 75.27 72.87 85l40.26 379.37c-37.74 10.53-66.15 43.41-70.06 83.49L441.29 875.9c-16.87-28.1-47.63-46.9-82.79-46.9-15 0-29.2 3.42-41.86 9.53L180.82 661.88c8.99-14.66 14.18-31.91 14.18-50.38 0-21.07-6.76-40.56-18.22-56.43l210.98-365.42c9.08 2.83 18.73 4.35 28.74 4.35z" fill="currentColor" p-id="2634"></path></svg>
\ No newline at end of file
This image diff could not be displayed because it is too large. You can view the blob instead.
...@@ -13,7 +13,9 @@ body { ...@@ -13,7 +13,9 @@ body {
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
Microsoft YaHei, Arial, sans-serif; Microsoft YaHei, Arial, sans-serif;
} }
.el-radio__original {
display: none !important; /* 隐藏原生 radio 输入,但仍然允许交互 */
}
label { label {
font-weight: 700; font-weight: 700;
} }
......
...@@ -63,7 +63,7 @@ export default { ...@@ -63,7 +63,7 @@ export default {
data: { data: {
type: Object, type: Object,
}, },
// 图片数量限制 // 文件数量限制
limit: { limit: {
type: Number, type: Number,
default: 1, default: 1,
...@@ -92,7 +92,7 @@ export default { ...@@ -92,7 +92,7 @@ export default {
dialogVisible: false, dialogVisible: false,
hideUpload: false, hideUpload: false,
baseUrl: process.env.VUE_APP_BASE_API, baseUrl: process.env.VUE_APP_BASE_API,
uploadImgUrl: process.env.VUE_APP_BASE_API + this.action, // 上传的图片服务器地址 uploadImgUrl: process.env.VUE_APP_BASE_API + this.action, // 上传的文件服务器地址
headers: { headers: {
Authorization: "Bearer " + getToken(), Authorization: "Bearer " + getToken(),
}, },
...@@ -151,7 +151,7 @@ export default { ...@@ -151,7 +151,7 @@ export default {
if (!isImg) { if (!isImg) {
this.$modal.msgError( this.$modal.msgError(
`文件格式不正确,请上传${this.fileType.join("/")}图片格式文件!` `文件格式不正确,请上传${this.fileType.join("/")}文件格式文件!`
); );
return false; return false;
} }
...@@ -162,11 +162,11 @@ export default { ...@@ -162,11 +162,11 @@ export default {
if (this.fileSize) { if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize; const isLt = file.size / 1024 / 1024 < this.fileSize;
if (!isLt) { if (!isLt) {
this.$modal.msgError(`上传头像图片大小不能超过 ${this.fileSize} MB!`); this.$modal.msgError(`上传头像文件大小不能超过 ${this.fileSize} MB!`);
return false; return false;
} }
} }
this.$modal.loading("正在上传图片,请稍候..."); this.$modal.loading("正在上传文件,请稍候...");
this.number++; this.number++;
}, },
// 文件个数超出 // 文件个数超出
...@@ -186,7 +186,7 @@ export default { ...@@ -186,7 +186,7 @@ export default {
this.uploadedSuccessfully(); this.uploadedSuccessfully();
} }
}, },
// 删除图片 // 删除文件
handleDelete(file) { handleDelete(file) {
const findex = this.fileList.map((f) => f.name).indexOf(file.name); const findex = this.fileList.map((f) => f.name).indexOf(file.name);
if (findex > -1) { if (findex > -1) {
...@@ -196,7 +196,7 @@ export default { ...@@ -196,7 +196,7 @@ export default {
}, },
// 上传失败 // 上传失败
handleUploadError() { handleUploadError() {
this.$modal.msgError("上传图片失败,请重试"); this.$modal.msgError("上传文件失败,请重试");
this.$modal.closeLoading(); this.$modal.closeLoading();
}, },
// 上传结束处理 // 上传结束处理
......
<template>
<div class="alarmmsg">
<el-row>
<el-col :span="12">
<p>
<dict-tag
:options="dict.type.algorithm_level"
:value="message.alarmLevel"
class="alarmmsg-dict"
/>
<span>{{ message.algorithmName }}</span>
</p>
<p>{{ message.cameraName }}</p>
<p>{{ message.alarmTime }}</p>
</el-col>
<el-col :span="12">
<img
:src="'data:image/path;base64,' + message.alarmImageUrl"
class="alarmmsg-img"
/>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
dicts: ["algorithm_level"],
name: "messageBox",
props: {
message: {
type: Object,
default: {
alarmImageUrl: "",
alarmLevel: "",
alarmMessage: "",
alarmTime: "",
cameraPosition: "",
},
},
},
components: {},
data() {
return {};
},
computed: {},
watch: {
message: {
handler(newVal, oldVal) {},
deep: true,
},
},
created() {},
mounted() {},
methods: {},
};
</script>
<style scoped lang="scss">
.alarmmsg {
width: 300px;
height: 100px;
p {
margin-bottom: 10px;
}
.alarmmsg-dict {
display: inline-block;
}
.alarmmsg-img {
width: 100px;
height: 80px;
}
}
</style>
<template>
<!-- <map-video :showV="videoShow" :rtspIp="rtspIp"></map-video> -->
<video
:id="videoID"
class="video-js"
style="object-fit: fill"
controls
autoplay
autobuffer
muted
preload="auto"
></video>
</template>
<script>
import "video.js/dist/video-js.css";
import videojs from "video.js";
export default {
name: "TanchengkjWebDialogCom",
props: {
videoID: {
type: String,
default() {
return "videoID";
},
},
rtspIp: {
type: String,
default() {
return "";
},
},
videoShow: {
type: Boolean,
default() {
return false;
},
},
videoPlayName: {
type: String,
default() {
return "";
},
},
},
data() {
return {
dialogFromVisible: this.videoShow,
inDate: "",
t2: null,
getUrl: "http://127.0.0.1:8000",
player: null,
webRtcServer: null,
webRtcList: [],
playerList: [],
videoSeting: {
language: "zh-CN",
autoplay: true, // true/false 播放器准备好之后,是否自动播放 【默认false】
controls: true, // /false 是否拥有控制条 【默认true】,如果设为false ,那么只能通过api进行控制了。也就是说界面上不会出现任何控制按钮
/* height: 100, // 视频容器的高度,字符串或数字 单位像素 比如: height:300 or height:‘300px‘
width: 100, // 视频容器的宽度, 字符串或数字 单位像素*/
loop: false, // /false 视频播放结束后,是否循环播放
muted: true, // /false 是否静音
poster: "", // 播放前显示的视频画面,播放开始之后自动移除。通常传入一个URL
preload: "auto", // 预加载 ‘auto‘ 自动 ’metadata‘ 元数据信息 ,比如视频长度,尺寸等 ‘none‘ 不预加载任何数据,直到用户开始播放才开始下载
bigPlayButton: true,
},
};
},
watch: {
videoShow: {
immediate: false,
handler: function (value) {
this.dialogFromVisible = value;
if (value == false) {
console.log("视频关闭了!");
// this.playerList[0].pause();
// this.webRtcList[0].webRtcServer.disconnect();
} else {
this.$forceUpdate();
console.log("输出一下rtsp:", this.rtspIp);
setTimeout(() => {
this.getWebRtc();
}, 1000);
}
},
},
height: {
immediate: false,
handler: function (value) {
this.$forceUpdate();
},
},
},
mounted() {
let that = this;
this.initNDate();
let procedure1 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("1执行crtWebRtc成功!");
that.crtWebRtc();
resolve();
});
});
let procedure2 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("2执行getvideo成功!");
that.getVideo();
resolve();
});
});
let procedure3 = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("3执行getWebRtc成功!");
that.getWebRtc();
resolve();
});
});
procedure1
.then((data) => {
return procedure2;
})
.then((data) => {
return procedure3;
});
console.log("mounted输出rtspip:", this.rtspIp);
// console.log("mounted输出videoID:", this.videoID);
// console.log("mounted输出getUrl:", this.getUrl);
},
created() {},
methods: {
/*设置当前系统时间*/
initNDate: function () {
this.t2 = setInterval(() => {
this.inDate = new Date().toLocaleString();
}, 1000);
},
getVideo() {
let that = this;
this.playerList[0] = videojs(
this.videoID,
this.videoSeting,
function onPlayerReady() {
videojs.log("Your player is ready!");
this.on("loadstart", function () {
console.log("开始请求数据 ");
});
this.on("progress", function () {
console.log("正在请求数据 ");
});
this.on("loadedmetadata", function () {
console.log("获取资源长度完成 ");
});
this.on("canplaythrough", function () {
console.log("视频源数据加载完成");
});
this.on("waiting", function () {
console.log("等待数据");
});
this.on("play", function () {
console.log("视频开始播放");
// that.getWebRtc();
});
this.on("playing", function () {
console.log("视频播放中");
});
this.on("pause", function () {
console.log("视频暂停播放");
that.webRtcList[0].webRtcServer.disconnect();
});
this.on("ended", function () {
console.log("视频播放结束");
});
this.on("error", function () {
console.log("加载错误");
});
this.on("seeking", function () {
console.log("视频跳转中");
});
this.on("seeked", function () {
console.log("视频跳转结束");
});
this.on("ratechange", function () {
console.log("播放速率改变");
});
this.on("timeupdate", function () {
console.log("播放时长改变");
});
this.on("volumechange", function () {
console.log("音量改变");
});
this.on("stalled", function () {
console.log("网速异常");
});
}
);
this.playerList.push(this.player);
console.log("playerList", this.playerList);
console.log("id_", this.playerList[0].id_);
},
crtWebRtc() {
console.log("webRtcList:", this.webRtcList);
for (const item of this.webRtcList) {
if (this.videoID == item.name) {
return;
}
}
console.log("进来了开始初始化webRtc模块:", this.videoID);
this.webRtcServer = new WebRtcStreamer(
this.videoID,
// "http://192.168.1.166:8000"
this.getUrl
);
let s = {};
s.name = this.videoID;
s.webRtcServer = this.webRtcServer;
this.webRtcList.push(s);
console.log("this.webrtc哈", this.webRtcList);
},
getWebRtc() {
console.log("将rtspip传递给webRtcServer.connect:", this.rtspIp);
console.log("打印:webrtclist", this.webRtcList);
this.webRtcList[0].webRtcServer.connect(this.rtspIp, this.rtspIp);
// for (const item of this.webRtcList) {
// if (this.videoID == item.name) {
// return;
// }
// item.webRtcServer.connect(this.rtspIp);
// }
},
},
beforeDestroy() {
console.log("运行销毁前生命周期事件");
this.webRtcList[0].webRtcServer.disconnect();
if (this.player) {
this.playerList[0].dispose();
}
},
};
</script>
<style scoped>
.lf20 {
margin-left: 20px;
}
/* // 覆盖层元素增加可穿透点击事件 */
.el-dialog__wrapper {
pointer-events: none;
}
/* // 弹窗层元素不可穿透点击事件(不影响弹窗层元素的点击事件) */
.el-dialog {
pointer-events: auto;
}
.el-dialog__title {
line-height: 24px !important;
font-size: 15px !important;
color: #303133;
}
.el-dialog__header {
padding: 5px 5px 0px 5px !important;
text-align: left;
}
.el-dialog__headerbtn {
position: absolute;
top: 5px !important;
right: 5px !important;
padding: 0;
background: 0 0;
border: none;
outline: 0;
cursor: pointer;
font-size: 16px;
}
.el-dialog__body {
padding: 2px !important;
color: #606266;
font-size: 14px;
word-break: break-all;
height: 40vh;
overflow-y: auto;
}
.el-divider--horizontal {
display: block;
height: 1px;
width: 100%;
margin: 0px 0 !important;
}
.el-menu:hover {
opacity: 1 !important;
}
.ve-line {
margin-left: 10px;
padding-top: 20px;
color: #fff4fd;
width: 540px !important;
}
.el-row {
margin-bottom: 2px;
&:last-child {
margin-bottom: 0;
}
}
.el-col {
border-radius: 4px;
}
.bg-purple-dark {
background: #99a9bf;
}
.bg-purple {
background: #d3dce6;
}
.bg-purple-light {
background: #e5e9f2;
}
.grid-content {
border-radius: 4px;
min-height: 36px;
height: 230px;
}
.row-bg {
background-color: #f9fafc;
}
.cell-v {
display: flex;
flex-direction: column;
height: 30vh;
}
.player {
height: 100%;
}
.bk-button-group {
color: #00a0e9;
}
.video-js {
width: 100%;
height: 100%;
}
.video-js .vjs-big-play-button {
top: 50%;
left: 50%;
margin-left: -1.5em;
margin-top: -1em;
}
.vjs-loading-spinner:before,
.vjs-loading-spinner:after {
content: "";
position: absolute;
margin: -6px;
-webkit-box-sizing: inherit;
box-sizing: inherit;
width: inherit;
height: inherit;
border-radius: inherit;
opacity: 1;
border: inherit;
border-color: transparent;
border-top-color: white;
display: none;
}
</style>
...@@ -13,7 +13,6 @@ import router from './router' ...@@ -13,7 +13,6 @@ import router from './router'
import directive from './directive' // directive import directive from './directive' // directive
import plugins from './plugins' // plugins import plugins from './plugins' // plugins
import { download } from '@/utils/request' import { download } from '@/utils/request'
import './assets/icons' // icon import './assets/icons' // icon
import './permission' // permission control import './permission' // permission control
import { getDicts } from "@/api/system/dict/data"; import { getDicts } from "@/api/system/dict/data";
...@@ -37,6 +36,8 @@ import DictTag from '@/components/DictTag' ...@@ -37,6 +36,8 @@ import DictTag from '@/components/DictTag'
import VueMeta from 'vue-meta' import VueMeta from 'vue-meta'
// 字典数据组件 // 字典数据组件
import DictData from '@/components/DictData' import DictData from '@/components/DictData'
//自适应组件
import VScaleScreen from 'v-scale-screen'
// 全局方法挂载 // 全局方法挂载
Vue.prototype.getDicts = getDicts Vue.prototype.getDicts = getDicts
...@@ -61,6 +62,7 @@ Vue.component('ImagePreview', ImagePreview) ...@@ -61,6 +62,7 @@ Vue.component('ImagePreview', ImagePreview)
Vue.use(directive) Vue.use(directive)
Vue.use(plugins) Vue.use(plugins)
Vue.use(VueMeta) Vue.use(VueMeta)
Vue.use(VScaleScreen)
DictData.install() DictData.install()
/** /**
......
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
<el-option <el-option
v-for="(item, index) in cameraAdderss" v-for="(item, index) in cameraAdderss"
:key="index" :key="index"
:value="item.address" :value="item.positionId"
:label="item.name" :label="item.positionName"
></el-option> ></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
placeholder="报警处理状态" placeholder="报警处理状态"
clearable clearable
> >
<el-option :value="3" label="待处理"></el-option> <el-option :value="2" label="待处理"></el-option>
<el-option :value="1" label="正确报警"></el-option> <el-option :value="1" label="正确报警"></el-option>
<el-option :value="0" label="错误报警"></el-option> <el-option :value="0" label="错误报警"></el-option>
</el-select> </el-select>
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
></el-table-column> ></el-table-column>
<el-table-column <el-table-column
label="视频分析任务名称" label="视频分析任务名称"
prop="videoAnlysisTasksName" prop="taskName"
align="center" align="center"
></el-table-column> ></el-table-column>
<el-table-column <el-table-column
...@@ -66,11 +66,15 @@ ...@@ -66,11 +66,15 @@
prop="cameraName" prop="cameraName"
align="center" align="center"
></el-table-column> ></el-table-column>
<el-table-column <el-table-column label="报警图片" prop="alarmImageUrl" align="center">
label="报警图片" <template slot-scope="scope">
prop="alarmImg" <image-preview
align="center" :src="scope.row.alarmImageUrl"
></el-table-column> :width="60"
:height="40"
/>
</template>
</el-table-column>
<el-table-column <el-table-column
label="报警时间" label="报警时间"
prop="alarmTime" prop="alarmTime"
...@@ -81,32 +85,40 @@ ...@@ -81,32 +85,40 @@
prop="algorithmName" prop="algorithmName"
align="center" align="center"
></el-table-column> ></el-table-column>
<el-table-column <el-table-column label="算法报警等级" prop="alarmLevel" align="center">
label="算法报警等级"
prop="algorithmLevel"
align="center"
>
<template slot-scope="scope"> <template slot-scope="scope">
<dict-tag <dict-tag
:options="dict.type.algorithm_level" :options="dict.type.algorithm_level"
:value="scope.row.algorithmLevel" :value="scope.row.alarmLevel"
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="报警处理状态" align="center"> <el-table-column label="报警处理状态" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
{{ scope.row.alarmStatus === 0 ? "错误报警" : "正确报警" }} <el-tag v-if="scope.row.alarmStatus === 2" type="warning"
>待处理</el-tag
>
<el-tag v-if="scope.row.alarmStatus === 1" type="success"
>正确报警</el-tag
>
<el-tag v-if="scope.row.alarmStatus === 0" type="danger"
>错误报警</el-tag
>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center" width="200px"> <el-table-column label="操作" align="center" width="200px">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button type="text" size="mini" @click="editBut(scope.row)"> <el-button type="text" size="mini" @click="editBut(scope.row, 1)">
正确报警 正确报警
</el-button> </el-button>
<el-button type="text" size="mini" @click="editBut(scope.row)"> <el-button type="text" size="mini" @click="editBut(scope.row, 0)">
错误报警 错误报警
</el-button> </el-button>
<el-button type="text" size="mini" @click="deleteBut(scope.row)"> <el-button
type="text"
size="mini"
@click="deleteBut(scope.row.logId)"
>
<i class="el-icon-delete el-icon--left"></i> <i class="el-icon-delete el-icon--left"></i>
删除 删除
</el-button> </el-button>
...@@ -130,6 +142,7 @@ import { ...@@ -130,6 +142,7 @@ import {
delAlarmLog, delAlarmLog,
updateAlarmLog, updateAlarmLog,
} from "@/api/business/alarmlog.js"; } from "@/api/business/alarmlog.js";
import { getCameraPositionList } from "@/api/business/cameraconfig.js";
export default { export default {
dicts: ["algorithm_level"], dicts: ["algorithm_level"],
name: "Alarmlog", name: "Alarmlog",
...@@ -144,8 +157,8 @@ export default { ...@@ -144,8 +157,8 @@ export default {
alarmImg: "", // 报警图片 alarmImg: "", // 报警图片
alarmTime: "", // 报警时间 alarmTime: "", // 报警时间
algorithmName: "", // 算法名称 algorithmName: "", // 算法名称
algorithmLevel: 1, // 算法报警等级 algorithmLevel: null, // 算法报警等级
alarmStatus: 0, // 报警处理状态 alarmStatus: null, // 报警处理状态
}, },
tableList: [], tableList: [],
cameraAdderss: [ cameraAdderss: [
...@@ -166,24 +179,81 @@ export default { ...@@ -166,24 +179,81 @@ export default {
watch: {}, watch: {},
created() { created() {
this.getList(); this.getList();
getCameraPositionList().then((res) => {
if (res.code == 200) {
// 处理空数组情况
const rawData = Array.isArray(res.data) ? res.data : [];
this.cameraAdderss = this.flattenJson(rawData).map((item) => ({
positionName: item.positionName,
positionId: item.positionId,
}));
} else {
this.cameraAdderss = []; // 接口异常时设为空数组
}
});
}, },
mounted() {}, mounted() {},
methods: { methods: {
// 新增扁平化方法
flattenJson(arr) {
return arr.reduce((acc, item) => {
acc.push(item);
if (item.children && item.children.length) {
acc.push(...this.flattenJson(item.children));
}
return acc;
}, []);
},
// 搜索 // 搜索
search() {}, search() {
let params = { ...this.queryParams, ...this.form };
getAlarmLogList(params).then((res) => {
if (res.code == 200) {
this.tableList = res.rows;
this.total = res.total;
}
});
},
// 删除 // 删除
deleteBut(row) { deleteBut(id) {
this.$confirm("确认删除吗?", "提示", { delAlarmLog(id).then((res) => {
confirmButtonText: "确定", if (res.code == 200) {
cancelButtonText: "取消", this.$message({
type: "warning", message: "删除成功",
type: "success",
});
this.getList();
} else {
this.$message({
message: res.msg,
type: "error",
});
}
}); });
}, },
// 编辑 // 编辑
editBut(row) {}, editBut(row, alarmStatus) {
updateAlarmLog({
logId: row.logId,
alarmStatus: alarmStatus, // 1 正确报警 0 错误报警
}).then((res) => {
if (res.code == 200) {
this.$message({
message: "操作成功",
type: "success",
});
this.getList();
} else {
this.$message({
message: res.msg,
type: "error",
});
}
});
},
// 获取数据列表 // 获取数据列表
getList() { getList() {
getAlarmLogList(this.queryParams).then((res) => { getAlarmLogList({ ...this.queryParams, ...this.form }).then((res) => {
if (res.code == 200) { if (res.code == 200) {
this.tableList = res.rows; this.tableList = res.rows;
this.total = res.total; this.total = res.total;
......
<template> <template>
<div class="app-container">报警推送</div> <div class="app-container">
<div class="Pd20">
<el-form :model="form" :inline="true" size="mini">
<el-form-item>
<el-select v-model="form.cameraName" placeholder="监控点位" clearable>
<el-option
v-for="(item, index) in cameraAdderss"
:key="index"
:value="item.positionId"
:label="item.positionName"
></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-select
v-model="form.algorithmLevel"
placeholder="算法报警等级"
clearable
>
<el-option
v-for="(item, index) in dict.type.algorithm_level"
:key="index"
:value="item.value"
:label="item.label"
></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-select
v-model="form.alarmStatus"
placeholder="报警处理状态"
clearable
>
<el-option :value="2" label="待处理"></el-option>
<el-option :value="1" label="正确报警"></el-option>
<el-option :value="0" label="错误报警"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-input
v-model="form.algorithmName"
placeholder="算法名称"
></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="search">
<i class="el-icon-search el-icon--left"></i>
搜索
</el-button>
</el-form-item>
</el-form>
<el-table :data="tableList">
<el-table-column
label="摄像头ID"
prop="cameraId"
align="center"
></el-table-column>
<el-table-column
label="视频分析任务名称"
prop="taskName"
align="center"
></el-table-column>
<el-table-column
label="摄像头名称"
prop="cameraName"
align="center"
></el-table-column>
<el-table-column label="报警图片" prop="alarmImageUrl" align="center">
<template slot-scope="scope">
<image-preview
:src="scope.row.alarmImageUrl"
:width="60"
:height="40"
/>
</template>
</el-table-column>
<el-table-column
label="报警时间"
prop="alarmTime"
align="center"
></el-table-column>
<el-table-column
label="算法名称"
prop="algorithmName"
align="center"
></el-table-column>
<el-table-column label="算法报警等级" prop="alarmLevel" align="center">
<template slot-scope="scope">
<dict-tag
:options="dict.type.algorithm_level"
:value="scope.row.alarmLevel"
/>
</template>
</el-table-column>
<el-table-column label="推送负责人" prop="pushBy" align="center">
</el-table-column>
<el-table-column label="报警处理状态" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.alarmStatus === 2" type="warning"
>待处理</el-tag
>
<el-tag v-if="scope.row.alarmStatus === 1" type="success"
>正确报警</el-tag
>
<el-tag v-if="scope.row.alarmStatus === 0" type="danger"
>错误报警</el-tag
>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200px">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="pushBut(scope.row)">
报警推送
</el-button>
<el-button
type="text"
size="mini"
@click="deleteBut(scope.row.logId)"
>
<i class="el-icon-delete el-icon--left"></i>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 新增弹窗组件 -->
<el-dialog
title="推送用户选择"
:visible.sync="pushDialogVisible"
width="30%"
center
>
<el-select
v-model="selectedUsers"
filterable
placeholder="请选择要推送的用户"
style="width: 100%"
>
<el-option
v-for="user in userList"
:key="user.userId"
:label="user.userName"
:value="user.userId"
/>
</el-select>
<span slot="footer" class="dialog-footer">
<el-button @click="pushDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="confirmPush">确 定</el-button>
</span>
</el-dialog>
</div>
</div>
</template> </template>
<script> <script>
import {
getAlarmLogList,
delAlarmLog,
updateAlarmLog,
} from "@/api/business/alarmlog.js";
import { getCameraPositionList } from "@/api/business/cameraconfig.js";
import { listUser } from "@/api/system/user.js";
export default { export default {
name: "Alarmpush", dicts: ["algorithm_level"],
name: "Alarmlog",
props: {}, props: {},
components: {}, components: {},
data() { data() {
return {}; return {
form: {
id: 1, // 摄像头ID
videoAnlysisTasksName: "", // 视频分析任务名称
cameraName: "", // 摄像头名称
alarmImg: "", // 报警图片
alarmTime: "", // 报警时间
algorithmName: "", // 算法名称
algorithmLevel: null, // 算法报警等级
alarmStatus: null, // 报警处理状态
// 新增弹窗相关数据
pushDialogVisible: false,
selectedUsers: [],
userList: [], // 用户列表
currentLogId: null, // 当前操作的报警日志ID
},
tableList: [],
cameraAdderss: [
{
name: "摄像头1", // 摄像头名称
address: "地址一", // 摄像头地址
},
], // 摄像头地址
total: 0, // 总数量
queryParams: {
pageNum: 1, // 当前页码
pageSize: 10, // 每页显示的数量
},
// 新增弹窗相关数据
pushDialogVisible: false,
selectedUsers: [],
userList: [], // 用户列表
currentLogId: null, // 当前操作的报警日志ID
};
}, },
computed: {}, computed: {},
watch: {}, watch: {},
created() {}, created() {
this.getList();
getCameraPositionList().then((res) => {
if (res.code == 200) {
// 处理空数组情况
const rawData = Array.isArray(res.data) ? res.data : [];
this.cameraAdderss = this.flattenJson(rawData).map((item) => ({
positionName: item.positionName,
positionId: item.positionId,
}));
} else {
this.cameraAdderss = []; // 接口异常时设为空数组
}
});
},
mounted() {}, mounted() {},
methods: {}, methods: {
// 新增扁平化方法
flattenJson(arr) {
return arr.reduce((acc, item) => {
acc.push(item);
if (item.children && item.children.length) {
acc.push(...this.flattenJson(item.children));
}
return acc;
}, []);
},
// 搜索
search() {
let params = { ...this.queryParams, ...this.form };
getAlarmLogList(params).then((res) => {
if (res.code == 200) {
this.tableList = res.rows;
this.total = res.total;
}
});
},
// 删除
deleteBut(id) {
delAlarmLog(id).then((res) => {
if (res.code == 200) {
this.$message({
message: "删除成功",
type: "success",
});
this.getList();
} else {
this.$message({
message: res.msg,
type: "error",
});
}
});
},
// 编辑
pushBut(row, alarmStatus) {
this.currentLogId = row.logId;
this.pushDialogVisible = true;
this.loadUserList();
},
// 加载用户列表(需要对接后端API)
async loadUserList() {
try {
const response = await listUser();
if (response.code === 200) {
this.userList = response.rows;
}
} catch (error) {
console.error("获取用户列表失败:", error);
}
},
// 确认推送
async confirmPush() {
try {
const params = {
logId: this.currentLogId,
pushBy: this.selectedUsers,
};
const res = await updateAlarmLog(params);
if (res.code === 200) {
this.$message.success("推送成功");
this.pushDialogVisible = false;
this.selectedUsers = [];
this.getList(); // 刷新表格数据
}
} catch (error) {
this.$message.error("推送失败");
console.error("推送异常:", error);
}
},
// 获取数据列表
getList() {
getAlarmLogList({ ...this.queryParams, ...this.form }).then((res) => {
if (res.code == 200) {
this.tableList = res.rows;
this.total = res.total;
}
});
},
},
}; };
</script> </script>
......
<template> <template>
<div class="app-container"> <div class="app-container">
<el-row type="flex" :gutter="10"> <el-card
<el-col :span="8"> shadow="hover"
<el-card :body-style="{ padding: '20px', height: 'calc(100vh - 200px)' }"
shadow="hover" >
:body-style="{ padding: '20px', height: 'calc(100vh - 200px)' }" <p slot="header"><b>提醒语音管理</b></p>
> <el-button
<p slot="header"><b>报警弹窗及语音提醒</b></p> type="primary"
<div> :disabled="selectedAlarmList.length === 0"
<p> size="small"
选择开启提醒的算法 共 plain
{{ selectedAlarmList.length }} 个算法开启提醒 @click="batchBut('isSelect')"
</p> >算法批量开启提醒</el-button
<el-select >
v-model="selectedAlarmList" <el-button
multiple type="primary"
style="width: 100%" size="small"
placeholder="请选择" :disabled="selectedAlarmList.length === 0"
plain
@click="batchBut('isSound')"
>报警语音开启</el-button
>
<el-button
type="primary"
size="small"
:disabled="selectedAlarmList.length === 0"
@click="batchBut('isPopup')"
plain
>报警弹窗开启</el-button
>
<el-table
:data="tableData"
style="width: 100%; margin-top: 10px"
:row-style="{ textAlign: 'center' }"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column
prop="algorithmId"
label="算法ID"
align="center"
width="80"
></el-table-column>
<el-table-column
prop="algorithmName"
label="算法名称"
></el-table-column>
<el-table-column label="算法等级" align="center">
<template slot-scope="scope">
<dict-tag
:options="dict.type.algorithm_level"
:value="scope.row.algorithmLevel"
/>
</template>
</el-table-column>
<el-table-column label="是否开启" align="center">
<template slot-scope="scope">
<el-switch
v-model="scope.row.isSelect"
active-color="#13ce66"
inactive-color="#ff4949"
:active-value="1"
:inactive-value="0"
@change="handleChange(scope.row)"
> >
<el-option </el-switch>
v-for="item in alarmOptions" </template>
:key="item.value" </el-table-column>
:label="item.label" <el-table-column label="是否开启弹窗" align="center">
:value="item.value" <template slot-scope="scope">
> <el-switch
</el-option> v-model="scope.row.isPopup"
</el-select> active-color="#13ce66"
<p>提醒方式</p> inactive-color="#ff4949"
<p> :active-value="1"
<span>系统报警语音</span> :inactive-value="0"
<el-switch @change="handleChange(scope.row)"
v-model="systemAlarmAudio" >
active-color="#13ce66" </el-switch>
inactive-color="#ff4949" </template>
> </el-table-column>
</el-switch> <el-table-column label="是否开启语音" align="center">
</p> <template slot-scope="scope">
<p> <el-switch
<span>系统报警弹窗</span> v-model="scope.row.isSound"
<el-switch active-color="#13ce66"
v-model="systemAlarmWidow" inactive-color="#ff4949"
active-color="#13ce66" :active-value="1"
inactive-color="#ff4949" :inactive-value="0"
@change="handleChange(scope.row)"
>
</el-switch>
</template>
</el-table-column>
<el-table-column prop="alarmSoundName" label="报警语音" align="center">
<template slot-scope="scope">
{{ scope.row.alarmSoundName | SoundNamefilter }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleEdit(scope.row)">
<i class="el-icon-video-play"></i>
试听
</el-button>
<el-upload
class="upload-demo"
:action="uploadImgUrl"
:on-success="handleUploadSuccess"
:multiple="false"
:show-file-list="false"
:headers="headers"
:limit="1"
>
<el-button
size="mini"
type="text"
@click="handleUpload(scope.row)"
> >
</el-switch> <i class="el-icon-upload"></i>
</p> 替换/上传
<p>显示方式</p> </el-button>
</div> <!-- <div slot="tip" class="el-upload__tip">
</el-card> 只能上传mp3/ACC/WAV/AMR文件,且不超过5MB
</el-col> </div> -->
<el-col :span="16"> </el-upload>
<el-card
shadow="hover" <el-button size="mini" type="text" @click="handleDelete(scope.row)">
:body-style="{ padding: '20px', height: 'calc(100vh - 200px)' }" <i class="el-icon-delete"></i>
> 删除
<p slot="header"><b>提醒语音管理</b></p> </el-button>
<el-table </template>
:data="tableData" </el-table-column>
style="width: 100%" </el-table>
:row-style="{ textAlign: 'center' }" <pagination
> v-show="total > 0"
<el-table-column prop="id" label="算法ID"></el-table-column> :total="total"
<el-table-column :page.sync="queryParams.pageNum"
prop="algorithmName" :limit.sync="queryParams.pageSize"
label="算法名称" @pagination="getList"
></el-table-column> />
<el-table-column label="算法等级"> </el-card>
<template slot-scope="scope">
<dict-tag
:options="dict.type.algorithm_level"
:value="scope.row.algorithmLevel"
/>
</template>
</el-table-column>
<el-table-column
prop="alarmAudio"
label="报警语音"
></el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
@click="handleEdit(scope.row)"
>
<i class="el-icon-video-play"></i>
试听
</el-button>
<el-button
size="mini"
type="text"
@click="handleUpload(scope.row)"
>
<i class="el-icon-upload"></i>
替换/上传
</el-button>
<el-button
size="mini"
type="text"
@click="handleDelete(scope.row)"
>
<i class="el-icon-delete"></i>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
</el-col>
</el-row>
</div> </div>
</template> </template>
<script> <script>
import {
getAlgorithmList,
updateAlgorithm,
changeAlgorithmStatus,
} from "@/api/business/algorithmconfig.js";
import { getToken } from "@/utils/auth";
export default { export default {
dicts: ["algorithm_level"], dicts: ["algorithm_level"],
name: "Alarmreminder", name: "Alarmreminder",
...@@ -127,39 +160,43 @@ export default { ...@@ -127,39 +160,43 @@ export default {
components: {}, components: {},
data() { data() {
return { return {
tableData: [ tableData: [],
{
id: "1",
algorithmName: "算法1",
algorithmLevel: 1,
alarmAudio: "audio1.mp3",
},
],
total: 0, // 总记录数 total: 0, // 总记录数
queryParams: { queryParams: {
pageNum: 1, // 当前页码 pageNum: 1, // 当前页码
pageSize: 10, // 每页显示的记录数 pageSize: 10, // 每页显示的记录数
}, },
selectedAlarmList: [], // 选中的报警算法列表 baseUrl: process.env.VUE_APP_BASE_API,
alarmOptions: [ uploadImgUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的文件服务器地址
{ value: "1", label: "算法1" }, headers: {
{ value: "2", label: "算法2" }, Authorization: "Bearer " + getToken(),
{ value: "3", label: "算法3" }, },
], currentRow: null, // 新增当前操作行数据
systemAlarmAudio: false, // 系统报警语音 selectedAlarmList: [], // 选中的报警列表
systemAlarmWidow: false, // 系统报警弹窗
}; };
}, },
computed: {}, computed: {},
watch: {}, watch: {},
created() {}, created() {
this.getList();
},
mounted() {}, mounted() {},
methods: { methods: {
//获取数据列表 //获取数据列表
getList() {}, getList() {
//编辑 getAlgorithmList(this.queryParams).then((res) => {
if (res.code !== 200) {
return;
}
this.tableData = res.rows; // 假设接口返回的数据列表在res.data.list中
this.total = res.total; // 假设接口返回的总记录数在res.data.total中
});
},
//试听
handleEdit(row) { handleEdit(row) {
console.log("编辑:", row); const audio = new Audio(row.alarmSoundUrl); // 假设alarmAudio是音频文件的路径
audio.play(); // 播放音频
}, },
//删除 //删除
handleDelete(row) { handleDelete(row) {
...@@ -167,10 +204,66 @@ export default { ...@@ -167,10 +204,66 @@ export default {
}, },
//上传 //上传
handleUpload(row) { handleUpload(row) {
console.log("上传:", row); this.currentRow = row; // 保存当前操作行
this.showUpload = true; // 显示上传组件
},
handleUploadSuccess(res) {
console.log(res, "上传成功");
// 上传成功回调处理
if (res.code === 200) {
updateAlgorithm({
algorithmId: this.currentRow.algorithmId, // 假设algorithmId是当前操作行的ID
alarmSoundUrl: res.url, // 假设res.url是上传成功后的音频文件URL
alarmSoundName: res.newFileName, // 假设res.fileName是上传成功后的音频文件名称
}).then((res) => {
if (res.code === 200) {
this.$message.success("上传成功");
this.showUpload = false; // 显示上传组件
this.getList(); // 刷新列表数据
} else {
this.$message.error(res.msg);
}
});
}
},
// 处理开关状态变化
handleChange(data) {
updateAlgorithm(data).then((res) => {
if (res.code !== 200) {
return;
}
this.$message.success("修改成功");
this.getList();
});
},
handleSelectionChange(selection) {
this.selectedAlarmList = selection.map((item) => item.algorithmId);
},
batchBut(type) {
changeAlgorithmStatus({
list: this.selectedAlarmList,
type: type, // 假设status是要修改的状态值
}).then((res) => {
if (res.code !== 200) {
return;
}
this.$message.success("修改成功");
this.getList(); // 刷新列表数据
});
},
},
filters: {
SoundNamefilter(value) {
if (!value) return ""; // 如果值为空,返回空字符串
return value.split("_")[0]; // 返回文件名部分
}, },
}, },
}; };
</script> </script>
<style scoped lang="scss"></style> <style scoped lang="scss">
.upload-demo {
display: inline-block;
}
</style>
<template>
<div class="app-container">
<el-row type="flex" :gutter="10">
<el-col :span="8">
<el-card
shadow="hover"
:body-style="{ padding: '20px', height: 'calc(100vh - 200px)' }"
>
<p slot="header"><b>报警弹窗及语音提醒</b></p>
<div>
<p>
选择开启提醒的算法 共
{{ selectedAlarmList.length }} 个算法开启提醒
</p>
<el-select
v-model="selectedAlarmList"
multiple
style="width: 100%"
placeholder="请选择"
>
<el-option
v-for="item in alarmOptions"
:key="item.algorithmId"
:label="item.algorithmName"
:value="item.algorithmId"
>
</el-option>
</el-select>
<p>提醒方式</p>
<p>
<span>系统报警语音</span>
<el-switch
v-model="systemAlarmAudio"
active-color="#13ce66"
inactive-color="#ff4949"
>
</el-switch>
</p>
<p>
<span>系统报警弹窗</span>
<el-switch
v-model="systemAlarmWidow"
active-color="#13ce66"
inactive-color="#ff4949"
>
</el-switch>
</p>
<p>显示方式</p>
</div>
</el-card>
</el-col>
<el-col :span="16">
<el-card
shadow="hover"
:body-style="{ padding: '20px', height: 'calc(100vh - 200px)' }"
>
<p slot="header"><b>提醒语音管理</b></p>
<el-table
:data="tableData"
style="width: 100%"
:row-style="{ textAlign: 'center' }"
>
<el-table-column
prop="algorithmId"
label="算法ID"
></el-table-column>
<el-table-column
prop="algorithmName"
label="算法名称"
></el-table-column>
<el-table-column label="算法等级">
<template slot-scope="scope">
<dict-tag
:options="dict.type.algorithm_level"
:value="scope.row.algorithmLevel"
/>
</template>
</el-table-column>
<el-table-column prop="alarmSoundName" label="报警语音">
<template slot-scope="scope">
{{ scope.row.alarmSoundName | SoundNamefilter }}
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
@click="handleEdit(scope.row)"
>
<i class="el-icon-video-play"></i>
试听
</el-button>
<el-upload
class="upload-demo"
:action="uploadImgUrl"
:on-success="handleUploadSuccess"
:multiple="false"
:show-file-list="false"
:headers="headers"
:limit="1"
>
<el-button
size="mini"
type="text"
@click="handleUpload(scope.row)"
>
<i class="el-icon-upload"></i>
替换/上传
</el-button>
<!-- <div slot="tip" class="el-upload__tip">
只能上传mp3/ACC/WAV/AMR文件,且不超过5MB
</div> -->
</el-upload>
<el-button
size="mini"
type="text"
@click="handleDelete(scope.row)"
>
<i class="el-icon-delete"></i>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import {
getAlgorithmList,
updateAlgorithm,
} from "@/api/business/algorithmconfig.js";
import { getToken } from "@/utils/auth";
export default {
dicts: ["algorithm_level"],
name: "Alarmreminder",
props: {},
components: {},
data() {
return {
tableData: [
{
id: "1",
algorithmName: "算法1",
algorithmLevel: 1,
alarmAudio: "audio1.mp3",
},
],
total: 0, // 总记录数
queryParams: {
pageNum: 1, // 当前页码
pageSize: 10, // 每页显示的记录数
},
selectedAlarmList: [], // 选中的报警算法列表
alarmOptions: [],
systemAlarmAudio: false, // 系统报警语音
systemAlarmWidow: false, // 系统报警弹窗
baseUrl: process.env.VUE_APP_BASE_API,
uploadImgUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的文件服务器地址
headers: {
Authorization: "Bearer " + getToken(),
},
currentRow: null, // 新增当前操作行数据
};
},
computed: {},
watch: {},
created() {
this.getList();
},
mounted() {},
methods: {
//获取数据列表
getList() {
getAlgorithmList(this.queryParams).then((res) => {
if (res.code !== 200) {
return;
}
this.tableData = res.rows; // 假设接口返回的数据列表在res.data.list中
this.alarmOptions = res.rows;
this.selectedAlarmList = res.rows.map((item) => {
if (item.isSelect === 1) {
return item.algorithmId;
}
});
this.total = res.total; // 假设接口返回的总记录数在res.data.total中
});
},
//试听
handleEdit(row) {
const audio = new Audio(row.alarmSoundUrl); // 假设alarmAudio是音频文件的路径
audio.play(); // 播放音频
},
//删除
handleDelete(row) {
console.log("删除:", row);
},
//上传
handleUpload(row) {
this.currentRow = row; // 保存当前操作行
this.showUpload = true; // 显示上传组件
},
handleUploadSuccess(res) {
console.log(res, "上传成功");
// 上传成功回调处理
if (res.code === 200) {
updateAlgorithm({
algorithmId: this.currentRow.algorithmId, // 假设algorithmId是当前操作行的ID
alarmSoundUrl: res.url, // 假设res.url是上传成功后的音频文件URL
alarmSoundName: res.newFileName, // 假设res.fileName是上传成功后的音频文件名称
}).then((res) => {
if (res.code === 200) {
this.$message.success("上传成功");
this.showUpload = false; // 显示上传组件
this.getList(); // 刷新列表数据
} else {
this.$message.error(res.msg);
}
});
}
},
},
filters: {
SoundNamefilter(value) {
if (!value) return ""; // 如果值为空,返回空字符串
return value.split("_")[0]; // 返回文件名部分
},
},
};
</script>
<style scoped lang="scss">
.upload-demo {
display: inline-block;
}
</style>
...@@ -120,8 +120,6 @@ export default { ...@@ -120,8 +120,6 @@ export default {
}, },
mounted() {}, mounted() {},
methods: { methods: {
//编辑
tableEdit(row) {},
//获取列表 //获取列表
getList() { getList() {
getAlgorithmList(this.queryParams).then((res) => { getAlgorithmList(this.queryParams).then((res) => {
...@@ -146,7 +144,7 @@ export default { ...@@ -146,7 +144,7 @@ export default {
handleEditConfirm() { handleEditConfirm() {
this.$refs.form.validate((valid) => { this.$refs.form.validate((valid) => {
if (!valid) return; if (!valid) return;
updateAlgorithm(data).then((res) => { updateAlgorithm(this.form).then((res) => {
if (res.code === 200) { if (res.code === 200) {
this.$modal.msgSuccess("修改成功"); this.$modal.msgSuccess("修改成功");
this.getList(); this.getList();
......
...@@ -126,8 +126,8 @@ ...@@ -126,8 +126,8 @@
<template slot-scope="scope"> <template slot-scope="scope">
<image-preview <image-preview
:src="scope.row.cameraImage" :src="scope.row.cameraImage"
:width="100" :width="60"
:height="60" :height="40"
/> />
</template> </template>
</el-table-column> </el-table-column>
......
...@@ -5,24 +5,541 @@ ...@@ -5,24 +5,541 @@
height="1080" height="1080"
:fullScreen="true" :fullScreen="true"
> >
<div class="">这里是大屏</div> <div class="screen-box1">
<div class="screen-title">
<span>智慧矿山AI推理平台</span>
</div>
<div class="screen-content">
<el-row>
<el-col :span="5" style="padding-right: 10px">
<el-button type="text" class="back-btn" @click="goTo">
<i class="el-icon-setting el-icon-arrow-left"></i> 管理后台&gt;
</el-button>
<div class="left-top">
<span>
<svg-icon icon-class="camera" class="on" />
<p>在线监控:{{ CameraOnlineData.on }}</p>
</span>
<span>
<svg-icon icon-class="camera" class="off" />
<p>离线监控:{{ CameraOnlineData.off }}</p>
</span>
</div>
<div class="left-content">
<div class="chart-title"></div>
<PieChartScreen
:pieData="VideoAnalysisOnlineData"
v-if="isDataLoaded"
width="100%"
height="298px"
/>
</div>
<div class="left-bottom">
<div class="left-bottom-txt">
<div>
<span>今日累计报警</span>
<p>{{ alarmStatisticsData.dayAlarm }}</p>
</div>
<div>
<span>报警已处理</span>
<p>{{ alarmStatisticsData.onAlarm }}</p>
</div>
<div>
<span>报警未处理</span>
<p>{{ alarmStatisticsData.offAlarm }}</p>
</div>
</div>
<img src="../../assets/images/UI_34.png" alt="" />
</div>
</el-col>
<el-col :span="14" style="padding: 0 10px">
<div class="content-top">
<div class="content-top-title">
<!-- <el-select
class="playsource"
v-model="selectVidoe"
value-key="id"
placeholder=""
size="small"
>
<el-option
v-for="item in videoList"
:key="item.id"
:label="item.name"
:value="item"
></el-option>
</el-select> -->
</div>
<!-- <VideoPlay
:videoID="'video'"
:rtspIp="selectVidoe.url"
:videoShow="videoShow"
class="video-box"
></VideoPlay> -->
<div class="video-box" ref="videoContainer">
<iframe
:src="selectVidoe.ifarme.url"
:style="{
width: width + 'px',
height: height + 'px',
border: 'none',
}"
ref="analysisIframe"
></iframe>
</div>
</div>
<div class="content-bottom">
<div class="chart-title">
<el-date-picker
v-model="pickerDate"
class="chart-picker"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
@change="getData"
>
</el-date-picker>
</div>
<div class="chart-box">
<LineChartScreen
:chartData="StatisticsData"
v-if="isDataLoaded"
width="100%"
height="297px"
/>
</div>
</div>
</el-col>
<el-col :span="5" style="padding-left: 10px">
<div class="right">
<el-button type="text" class="right-btn" @click="lookDetails">
详情<i class="el-icon-plus"></i>
</el-button>
<div class="alarm-group">
<div
class="alarm-item"
v-for="item of alarmList"
:key="item.logId"
>
<el-row>
<el-col :span="12">
<p>
<dict-tag
:options="dict.type.algorithm_level"
:value="item.alarmLevel"
class="alarm-dict"
/>
<span>{{ item.algorithmName }}</span>
</p>
<p>{{ item.cameraName }}</p>
<p>{{ item.alarmTime }}</p>
</el-col>
<el-col :span="12">
<image-preview
:src="item.alarmImageUrl"
class="alarm-img"
/>
</el-col>
</el-row>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
</v-scale-screen> </v-scale-screen>
</template> </template>
<script> <script>
import PieChartScreen from "../dashboard/PieChartScreen.vue";
import LineChartScreen from "../dashboard/LineChartScreen.vue";
import VideoPlay from "@/components/VideoPlay/videoPlay.vue";
import { getCameraList } from "@/api/business/cameraconfig.js";
import {
getCameraOnlineData,
getVideoAnalysisOnlineData,
getStatisticsData,
getAlarmStatisticsData,
} from "@/api/business/home.js";
import { getAlarmLogList } from "@/api/business/alarmlog.js";
export default { export default {
dicts: ["algorithm_level"],
name: "Screen", name: "Screen",
props: {}, props: {},
components: {}, components: { PieChartScreen, VideoPlay, LineChartScreen },
data() { data() {
return {}; return {
selectVidoe: {}, // 选择视频
videoList: [],
webRtcServer: null, //webRtcServer上下文
videoShow: false, //视频是否显示
isDataLoaded: false, // 新增加载状态
CameraOnlineData: {
on: 0,
off: 0,
},
VideoAnalysisOnlineData: [],
StatisticsData: {},
pickerDate: [],
alarmList: [],
alarmStatisticsData: {
dayAlarm: 0,
offAlarm: 0,
onAlarm: 0,
},
width: 0,
height: 0,
resizeObserver: null,
};
}, },
computed: {}, computed: {},
watch: {}, watch: {},
created() {}, created() {
mounted() {}, // 设置最近7天为默认值
methods: {}, const end = new Date();
const start = new Date();
start.setDate(start.getDate() - 14);
// 添加日期格式化
const formatDate = (date) => {
return `${date.getFullYear()}-${(date.getMonth() + 1)
.toString()
.padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
};
this.pickerDate = [formatDate(start), formatDate(end)];
this.init();
this.getAlarmLog();
},
mounted() {
this.$nextTick(() => {
// 初始化观察器
this.resizeObserver = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
this.width = width;
this.height = height;
// 传递尺寸到iframe内部(需同源)
this.$refs.analysisIframe.contentWindow.postMessage(
{
type: "resize",
width,
height,
},
"*"
);
});
this.resizeObserver.observe(this.$refs.videoContainer);
});
},
beforeDestroy() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
},
methods: {
async init() {
// 使用Promise.all等待所有请求完成
await Promise.all([
getCameraList().then((res) => {
if (res.code !== 200) return;
this.videoList = res.rows.map((item) => ({
id: item.cameraId,
name: `${item.cameraName}-${item.cameraPosition}`,
url: item.cameraAddress,
}));
this.selectVidoe = this.videoList[0];
this.videoShow = true;
}),
getCameraOnlineData().then((res) => {
if (res.code !== 200) return;
this.CameraOnlineData = { off: res.data.off, on: res.data.on };
}),
getVideoAnalysisOnlineData().then((res) => {
if (res.code !== 200) return;
this.VideoAnalysisOnlineData = [
{ value: res.data.off, name: "未启动" },
{ value: res.data.on, name: "已启动" },
];
}),
getStatisticsData({
startTime: this.pickerDate[0],
endTime: this.pickerDate[this.pickerDate.length - 1],
}).then((res) => {
if (res.code !== 200) return;
this.StatisticsData = res.data;
this.pickerDate = [
res.data.xdata[0],
res.data.xdata[res.data.xdata.length - 1],
];
}),
getAlarmStatisticsData().then((res) => {
if (res.code !== 200) return;
this.alarmStatisticsData = res.data;
}),
]);
// 所有请求完成后设置加载完成状态
this.$nextTick(() => {
this.isDataLoaded = true;
});
},
getData() {
getStatisticsData({
startTime: this.pickerDate[0],
endTime: this.pickerDate[this.pickerDate.length - 1],
}).then((res) => {
if (res.code !== 200) return;
this.StatisticsData = res.data;
});
},
getAlarmLog() {
getAlarmLogList({}).then((res) => {
if (res.code !== 200) return;
this.alarmList = res.rows;
});
},
goTo() {
this.$router.push({ path: "/index" });
},
lookDetails() {
console.log("查看详情");
},
},
}; };
</script> </script>
<style scoped lang="scss"></style> <style scoped lang="scss">
.screen-box1 {
width: 100%;
height: 100%;
background: url("../../assets/images/screenbg.jpg") no-repeat center;
background-size: 100% 100%;
.screen-title {
width: 100%;
height: 95px;
text-align: center;
font-size: 30px;
font-weight: 550;
line-height: 45px;
color: #fff;
padding-top: 15px;
font-family: Franklin Gothic Medium, Arial Narrow, Arial, sans-serif;
}
.screen-content {
position: relative;
padding: 10px;
height: calc(100% - 115px);
.back-btn {
font-size: 22px !important;
color: white !important;
position: absolute;
top: -70px;
left: 15px;
}
.left-top {
width: 100%;
height: 279px;
background: url("../../assets/images/lefttop.png") no-repeat center;
background-size: 100% 100%;
margin-bottom: 10px;
font-size: 22px;
color: white;
font-weight: 550;
font-family: Franklin Gothic Medium, Arial Narrow, Arial, sans-serif;
display: flex;
// ... 其他样式保持不变 ...
justify-content: center; // 改为居中对齐
gap: 50px; // 新增间距控制
align-items: center; // 垂直居中对齐
span {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 50px;
p {
margin-top: 10px;
}
.on {
color: #184ff0;
font-size: 46px;
}
.off {
color: #d1d6eb;
font-size: 46px;
}
}
}
.left-content {
width: 100%;
height: 338px;
background: url("../../assets/images/leftcontent.png") no-repeat center;
background-size: 100% 100%;
margin-bottom: 10px;
.chart-title {
width: 100%;
height: 40px;
}
}
.left-bottom {
width: 100%;
height: 290px;
background: url("../../assets/images/leftbottom.png") no-repeat center;
background-size: 100% 100%;
position: relative;
overflow: hidden;
img {
width: 100%;
margin-top: 140px;
}
.left-bottom-txt {
width: 100%;
height: 140px;
font-size: 18px;
display: flex;
justify-content: center;
gap: 25px;
align-items: center;
color: white;
text-align: center;
position: absolute;
top: 80px;
p {
font-size: 30px;
}
}
}
.content-top {
width: 100%;
height: 580px;
background: url("../../assets/images/contenttop.png") no-repeat center;
background-size: 100% 100%;
margin-bottom: 10px;
padding: 20px;
position: relative;
overflow: hidden; // 新增溢出控制
box-sizing: border-box; // 新增盒模型修正
.content-top-title {
width: calc(100% - 40px);
height: 40px;
position: absolute;
top: 18px;
z-index: 1000;
background: url("../../assets/images/contenttop_title.png") no-repeat;
display: flex; // 新增flex布局
justify-content: flex-end; // 新增右对齐
.playsource {
width: 200px; // 新增宽度设置
margin-top: 2px;
::v-deep .el-input__inner {
background-color: #0e4ab3; // 新增背景色
color: white; // 新增文字颜色
border-color: #409eff; // 保持原有边框色
}
}
}
.video-box {
width: 100%;
// height: calc(100%-40px);
height: 100%; // 修复计算表达式(原值缺少空格)
position: relative;
flex-shrink: 0; // 新增弹性收缩限制
}
}
.content-bottom {
width: 100%;
height: 337px;
background: url("../../assets/images/contentbottom.png") no-repeat center;
background-size: 100% 100%;
.chart-title {
width: 100%;
height: 40px;
display: flex; // 新增flex布局
justify-content: flex-end; // 右对齐
padding: 2px 4px;
::v-deep .el-date-editor {
background-color: #0e4ab3; // 半透明白底
.el-range-input {
background: transparent;
color: white; // 文字白色
}
}
}
.chart-box {
width: 100%;
height: calc(100% - 38px);
}
}
.right {
width: 100%;
height: 928px;
background: url("../../assets/images/right.png") no-repeat center;
background-size: 100% 100%;
position: relative;
.right-btn {
font-size: 20px !important;
color: white !important;
position: absolute;
margin-left: 15px;
right: 0px;
top: -4px;
}
.alarm-group {
padding-top: 45px;
width: 100%;
overflow: hidden;
.alarm-item {
width: calc(100% - 2px);
margin-left: 1px;
height: 140px;
background: #000348;
background-size: 100% 100%;
padding: 15px;
color: #fff;
margin-bottom: 5px;
p {
margin: 0;
margin-bottom: 15px;
.alarm-dict {
display: inline-block;
margin-right: 5px;
}
}
.alarm-img {
width: 170px;
height: 110px;
}
&:hover {
width: 100%;
height: 160px;
background: url("../../assets/images/spkuang_01.png") no-repeat
center;
background-size: 100% 100%;
margin-left: 0;
cursor: pointer;
p {
margin: 0;
margin-bottom: 15px;
.alarm-dict {
display: block;
margin-right: 5px;
margin-bottom: 15px;
}
}
.alarm-img {
width: 170px;
height: 130px;
}
}
}
}
}
}
}
</style>
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<div class="Pd20"> <div class="Pd20">
<el-row type="flex" :gutter="10"> <el-row type="flex" :gutter="10">
<el-col :span="6"> <el-col :span="6">
<el-button type="primary" plain size="mini" @click="drawer = true"> <el-button type="primary" plain size="mini" @click="append">
<i class="el-icon-plus el-icon--left"></i> <i class="el-icon-plus el-icon--left"></i>
创建视频分析任务 创建视频分析任务
</el-button> </el-button>
...@@ -216,6 +216,15 @@ export default { ...@@ -216,6 +216,15 @@ export default {
); );
}, },
}, },
// 在beforeDestroy生命周期添加
beforeDestroy() {
// 清理图表实例
this.chartInstance?.dispose();
// 释放图片资源
document.querySelectorAll(".content-top, .content-bottom").forEach((el) => {
el.style.backgroundImage = "none";
});
},
}; };
</script> </script>
...@@ -238,4 +247,11 @@ export default { ...@@ -238,4 +247,11 @@ export default {
::v-deep .el-drawer__header { ::v-deep .el-drawer__header {
margin-bottom: 0; margin-bottom: 0;
} }
.content-top {
background: url("../../assets/images/contenttop.png") no-repeat center;
will-change: transform; // 提示浏览器优化
}
.chart-box {
transform: translateZ(0);
}
</style> </style>
...@@ -26,7 +26,11 @@ ...@@ -26,7 +26,11 @@
</p> </p>
</span> </span>
<div> <div>
<el-checkbox-group v-model="checkAnalysisList" size="small"> <el-checkbox-group
v-model="checkAnalysisList"
size="small"
:key="uniqueKey"
>
<!-- 自定义复选框组件 --> <!-- 自定义复选框组件 -->
<div <div
class="custom-checkbox" class="custom-checkbox"
...@@ -36,8 +40,8 @@ ...@@ -36,8 +40,8 @@
), ),
}" }"
@click="handleCheckboxChange(item)" @click="handleCheckboxChange(item)"
v-for="(item, index) in analysisList" v-for="item in analysisList"
:key="index" :key="item.algorithmId"
> >
<div class="checkbox-inner"> <div class="checkbox-inner">
<i <i
...@@ -124,16 +128,27 @@ ...@@ -124,16 +128,27 @@
> >
<div class="checkbox-content"> <div class="checkbox-content">
<p>{{ item.algorithmName }}</p> <p>{{ item.algorithmName }}</p>
<el-tag <!-- 替换el-tag为原生标签 -->
<div
v-for="tag in item.checkCameraList" v-for="tag in item.checkCameraList"
:key="tag.cameraName" :key="tag.cameraId"
closable class="custom-tag"
size="mini" :class="[tag.type]"
@close="deleteTag(item, tag)"
:type="tag.type"
> >
{{ tag.name }} <span>{{ tag.cameraName }}</span>
</el-tag> <button
class="close-btn"
@click="setUpCamera(item, tag)"
style="maring-right: 10px"
>
<i class="el-icon-s-tools"></i>
</button>
<button class="close-btn" @click="deleteTag(item, tag)">
<i class="el-icon-delete" style="color: red"></i>
</button>
</div>
<!-- 原有其他代码保持不变... -->
<p> <p>
<span>ID : {{ item.algorithmId }} </span> <span>ID : {{ item.algorithmId }} </span>
<span>版本 {{ item.algorithmVersion }} </span> <span>版本 {{ item.algorithmVersion }} </span>
...@@ -145,12 +160,88 @@ ...@@ -145,12 +160,88 @@
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
<el-dialog
title="算法配置"
:visible.sync="cameraDialogVisible"
append-to-body
width="30%"
>
<div class="camera-dialog-title">
<div>
<span>算法名称</span>
<p>{{ algorithmDiaData.algorithmName }}</p>
</div>
<div>
<span>摄像头</span>
<p>{{ cameraDiaData.cameraName }}</p>
</div>
</div>
<el-row type="flex" align="middle" class="compact-row">
<el-col :span="4">
<div class="compact-label">
<span>算法阈值</span>
<el-tooltip content="阈值取数为整数" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</div>
</el-col>
<el-col :span="20">
<el-input
v-model="cameraDiaData.interval"
size="mini"
placeholder="输入阈值"
class="compact-input"
/>
</el-col>
</el-row>
<el-row type="flex" align="middle" class="compact-row">
<el-col :span="4">
<div class="compact-label">检测区域</div>
</el-col>
<el-col :span="20">
<el-radio-group
v-model="cameraDiaData.useRoi"
size="mini"
class="compact-radio"
v-if="cameraDialogVisible"
>
<el-radio label="false" class="custom-radio">全图</el-radio>
<el-radio label="true" class="custom-radio">自定义</el-radio>
</el-radio-group>
</el-col>
</el-row>
<Draw
v-if="cameraDiaData.useRoi === 'true'"
:callback="getLocalInfo"
:val="cameraDiaData.roiPoints"
:cameraId="cameraDiaData.cameraId"
></Draw>
<div class="camera-dialog-footer">
<el-button
size="small"
class="dialog-btn cancel-btn"
@click="cameraDialogVisible = false"
>
取消
</el-button>
<el-button
type="primary"
size="small"
class="dialog-btn confirm-btn"
@click="handleSaveConfig"
>
保存配置
</el-button>
</div>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import { getAlgorithmList } from "@/api/business/algorithmconfig.js"; import { getAlgorithmList } from "@/api/business/algorithmconfig.js";
import { getCameraList } from "@/api/business/cameraconfig.js"; import { getCameraList } from "@/api/business/cameraconfig.js";
import Draw from "./draw.vue";
import { import {
addVideoAnalysisTask, addVideoAnalysisTask,
updateVideoAnalysisTask, updateVideoAnalysisTask,
...@@ -163,7 +254,7 @@ export default { ...@@ -163,7 +254,7 @@ export default {
default: () => null, default: () => null,
}, },
}, },
components: {}, components: { Draw },
data() { data() {
return { return {
form: { form: {
...@@ -186,6 +277,10 @@ export default { ...@@ -186,6 +277,10 @@ export default {
cameraList: [], cameraList: [],
checkCameraList: [], // 选中的摄像头 checkCameraList: [], // 选中的摄像头
selectedAlgorithmIndex: -1, // 当前选中算法索引 selectedAlgorithmIndex: -1, // 当前选中算法索引
cameraDialogVisible: false, // 摄像头配置弹窗
algorithmDiaData: {}, // 算法配置弹窗数据
cameraDiaData: {}, // 摄像头配置弹窗数据
uniqueKey: Math.random().toString(36).substr(2, 9), // 生成随机唯一键
}; };
}, },
computed: {}, computed: {},
...@@ -254,11 +349,13 @@ export default { ...@@ -254,11 +349,13 @@ export default {
return originItem return originItem
? { ? {
...originItem, ...originItem,
checkCameraList: item.cameraList.map((cameraId) => ({ checkCameraList: item.cameraList.map((camera) => ({
cameraId: String(cameraId), cameraId: String(camera.cameraId), // 直接使用传入的摄像头ID
name: this.cameraList.find((cam) => cam.cameraId == cameraId) cameraName: camera.cameraName, // 直接使用传入的摄像头名称
?.cameraName,
type: "success", type: "success",
interval: camera.interval, // 使用传入的阈值
useRoi: camera.useRoi, // 使用传入的ROI配置
roiPoints: camera.roiPoints, // 使用传入的坐标点
})), })),
} }
: null; : null;
...@@ -300,8 +397,11 @@ export default { ...@@ -300,8 +397,11 @@ export default {
if (currentAlgorithm) { if (currentAlgorithm) {
currentAlgorithm.checkCameraList = val.map((item) => ({ currentAlgorithm.checkCameraList = val.map((item) => ({
cameraId: item.cameraId, cameraId: item.cameraId,
name: item.cameraName, cameraName: item.cameraName,
type: "success", type: "success",
interval: "", //阈值
useRoi: "false", //全图/自定义
roiPoints: "", //坐标系
})); }));
} }
}, },
...@@ -322,8 +422,12 @@ export default { ...@@ -322,8 +422,12 @@ export default {
} }
} }
}, },
//提交 //设置摄像头
setUpCamera(item, tag) {
this.cameraDialogVisible = true;
this.algorithmDiaData = { ...item };
this.cameraDiaData = { ...tag };
},
// 在提交时触发验证 // 在提交时触发验证
submitForm(taskStatus) { submitForm(taskStatus) {
console.log(this.editData); console.log(this.editData);
...@@ -336,9 +440,7 @@ export default { ...@@ -336,9 +440,7 @@ export default {
taskName: this.form.videoTasksName, taskName: this.form.videoTasksName,
algorithmList: this.checkAnalysisList.map((algorithm) => ({ algorithmList: this.checkAnalysisList.map((algorithm) => ({
algorithmId: algorithm.algorithmId, algorithmId: algorithm.algorithmId,
cameraList: algorithm.checkCameraList.map( cameraList: algorithm.checkCameraList.map((camera) => camera),
(camera) => camera.cameraId
),
})), })),
}; };
...@@ -358,6 +460,31 @@ export default { ...@@ -358,6 +460,31 @@ export default {
} }
}); });
}, },
// 获取字符串坐标信息
getLocalInfo(str) {
this.cameraDiaData.roiPoints = str;
},
//阈值弹窗保存配置
handleSaveConfig() {
// 优化后的代码
const matchedAlgorithm = this.checkAnalysisList.find(
(alg) => alg.algorithmId === this.algorithmDiaData.algorithmId
);
const matchedCamera = matchedAlgorithm?.checkCameraList.find(
(cam) => cam.cameraId === this.cameraDiaData.cameraId
);
if (matchedCamera) {
Object.assign(matchedCamera, {
interval: this.cameraDiaData.interval ?? matchedCamera.interval,
useRoi: this.cameraDiaData.useRoi ?? matchedCamera.useRoi,
roiPoints: this.cameraDiaData.roiPoints || matchedCamera.roiPoints,
});
}
// 关闭弹窗
this.cameraDialogVisible = false;
},
}, },
}; };
</script> </script>
...@@ -447,4 +574,136 @@ export default { ...@@ -447,4 +574,136 @@ export default {
color: rgb(24, 114, 240); color: rgb(24, 114, 240);
font-size: 25px; font-size: 25px;
} }
// 新增自定义标签样式
.custom-tag {
display: inline-flex;
align-items: center;
height: 24px;
padding: 0 8px;
margin: 2px;
font-size: 12px;
border-radius: 4px;
transition: 0.3s;
// 不同类型样式
&.success {
background-color: #f0f9eb;
border: 1px solid #e1f3d8;
color: #67c23a;
}
&.warning {
background-color: #fdf6ec;
border: 1px solid #faecd8;
color: #e6a23c;
}
&.danger {
background-color: #fef0f0;
border: 1px solid #fde2e2;
color: #f56c6c;
}
.close-btn {
margin-left: 6px;
border: none;
background: none;
color: inherit;
cursor: pointer;
font-size: 14px;
line-height: 1;
padding: 0 2px;
&:hover {
opacity: 0.8;
}
}
}
.camera-dialog-title {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 16px;
text-align: center;
background: #d3d0cd;
border-radius: 4px;
padding: 10px;
div {
flex: 1;
p {
font-size: 20px;
color: #606266;
font-weight: bold;
}
}
}
.compact-row {
margin-bottom: 12px;
padding: 8px 12px;
background: #f8fafc;
border-radius: 4px;
}
.compact-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #909399;
}
.compact-input {
::v-deep .el-input__inner {
border-radius: 15px;
padding: 0 12px;
}
}
.compact-radio {
::v-deep .el-radio-button {
margin-right: 8px;
&.is-active {
.el-radio-button__inner {
background: #409eff;
border-color: #409eff;
box-shadow: none;
}
}
}
}
.camera-dialog-footer {
margin-top: 20px;
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 0 0;
border-top: 1px solid #ebeef5;
.dialog-btn {
padding: 7px 15px;
border-radius: 4px;
font-weight: 500;
transition: all 0.3s;
&.cancel-btn {
background: #f5f7fa;
border-color: #dcdfe6;
color: #606266;
&:hover {
background: #f0f6ff;
border-color: #c6e2ff;
color: #409eff;
}
}
&.confirm-btn {
background: #409eff;
border-color: #409eff;
&:hover {
background: #66b1ff;
border-color: #66b1ff;
}
}
}
}
</style> </style>
<template>
<div>
<div>
<div class="draw-tool">
<div class="tool-group">
<div
@click="!isCapturing && handleCaptureVideoImg()"
:class="{ 'loading-state': isCapturing }"
>
<i class="el-icon-camera-solid" style="font-size: 26px"></i>
<br />
<span>获取抓图</span>
<div v-if="isCapturing" class="capture-loading">
<i class="el-icon-loading"></i>
</div>
</div>
<div @click="isTranslate = false">
<!-- <el-icon :size="26">
<Close />
</el-icon> -->
<svg-icon icon-class="polygon" style="font-size: 26px" />
<br />
<span>多边形</span>
</div>
<div @click="handleResetDraw">
<!-- <el-icon :size="26">
<Refresh />
</el-icon> -->
<svg-icon
icon-class="goback"
style="font-size: 26px; color: '#606266'"
/>
<br />
<span>撤销</span>
</div>
<div @click="handleDrawClean">
<!-- <el-icon :size="26">
<Delete />
</el-icon> -->
<i class="el-icon-brush" style="font-size: 26px"></i>
<br />
<span>清空</span>
</div>
<el-upload
:action="serviceUrl + '/common/upload'"
:limit="1"
:headers="headers"
:show-file-list="false"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
>
<div class="upload-btn">
<i class="el-icon-picture" style="font-size: 26px"></i>
<br />
<span>上传图片</span>
</div>
</el-upload>
</div>
</div>
<div id="DrawWrap">
<canvas id="DrawCompoent" @click="handleClickCanvas"></canvas>
</div>
</div>
</div>
</template>
<script>
import { getToken } from "@/utils/auth";
import { getVideoAnalysisTaskScreenshot } from "@/api/business/videoanalysistasks.js";
export default {
components: {},
props: {
val: {
type: String,
required: true,
default: "",
},
callback: {
type: Function,
required: true,
},
cameraId: {
type: String,
required: true,
default: "",
},
},
data() {
return {
serviceUrl: process.env.VUE_APP_BASE_API,
imgURL: "",
nodeStr: "",
node: [],
canvasBoxConf: {
width: 200,
height: 200,
el: null,
},
isTranslate: false,
fileList: [],
canvasContext: null,
headers: {
Authorization: "Bearer " + getToken(),
},
isCapturing: false, // 新增加载状态
};
},
created() {
this.handleCaptureVideoImg();
},
methods: {
parseCoordinates(str) {
const regex = /\((\d+),(\d+)\)/g;
const coordinates = [];
let match;
while ((match = regex.exec(str)) !== null) {
coordinates.push({
x: parseInt(match[1], 10),
y: parseInt(match[2], 10),
});
}
return coordinates;
},
autoComputePercentage() {
const _w = 1920;
const _h = 1080;
let _str = "";
const { width, height } = this.canvasBoxConf;
this.node.forEach((info) => {
_str += `(${((info.x / width) * _w).toFixed(0)},${(
(info.y / height) *
_h
).toFixed(0)}),`;
});
return _str.slice(0, -1);
},
beforeUpload(file) {
const isImage = ["image/jpeg", "image/png", "image/jpg"].includes(
file.type
);
if (!isImage) {
this.$modal.msgError("仅支持JPEG/JPG/PNG格式");
return false;
}
return true;
},
handleUploadSuccess(res) {
if (res.code === 200) {
this.updateImageSource(res.url);
}
},
handleCaptureVideoImg() {
this.isCapturing = true; // 开始加载
this.$nextTick(() => {
getVideoAnalysisTaskScreenshot(this.cameraId)
.then((res) => {
if (res.code === 200) {
// 创建新的Image对象并等待加载
this.$modal.msgSuccess("截图抓取成功");
this.updateImageSource("data:image/jpg;base64," + res.data);
}
})
.finally(() => {
this.isCapturing = false; // 无论成功失败都关闭加载
});
});
},
updateImageSource(url) {
const newImg = new Image();
newImg.src = url;
newImg.onload = () => {
// 图片加载完成后再更新imgURL并绘制
this.imgURL = newImg.src;
this.$nextTick(() => {
this.initCanvasImg(() => {
this.drawCanvas();
});
});
};
// 处理加载错误的情况
newImg.onerror = () => {
console.error("图片加载失败");
this.$modal.msgError("图片加载失败");
};
},
handleClickCanvas(e) {
if (this.isTranslate) return;
// 获取canvas元素
const canvas = e.target;
const rect = canvas.getBoundingClientRect();
// 计算缩放比例
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
// 计算精确坐标
const offsetX = (e.clientX - rect.left) * scaleX;
const offsetY = (e.clientY - rect.top) * scaleY;
// 多边形闭合判断
if (this.node.length > 2) {
const first = this.node[0];
const last = this.node[this.node.length - 1];
if (first.x === last.x && first.y === last.y) {
this.$modal.msgWarning("多边形已闭合,不可继续绘制");
return;
}
}
// 更新节点列表
this.node.push({ x: offsetX, y: offsetY });
// 绘制画布
this.drawCanvas();
// 回调坐标计算
this.callback(this.autoComputePercentage());
// this.callback(this.autoComputePercentage());
},
updateNodeList(e) {
const { offsetX, offsetY } = e;
const curNode = { x: offsetX, y: offsetY };
if (this.node.length > 2) {
const lastNode = this.node[0];
if (
Math.abs(offsetX - lastNode.x) < 15 &&
Math.abs(offsetY - lastNode.y) < 15
) {
this.node.push(lastNode);
return;
}
}
this.node.push(curNode);
},
drawCanvas() {
const ctx = this.canvasContext;
// 添加空数组检查
if (this.node.length === 0) return;
const drawLine = (prev, next) => {
ctx.beginPath();
ctx.moveTo(prev.x, prev.y);
ctx.lineTo(next.x, next.y);
ctx.strokeStyle = "red";
ctx.lineWidth = 5;
ctx.stroke();
};
// 添加初始值参数
this.node.reduce((prev, next) => {
drawLine(prev, next);
return next;
}, this.node[0]); // 使用数组第一个元素作为初始值
},
initCanvasImg(callback) {
const ctx = this.canvasContext;
// 先清空画布
ctx.clearRect(0, 0, this.canvasBoxConf.width, this.canvasBoxConf.height);
const img = new Image();
img.crossOrigin = "anonymous"; // 添加跨域属性
img.src = this.imgURL;
img.onload = () => {
// 重新设置canvas尺寸
ctx.canvas.width = img.naturalWidth;
ctx.canvas.height = img.naturalHeight;
ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height);
if (callback) callback();
};
},
handleDrawClean() {
if (this.node.length === 0) {
this.$modal.msgWarning("当前画布为空");
return;
}
this.canvasContext.clearRect(
0,
0,
this.canvasBoxConf.width,
this.canvasBoxConf.height
);
this.initCanvasImg();
this.node = [];
},
handleResetDraw() {
if (this.node.length === 0) {
this.$modal.msgWarning("无可撤销内容");
return;
}
this.node.pop();
this.initCanvasImg(() => this.drawCanvas());
},
},
mounted() {
const canvas = document.getElementById("DrawCompoent");
const canvasWrap = document.getElementById("DrawWrap");
// 获取容器实际显示尺寸
const rect = canvasWrap.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
// 设置canvas物理像素尺寸
canvas.width = Math.floor(rect.width * dpr);
canvas.height = Math.floor(rect.height * dpr);
// 设置CSS显示尺寸
canvas.style.width = rect.width + "px";
canvas.style.height = rect.height + "px";
// 初始化坐标系
const ctx = canvas.getContext("2d");
ctx.scale(dpr, dpr);
// 保存canvas配置
this.canvasContext = ctx;
this.canvasBoxConf = {
width: rect.width,
height: rect.height,
el: canvas,
dpr: dpr,
};
// this.fempegVideoInit();
this.nodeStr = this.val;
if (this.nodeStr) {
this.node = this.parseCoordinates(this.nodeStr).map((item) => ({
x: ((item.x / 1920) * this.canvasBoxConf.width).toFixed(0),
y: ((item.y / 1080) * this.canvasBoxConf.height).toFixed(0),
}));
}
this.initCanvasImg(() => this.drawCanvas());
},
};
</script>
<style lang="scss" scoped>
#canvas-4 {
position: fixed;
top: -10000px;
}
#canvas-4 > canvas {
width: 600px;
height: 350px;
}
.draw-tool {
height: 70px;
margin: 20px 0;
display: flex;
padding-top: 5px;
justify-content: space-between;
background: #f8f8f9;
border-radius: 10px;
.tool-group {
display: flex;
* {
user-select: none; // 禁止文字选中
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
> div {
width: 80px;
text-align: center;
cursor: pointer;
padding: 8px;
font-size: 14px;
&:hover {
background: #f0f7ff;
}
}
.upload-btn {
width: 80px;
text-align: center;
cursor: pointer;
font-size: 14px;
&:hover {
background: #f0f7ff;
}
}
}
}
#DrawCompoent {
height: 100%;
width: 100%;
background-color: #f8f8f9;
}
#DrawWrap {
height: 300px;
position: relative;
transition: all 0.2s;
}
.loading-state {
position: relative;
cursor: wait !important;
opacity: 0.7;
}
.capture-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 20px;
color: #409eff;
}
</style>
<template> <template>
<div :class="className" :style="{height:height,width:width}" /> <div :class="className" :style="{ height: height, width: width }" />
</template> </template>
<script> <script>
import * as echarts from 'echarts' import * as echarts from "echarts";
require('echarts/theme/macarons') // echarts theme require("echarts/theme/macarons"); // echarts theme
import resize from './mixins/resize' import resize from "./mixins/resize";
export default { export default {
mixins: [resize], mixins: [resize],
props: { props: {
className: { className: {
type: String, type: String,
default: 'chart' default: "chart",
}, },
width: { width: {
type: String, type: String,
default: '100%' default: "100%",
}, },
height: { height: {
type: String, type: String,
default: '350px' default: "350px",
}, },
autoResize: { autoResize: {
type: Boolean, type: Boolean,
default: true default: true,
}, },
chartData: { chartData: {
type: Object, type: Object,
required: true required: true,
} },
}, },
data() { data() {
return { return {
chart: null chart: null,
} };
}, },
watch: { watch: {
chartData: { chartData: {
deep: true, deep: true,
handler(val) { handler(val) {
this.setOptions(val) this.setOptions(val);
} },
} },
}, },
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
this.initChart() this.initChart();
}) });
}, },
beforeDestroy() { beforeDestroy() {
if (!this.chart) { if (!this.chart) {
return return;
} }
this.chart.dispose() this.chart.dispose();
this.chart = null this.chart = null;
}, },
methods: { methods: {
initChart() { initChart() {
this.chart = echarts.init(this.$el, 'macarons') this.chart = echarts.init(this.$el, "macarons");
this.setOptions(this.chartData) this.setOptions(this.chartData);
}, },
setOptions({ expectedData, actualData } = {}) { setOptions({ xdata, ydata } = {}) {
let list = ydata.map((item) => {
return {
name: item.name,
smooth: true,
type: "line",
itemStyle: {
normal: {
color: "#3888fa",
lineStyle: {
color: "#3888fa",
width: 2,
},
areaStyle: {
color: "#f3f8ff",
},
},
},
data: item.data,
animationDuration: 2800,
animationEasing: "quadraticOut",
};
});
let lendata = ydata.map((item) => {
return item.name;
});
this.chart.setOption({ this.chart.setOption({
xAxis: { xAxis: {
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], data: xdata,
boundaryGap: false, boundaryGap: false,
axisTick: { axisTick: {
show: false show: false,
} },
}, },
grid: { grid: {
left: 10, left: 10,
right: 10, right: 40,
bottom: 20, bottom: 20,
top: 30, top: 30,
containLabel: true containLabel: true,
}, },
tooltip: { tooltip: {
trigger: 'axis', trigger: "axis",
axisPointer: { axisPointer: {
type: 'cross' type: "cross",
}, },
padding: [5, 10] padding: [5, 10],
}, },
yAxis: { yAxis: {
axisTick: { axisTick: {
show: false show: false,
} },
}, },
legend: { legend: {
data: ['expected', 'actual'] data: lendata,
}, },
series: [{ series: list,
name: 'expected', itemStyle: { });
normal: { },
color: '#FF005A', },
lineStyle: { };
color: '#FF005A',
width: 2
}
}
},
smooth: true,
type: 'line',
data: expectedData,
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: 'actual',
smooth: true,
type: 'line',
itemStyle: {
normal: {
color: '#3888fa',
lineStyle: {
color: '#3888fa',
width: 2
},
areaStyle: {
color: '#f3f8ff'
}
}
},
data: actualData,
animationDuration: 2800,
animationEasing: 'quadraticOut'
}]
})
}
}
}
</script> </script>
<template>
<div :class="className" :style="{ height: height, width: width }" />
</template>
<script>
import * as echarts from "echarts";
require("echarts/theme/macarons"); // echarts theme
import resize from "./mixins/resize";
// 柔和颜色随机
const getRandomColor = () => {
// 定义艳丽颜色的RGB范围
const min = 0;
const max = 255;
// 生成三个在指定范围内的随机数,分别代表R、G、B
const r = Math.floor(Math.random() * (max - min + 1)) + min;
const g = Math.floor(Math.random() * (max - min + 1)) + min;
const b = Math.floor(Math.random() * (max - min + 1)) + min;
// 为了确保颜色清新,适当调整色差
const adjustColor = (color) => {
if (Math.random() > 0.5) {
return Math.min(255, color + Math.floor((255 - color) * 0.2));
} else {
return Math.max(0, color - Math.floor(color * 0.2));
}
};
const adjustedR = adjustColor(r);
const adjustedG = adjustColor(g);
const adjustedB = adjustColor(b);
// 确保颜色组合舒适自然
const ensureComfortable = (r, g, b) => {
// 计算颜色的亮度
const brightness = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
// 如果亮度太低或太高,调整其中一个颜色分量
if (brightness < 0.3) {
return [Math.min(255, r + 50), g, b];
} else if (brightness > 0.7) {
return [Math.max(0, r - 50), g, b];
}
return [r, g, b];
};
const [finalR, finalG, finalB] = ensureComfortable(
adjustedR,
adjustedG,
adjustedB
);
// 将RGB值转换为16进制字符串
const hexR = finalR.toString(16).padStart(2, "0");
const hexG = finalG.toString(16).padStart(2, "0");
const hexB = finalB.toString(16).padStart(2, "0");
// 返回带有#前缀的十六进制颜色字符串
return `#${hexR}${hexG}${hexB}`;
};
export default {
mixins: [resize],
props: {
className: {
type: String,
default: "chart",
},
width: {
type: String,
default: "100%",
},
height: {
type: String,
default: "350px",
},
autoResize: {
type: Boolean,
default: true,
},
chartData: {
type: Object,
required: true,
},
},
data() {
return {
chart: null,
};
},
watch: {
chartData: {
deep: true,
handler(val) {
this.setOptions(val);
},
},
},
mounted() {
this.$nextTick(() => {
this.initChart();
});
},
beforeDestroy() {
if (!this.chart) {
return;
}
this.chart.dispose();
this.chart = null;
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, "macarons");
this.setOptions(this.chartData);
},
setOptions({ xdata, ydata } = {}) {
let list = ydata.map((item) => {
return {
name: item.name,
smooth: true,
type: "line",
itemStyle: {
normal: {
color: getRandomColor(), // 修改这里使用随机颜色
lineStyle: {
color: getRandomColor(), // 修改这里使用随机颜色
width: 2,
},
areaStyle: {
color: "#f3f8ff",
},
},
},
data: item.data,
animationDuration: 2800,
animationEasing: "quadraticOut",
};
});
let lendata = ydata.map((item) => {
return item.name;
});
this.chart.setOption({
xAxis: {
data: xdata,
boundaryGap: false,
axisTick: {
show: false,
},
},
grid: {
left: 10,
right: 40,
bottom: 20,
top: 30,
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
padding: [5, 10],
},
yAxis: {
axisTick: {
show: false,
},
},
legend: {
data: lendata,
},
series: list,
});
},
},
};
</script>
...@@ -22,6 +22,31 @@ export default { ...@@ -22,6 +22,31 @@ export default {
type: String, type: String,
default: "300px", default: "300px",
}, },
pieData: {
type: Array,
default: function () {
return [
{ value: 0, name: "已启动" },
{ value: 0, name: "未启动" },
];
},
},
},
watch: {
pieData: {
handler(newVal, oldVal) {
if (this.chart) {
this.chart.setOption({
series: [
{
data: newVal,
},
],
});
}
},
deep: true,
},
}, },
data() { data() {
return { return {
...@@ -61,11 +86,8 @@ export default { ...@@ -61,11 +86,8 @@ export default {
type: "pie", type: "pie",
radius: ["45%", "75%"], radius: ["45%", "75%"],
center: ["50%", "55%"], center: ["50%", "55%"],
color: ["#67C23A", "#F56C6C"], // 新增颜色数组配置 color: ["#F56C6C", "#67C23A"], // 新增颜色数组配置
data: [ data: this.pieData,
{ value: 320, name: "已启动" },
{ value: 240, name: "未启动" },
],
avoidLabelOverlap: false, avoidLabelOverlap: false,
itemStyle: { itemStyle: {
borderRadius: 10, borderRadius: 10,
......
<template>
<div :class="className" :style="{ height: height, width: width }" />
</template>
<script>
import * as echarts from "echarts";
require("echarts/theme/macarons"); // echarts theme
import resize from "./mixins/resize";
export default {
mixins: [resize],
props: {
className: {
type: String,
default: "chart",
},
width: {
type: String,
default: "100%",
},
height: {
type: String,
default: "300px",
},
pieData: {
type: Array,
default: function () {
return [
{ value: 0, name: "已启动" },
{ value: 0, name: "未启动" },
];
},
},
},
watch: {
pieData: {
handler(newVal, oldVal) {
if (this.chart) {
this.chart.setOption({
series: [
{
data: newVal,
},
],
});
}
},
deep: true,
},
},
data() {
return {
chart: null,
};
},
mounted() {
this.$nextTick(() => {
this.initChart();
});
},
beforeDestroy() {
if (!this.chart) {
return;
}
this.chart.dispose();
this.chart = null;
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, "macarons");
this.chart.setOption({
tooltip: {
trigger: "item",
formatter: "{a} <br/>{b} : {c} ({d}%)",
},
legend: {
left: "center",
bottom: "1%",
data: ["已启动", "未启动"],
textStyle: {
color: "#fff", // 设置图例文字颜色为白色
},
},
series: [
{
name: "监控摄像头",
type: "pie",
color: ["#c9cee2", "#184ff0"], // 新增颜色数组配置
data: this.pieData,
avoidLabelOverlap: false,
emphasis: {
label: {
show: true,
fontSize: 40,
fontWeight: "bold",
},
},
label: {
position: "center",
},
},
],
});
},
},
};
</script>
<template> <template>
<div class="app-container"> <div class="app-container">
<el-row :gutter="15" type="flex"> <el-row :gutter="15" type="flex">
<el-col :span="5"> <el-col :span="18">
<div class="status-container"> <el-row :gutter="15" type="flex">
<h3 class="status-title">定位状态</h3> <el-col :span="6">
<div class="status-group"> <div class="status-container">
<div class="status-item"> <h3 class="status-title">定位状态</h3>
<span class="count online">18</span> <div class="status-group">
<div class="label"><svg-icon icon-class="camera" />在线数量</div> <div class="status-item">
<span class="count online">{{ CameraOnlineData.on }}</span>
<div class="label">
<svg-icon icon-class="camera" />在线数量
</div>
</div>
<div class="status-item">
<span class="count offline">{{ CameraOnlineData.off }}</span>
<div class="label">
<svg-icon icon-class="camera" />离线数量
</div>
</div>
</div>
</div> </div>
<div class="status-item"> <div class="status-container">
<span class="count offline">18</span> <h3 class="status-title">视频分析任务启动状态</h3>
<div class="label"><svg-icon icon-class="camera" />离线数量</div> <div class="status-group">
<div class="status-item" style="height: 300px">
<pie-chart
:option="VideoAnalysisOnlineData"
v-if="isDataLoaded"
/>
</div>
</div>
</div> </div>
</div> </el-col>
</div> <el-col :span="18">
<div class="status-container"> <div class="status-container">
<h3 class="status-title">视频分析任务启动状态</h3> <h3 class="status-title both-end">
<div class="status-group"> <span
<div class="status-item" style="height: 300px"> ><span>{{ selectVidoe.name }}</span
<pie-chart /> ><span>正在查看</span></span
>
<el-select v-model="selectVidoe" value-key="id" placeholder="">
<el-option
v-for="item in videoList"
:key="item.id"
:label="item.name"
:value="item"
></el-option>
</el-select>
</h3>
<div class="video-box" ref="videoContainer">
<iframe
:src="iframeSrc"
:style="{
width: width + 'px',
height: height + 'px',
border: 'none',
}"
ref="analysisIframe"
></iframe>
</div>
</div> </div>
</div> </el-col>
</div> </el-row>
<el-row :gutter="15" type="flex">
<el-col :span="24">
<div class="status-container">
<h3 class="status-title both-end">
<span>统计分析(条/天)</span>
<el-date-picker
v-model="pickerDate"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
@change="getData"
>
</el-date-picker>
</h3>
<line-chart :chartData="StatisticsData" v-if="isDataLoaded" />
</div>
</el-col>
</el-row>
</el-col> </el-col>
<el-col :span="13"> <el-col :span="6">
<div class="status-container"> <div class="status-container h100">
<h3 class="status-title both-end"> <h3 class="status-title both-end">
<span <span>实时报警</span>
><span>{{ selectVidoe.name }}</span
><span>正在查看</span></span
>
<el-select v-model="selectVidoe" value-key="url" placeholder="">
<el-option
v-for="item in videoList"
:key="item.id"
:label="item.name"
:value="item"
></el-option>
</el-select>
</h3> </h3>
<div class="video-box"></div> <div
class="card-box"
v-for="item of alarmList"
:key="item.logId"
@click="lookAlarm(item)"
>
<el-row>
<el-col :span="12">
<dict-tag
:options="dict.type.algorithm_level"
:value="item.alarmLevel"
/>
<p>{{ item.algorithmName }}</p>
<p>{{ item.cameraName }}</p>
<p>{{ item.alarmTime }}</p>
</el-col>
<el-col :span="12">
<image-preview
:src="item.alarmImageUrl"
:width="170"
:height="145"
/>
</el-col>
</el-row>
</div>
</div> </div>
</el-col> </el-col>
<el-col :span="6">
<el-card
shadow="hover"
:body-style="{ padding: '20px', height: 'calc(100vh - 200px)' }"
>
</el-card>
</el-col>
</el-row> </el-row>
<el-dialog :visible.sync="dialogVisible" width="60%">
<div slot="title" class="dialog-title">
<span>
<dict-tag
style="display: inline-block; margin-right: 10px"
:options="dict.type.algorithm_level"
:value="currentAlarm.alarmLevel"
/>
<span style="margin-right: 20px">{{
(currentAlarm && currentAlarm.alarmMessage) || "报警详情"
}}</span>
<span style="margin-right: 20px">
{{ currentAlarm && currentAlarm.cameraPosition }}</span
>
<span style="margin-right: 20px">{{
currentAlarm && currentAlarm.alarmTime
}}</span>
</span>
<el-button
@click="playBack"
:disabled="currentAlarm.videoStatus === 0"
type="text"
class="right-btn"
>
<i
v-if="lookBackVideo === 0"
:class="
currentAlarm.videoStatus === 1
? 'el-icon-video-play'
: 'el-icon-loading'
"
></i>
{{
lookBackVideo === 1
? "查看图片"
: currentAlarm.videoStatus === 1
? "查看回放"
: "视频加载中"
}}
</el-button>
</div>
<div v-if="currentAlarm">
<img
v-if="lookBackVideo === 0"
:src="currentAlarm.alarmImageUrl"
style="width: 100%; height: 400px"
/>
<video
v-if="lookBackVideo === 1"
:src="currentAlarm.videoBackUrl"
style="width: 100%; height: 400px"
controls
autoplay
></video>
</div>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import PieChart from "./dashboard/PieChart.vue"; import PieChart from "./dashboard/PieChart.vue";
import LineChart from "./dashboard/LineChart.vue";
import { getCameraList } from "@/api/business/cameraconfig.js";
import {
getCameraOnlineData,
getVideoAnalysisOnlineData,
getStatisticsData,
} from "@/api/business/home.js";
import { getAlarmLogList } from "@/api/business/alarmlog.js";
export default { export default {
dicts: ["algorithm_level"],
name: "Index", name: "Index",
components: { PieChart }, components: { PieChart, LineChart },
data() { data() {
return { return {
selectVidoe: {}, // 选择视频 selectVidoe: {}, // 选择视频
videoList: [ videoList: [],
// 视频列表 webRtcServer: null, //webRtcServer上下文
{ name: "视频1", url: "URL_ADDRESS ", id: 1 }, videoShow: false, //视频是否显示
{ name: "视频2", url: "URL_ADDRESS:8080/video1.mp4", id: 2 }, isDataLoaded: false, // 新增加载状态
{ name: "视频3", url: "URL_ADDRESSlhost:8080/video1.mp4", id: 3 }, CameraOnlineData: {
], on: 0,
off: 0,
},
VideoAnalysisOnlineData: [],
StatisticsData: {},
pickerDate: [],
alarmList: [],
// 新增弹窗相关数据
dialogVisible: false,
currentAlarm: {},
lookBackVideo: 0, // 0-图片 1-视频
iframeSrc: "http://192.168.2.22:5000/view/17/42", // 根据实际路径修改
width: 0,
height: 0,
resizeObserver: null,
}; };
}, },
methods: {}, created() {
// 设置最近7天为默认值
const end = new Date();
const start = new Date();
start.setDate(start.getDate() - 14);
// 添加日期格式化
const formatDate = (date) => {
return `${date.getFullYear()}-${(date.getMonth() + 1)
.toString()
.padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
};
this.pickerDate = [formatDate(start), formatDate(end)];
this.init();
this.getAlarmLog();
},
methods: {
async init() {
// 使用Promise.all等待所有请求完成
await Promise.all([
getCameraList().then((res) => {
if (res.code !== 200) return;
this.videoList = res.rows.map((item) => ({
id: item.cameraId,
name: `${item.cameraName}-${item.cameraPosition}`,
url: item.cameraAddress,
}));
this.selectVidoe = this.videoList[0];
this.videoShow = true;
}),
getCameraOnlineData().then((res) => {
if (res.code !== 200) return;
this.CameraOnlineData = { off: res.data.off, on: res.data.on };
}),
getVideoAnalysisOnlineData().then((res) => {
if (res.code !== 200) return;
this.VideoAnalysisOnlineData = [
{ value: res.data.off, name: "未启动" },
{ value: res.data.on, name: "已启动" },
];
}),
getStatisticsData({
startTime: this.pickerDate[0],
endTime: this.pickerDate[this.pickerDate.length - 1],
}).then((res) => {
if (res.code !== 200) return;
this.StatisticsData = res.data;
this.pickerDate = [
res.data.xdata[0],
res.data.xdata[res.data.xdata.length - 1],
];
}),
]);
// 所有请求完成后设置加载完成状态
this.$nextTick(() => {
this.isDataLoaded = true;
});
},
getData() {
getStatisticsData({
startTime: this.pickerDate[0],
endTime: this.pickerDate[this.pickerDate.length - 1],
}).then((res) => {
if (res.code !== 200) return;
this.StatisticsData = res.data;
});
},
getAlarmLog() {
getAlarmLogList({}).then((res) => {
if (res.code !== 200) return;
this.alarmList = res.rows;
});
},
lookAlarm(item) {
this.currentAlarm = item;
this.lookBackVideo = 0; // 默认显示图片
this.dialogVisible = true;
},
playBack() {
this.lookBackVideo = this.lookBackVideo === 0 ? 1 : 0;
},
},
mounted() {
this.$nextTick(() => {
// 初始化观察器
this.resizeObserver = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
this.width = width;
this.height = height;
// 传递尺寸到iframe内部(需同源)
this.$refs.analysisIframe.contentWindow.postMessage(
{
type: "resize",
width,
height,
},
"*"
);
});
this.resizeObserver.observe(this.$refs.videoContainer);
});
},
beforeDestroy() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null; // 重要!解除引用
}
},
}; };
</script> </script>
...@@ -89,6 +348,9 @@ export default { ...@@ -89,6 +348,9 @@ export default {
border-radius: 5px; border-radius: 5px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
margin-bottom: 20px; margin-bottom: 20px;
&.h100 {
height: 100vh;
}
.status-title { .status-title {
margin: 0 0 20px 0; margin: 0 0 20px 0;
font-size: 16px; font-size: 16px;
...@@ -104,6 +366,14 @@ export default { ...@@ -104,6 +366,14 @@ export default {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.card-box {
border-radius: 5px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
width: 100%;
padding: 15px;
margin-top: 10px;
cursor: pointer;
}
.status-group { .status-group {
display: flex; display: flex;
gap: 20px; gap: 20px;
...@@ -137,7 +407,39 @@ export default { ...@@ -137,7 +407,39 @@ export default {
.video-box { .video-box {
height: 440px; height: 440px;
width: 100%; width: 100%;
background: #409eff; position: relative;
flex-shrink: 0; // 禁止弹性收缩
overflow: hidden; // 添加溢出隐藏
iframe {
width: 100%;
height: 100%;
}
.loading-overlay,
.error-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
i {
font-size: 40px;
}
&-text {
font-size: 14px;
}
}
.error-overlay {
color: #f56c6c;
}
}
}
.dialog-title {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
.right-btn {
margin-right: 40px;
} }
} }
} }
......
<template> <template>
<div class="login"> <div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form"> <el-form
<h3 class="title">{{title}}</h3> ref="loginForm"
:model="loginForm"
:rules="loginRules"
class="login-form"
>
<h3 class="title">{{ title }}</h3>
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input
v-model="loginForm.username" v-model="loginForm.username"
...@@ -9,7 +14,11 @@ ...@@ -9,7 +14,11 @@
auto-complete="off" auto-complete="off"
placeholder="账号" placeholder="账号"
> >
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" /> <svg-icon
slot="prefix"
icon-class="user"
class="el-input__icon input-icon"
/>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="password"> <el-form-item prop="password">
...@@ -20,7 +29,11 @@ ...@@ -20,7 +29,11 @@
placeholder="密码" placeholder="密码"
@keyup.enter.native="handleLogin" @keyup.enter.native="handleLogin"
> >
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" /> <svg-icon
slot="prefix"
icon-class="password"
class="el-input__icon input-icon"
/>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="code" v-if="captchaEnabled"> <el-form-item prop="code" v-if="captchaEnabled">
...@@ -31,32 +44,42 @@ ...@@ -31,32 +44,42 @@
style="width: 63%" style="width: 63%"
@keyup.enter.native="handleLogin" @keyup.enter.native="handleLogin"
> >
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" /> <svg-icon
slot="prefix"
icon-class="validCode"
class="el-input__icon input-icon"
/>
</el-input> </el-input>
<div class="login-code"> <div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/> <img :src="codeUrl" @click="getCode" class="login-code-img" />
</div> </div>
</el-form-item> </el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox> <el-checkbox
<el-form-item style="width:100%;"> v-model="loginForm.rememberMe"
style="margin: 0px 0px 25px 0px"
>记住密码</el-checkbox
>
<el-form-item style="width: 100%">
<el-button <el-button
:loading="loading" :loading="loading"
size="medium" size="medium"
type="primary" type="primary"
style="width:100%;" style="width: 100%"
@click.native.prevent="handleLogin" @click.native.prevent="handleLogin"
> >
<span v-if="!loading">登 录</span> <span v-if="!loading">登 录</span>
<span v-else>登 录 中...</span> <span v-else>登 录 中...</span>
</el-button> </el-button>
<div style="float: right;" v-if="register"> <div style="float: right" v-if="register">
<router-link class="link-type" :to="'/register'">立即注册</router-link> <router-link class="link-type" :to="'/register'"
>立即注册</router-link
>
</div> </div>
</el-form-item> </el-form-item>
</el-form> </el-form>
<!-- 底部 --> <!-- 底部 -->
<div class="el-login-footer"> <div class="el-login-footer">
<span>Copyright © 2018-2025 ruoyi.vip All Rights Reserved.</span> <span></span>
</div> </div>
</div> </div>
</template> </template>
...@@ -64,7 +87,7 @@ ...@@ -64,7 +87,7 @@
<script> <script>
import { getCodeImg } from "@/api/login"; import { getCodeImg } from "@/api/login";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { encrypt, decrypt } from '@/utils/jsencrypt' import { encrypt, decrypt } from "@/utils/jsencrypt";
export default { export default {
name: "Login", name: "Login",
...@@ -77,32 +100,32 @@ export default { ...@@ -77,32 +100,32 @@ export default {
password: "admin123", password: "admin123",
rememberMe: false, rememberMe: false,
code: "", code: "",
uuid: "" uuid: "",
}, },
loginRules: { loginRules: {
username: [ username: [
{ required: true, trigger: "blur", message: "请输入您的账号" } { required: true, trigger: "blur", message: "请输入您的账号" },
], ],
password: [ password: [
{ required: true, trigger: "blur", message: "请输入您的密码" } { required: true, trigger: "blur", message: "请输入您的密码" },
], ],
code: [{ required: true, trigger: "change", message: "请输入验证码" }] code: [{ required: true, trigger: "change", message: "请输入验证码" }],
}, },
loading: false, loading: false,
// 验证码开关 // 验证码开关
captchaEnabled: true, captchaEnabled: true,
// 注册开关 // 注册开关
register: false, register: false,
redirect: undefined redirect: undefined,
}; };
}, },
watch: { watch: {
$route: { $route: {
handler: function(route) { handler: function (route) {
this.redirect = route.query && route.query.redirect; this.redirect = route.query && route.query.redirect;
}, },
immediate: true immediate: true,
} },
}, },
created() { created() {
this.getCode(); this.getCode();
...@@ -110,8 +133,9 @@ export default { ...@@ -110,8 +133,9 @@ export default {
}, },
methods: { methods: {
getCode() { getCode() {
getCodeImg().then(res => { getCodeImg().then((res) => {
this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled; this.captchaEnabled =
res.captchaEnabled === undefined ? true : res.captchaEnabled;
if (this.captchaEnabled) { if (this.captchaEnabled) {
this.codeUrl = "data:image/gif;base64," + res.img; this.codeUrl = "data:image/gif;base64," + res.img;
this.loginForm.uuid = res.uuid; this.loginForm.uuid = res.uuid;
...@@ -121,38 +145,46 @@ export default { ...@@ -121,38 +145,46 @@ export default {
getCookie() { getCookie() {
const username = Cookies.get("username"); const username = Cookies.get("username");
const password = Cookies.get("password"); const password = Cookies.get("password");
const rememberMe = Cookies.get('rememberMe') const rememberMe = Cookies.get("rememberMe");
this.loginForm = { this.loginForm = {
username: username === undefined ? this.loginForm.username : username, username: username === undefined ? this.loginForm.username : username,
password: password === undefined ? this.loginForm.password : decrypt(password), password:
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe) password === undefined ? this.loginForm.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe),
}; };
}, },
handleLogin() { handleLogin() {
this.$refs.loginForm.validate(valid => { this.$refs.loginForm.validate((valid) => {
if (valid) { if (valid) {
this.loading = true; this.loading = true;
if (this.loginForm.rememberMe) { if (this.loginForm.rememberMe) {
Cookies.set("username", this.loginForm.username, { expires: 30 }); Cookies.set("username", this.loginForm.username, { expires: 30 });
Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 }); Cookies.set("password", encrypt(this.loginForm.password), {
Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 }); expires: 30,
});
Cookies.set("rememberMe", this.loginForm.rememberMe, {
expires: 30,
});
} else { } else {
Cookies.remove("username"); Cookies.remove("username");
Cookies.remove("password"); Cookies.remove("password");
Cookies.remove('rememberMe'); Cookies.remove("rememberMe");
} }
this.$store.dispatch("Login", this.loginForm).then(() => { this.$store
this.$router.push({ path: this.redirect || "/" }).catch(()=>{}); .dispatch("Login", this.loginForm)
}).catch(() => { .then(() => {
this.loading = false; this.$router.push({ path: this.redirect || "/" }).catch(() => {});
if (this.captchaEnabled) { })
this.getCode(); .catch(() => {
} this.loading = false;
}); if (this.captchaEnabled) {
this.getCode();
}
});
} }
}); });
} },
} },
}; };
</script> </script>
...@@ -162,8 +194,11 @@ export default { ...@@ -162,8 +194,11 @@ export default {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100%; height: 100%;
background-image: url("../assets/images/login-background.jpg"); background: url("../assets/images/login-bg.png") no-repeat;
background-size: cover; background-size: cover;
image-rendering: -webkit-optimize-contrast; // 优化渲染
backface-visibility: hidden; // 开启硬件加速
transform: translateZ(0);
} }
.title { .title {
margin: 0px auto 30px auto; margin: 0px auto 30px auto;
......
...@@ -45,6 +45,14 @@ module.exports = { ...@@ -45,6 +45,14 @@ module.exports = {
pathRewrite: { pathRewrite: {
['^' + process.env.VUE_APP_BASE_API]: '' ['^' + process.env.VUE_APP_BASE_API]: ''
} }
},
'/proxy-iframe': {
target: 'http://192.168.2.22:5000', // 替换为实际目标地址
changeOrigin: true,
pathRewrite: {
'^/proxy-iframe': '' // 替换为实际路径前缀
}
} }
}, },
disableHostCheck: true disableHostCheck: true
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment