first commit
This commit is contained in:
110
src/components/Common/flv-player.vue
Normal file
110
src/components/Common/flv-player.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<!-- flv视频流播放 -->
|
||||
<div>
|
||||
<video
|
||||
v-if="show"
|
||||
id="videoElement"
|
||||
:controls="true"
|
||||
autoplay
|
||||
style="width: 100%; height: 100%; object-fit: fill;">
|
||||
</video>
|
||||
<el-empty v-else :image="getAssetsFile('lbtmenu/icon-UAV-inner.png')" description="无人机暂未起飞" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref, defineExpose, nextTick
|
||||
} from 'vue'
|
||||
import flvjs from 'flv.js'
|
||||
import { getAssetsFile } from '@/utils/common.js'
|
||||
|
||||
const emit = defineEmits([ 'toggleShow' ])
|
||||
let flvPlayer = null
|
||||
const show = ref(false)
|
||||
const clear = () => {
|
||||
if (flvPlayer) {
|
||||
flvPlayer.unload()
|
||||
flvPlayer.detachMediaElement()
|
||||
flvPlayer = null
|
||||
}
|
||||
}
|
||||
const onPlay = (url) => {
|
||||
show.value = true
|
||||
nextTick(() => {
|
||||
if (flvjs.isSupported()) {
|
||||
const videoElement = document.getElementById('videoElement')
|
||||
clear()
|
||||
flvPlayer = flvjs.createPlayer(
|
||||
{
|
||||
type: 'flv',
|
||||
isLive: true,
|
||||
hasAudio: false,
|
||||
url
|
||||
},
|
||||
{
|
||||
// 启用IO隐藏缓冲区
|
||||
// 如果需要实时(最小延迟)来进行实时流播放,则设置为false
|
||||
// 但是如果网络抖动,则可能会停顿
|
||||
enableStashBuffer: false,
|
||||
// 之时IO暂存缓冲区的初始大小,默认值为384kb,指出合适的尺寸可以改善视频负载/搜索时间
|
||||
stashInitialSize: 128
|
||||
}
|
||||
)
|
||||
flvPlayer.attachMediaElement(videoElement)
|
||||
flvPlayer.load()
|
||||
flvPlayer.play()
|
||||
emit('toggleShow', show.value)
|
||||
flvPlayer.on('error', () => {
|
||||
console.log('errorrrrrrrrrrrrrrrrrrrrrrrr')
|
||||
show.value = false
|
||||
flvPlayer.pause()
|
||||
if (flvPlayer) {
|
||||
flvPlayer = null
|
||||
}
|
||||
emit('toggleShow', show.value)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
defineExpose({
|
||||
onPlay
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep .el-empty__image{
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
:deep .el-empty__description p{
|
||||
color:#fff;
|
||||
}
|
||||
/* 播放按钮 */
|
||||
video::-webkit-media-controls-play-button {
|
||||
display: none;
|
||||
}
|
||||
/* 进度条 */
|
||||
video::-webkit-media-controls-timeline {
|
||||
display: none;
|
||||
}
|
||||
/* 观看的当前时间 */
|
||||
video::-webkit-media-controls-current-time-display{
|
||||
display: none;
|
||||
}
|
||||
/* 剩余时间 */
|
||||
video::-webkit-media-controls-time-remaining-display {
|
||||
display: none;
|
||||
}
|
||||
/* 音量按钮 */
|
||||
video::-webkit-media-controls-mute-button {
|
||||
display: none;
|
||||
}
|
||||
video::-webkit-media-controls-toggle-closed-captions-button {
|
||||
display: none;
|
||||
}
|
||||
/* 音量的控制条 */
|
||||
video::-webkit-media-controls-volume-slider {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
332
src/components/Common/video-comp-back.vue
Normal file
332
src/components/Common/video-comp-back.vue
Normal file
@@ -0,0 +1,332 @@
|
||||
<template>
|
||||
|
||||
<!-- 绑定父组件class:videoWindow -->
|
||||
<div
|
||||
:id="playWndId"
|
||||
ref="video-preview">
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint-disable */
|
||||
|
||||
export default {
|
||||
name: 'video-preview',
|
||||
data() {
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
secret: '',
|
||||
oWebControl: null,
|
||||
initCount: 0,
|
||||
appkey: import.meta.env.VITE_APP_HAIKANG_APPKEY,
|
||||
ip: import.meta.env.VITE_APP_HAIKANG_IP,
|
||||
port: parseInt(import.meta.env.VITE_APP_HAIKANG_PORT),
|
||||
playMode: 1,
|
||||
pubKey: '',
|
||||
isInit: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
layout: {
|
||||
type: String,
|
||||
default: () => '1x1'
|
||||
},
|
||||
cameraIndexCode: {},
|
||||
timeOut: {
|
||||
type: Number,
|
||||
default: () => 500
|
||||
},
|
||||
videoWindowClassName: {
|
||||
type: String,
|
||||
default: "videoWindow",
|
||||
required: false
|
||||
},
|
||||
playWndId: {
|
||||
type: String,
|
||||
default: "playWnd",
|
||||
required: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
var that = this
|
||||
this.$nextTick(function() {
|
||||
that.windowChange()
|
||||
setTimeout(() => {
|
||||
that.initPlugin()
|
||||
}, that.timeOut)
|
||||
window.addEventListener('resize', function() {
|
||||
that.onResize()
|
||||
})
|
||||
})
|
||||
},
|
||||
destroyed() {
|
||||
this.uninit()
|
||||
},
|
||||
methods: {
|
||||
onResize(change=true){
|
||||
let that = this
|
||||
if (that.oWebControl) {
|
||||
if(change){
|
||||
that.windowChange()
|
||||
}
|
||||
that.oWebControl.JS_Resize(that.width, that.height)
|
||||
}
|
||||
},
|
||||
wait (fn, timeout, tick) {
|
||||
timeout = timeout || 5000;
|
||||
tick = tick || 250;
|
||||
var timeoutTimer = null;
|
||||
var execTimer = null;
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
|
||||
timeoutTimer = setTimeout(function() {
|
||||
clearTimeout(execTimer);
|
||||
reject(new Error('polling fail because timeout'));
|
||||
}, timeout);
|
||||
|
||||
tickHandler(fn);
|
||||
|
||||
function tickHandler(fn) {
|
||||
var ret = fn();
|
||||
if (!ret) {
|
||||
execTimer = setTimeout(function() {
|
||||
tickHandler(fn);
|
||||
}, tick)
|
||||
} else {
|
||||
clearTimeout(timeoutTimer);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
// 初始化plugin
|
||||
initPlugin() {
|
||||
let that = this
|
||||
this.oWebControl = new WebControl({
|
||||
szPluginContainer: this.playWndId,
|
||||
iServicePortStart: 15900,
|
||||
iServicePortEnd: 15909,
|
||||
szClassId: '23BF3B0A-2C56-4D97-9C03-0CB103AA8F11', // 用于IE10使用ActiveX的clsid
|
||||
cbConnectSuccess: function() {
|
||||
that.setCallbacks();
|
||||
//实例创建成功后需要启动服务
|
||||
that.oWebControl
|
||||
.JS_StartService("window", {
|
||||
dllPath: "./VideoPluginConnect.dll"
|
||||
})
|
||||
.then(
|
||||
function() {
|
||||
that.oWebControl
|
||||
.JS_CreateWnd(that.playWndId, that.width, that.height)
|
||||
.then(function() {
|
||||
//JS_CreateWnd创建视频播放窗口,宽高可设定
|
||||
that.init(); //创建播放实例成功后初始化
|
||||
});
|
||||
},
|
||||
function() {}
|
||||
);
|
||||
},
|
||||
cbConnectError: function() {
|
||||
that.oWebControl = null;
|
||||
$("#" + that.playWndId).html("插件未启动,正在尝试启动,请稍候...");
|
||||
WebControl.JS_WakeUp("VideoWebPlugin://"); //程序未启动时执行error函数,采用wakeup来启动程序
|
||||
initCount++;
|
||||
if (initCount < 3) {
|
||||
setTimeout(function() {
|
||||
that.initPlugin();
|
||||
}, 2000);
|
||||
} else {
|
||||
$("#" + that.playWndId).html("插件启动失败,请检查插件是否安装!");
|
||||
}
|
||||
},
|
||||
cbConnectClose: function(bNormalClose) {
|
||||
that.oWebControl = null
|
||||
}
|
||||
})
|
||||
},
|
||||
init() {
|
||||
|
||||
let that = this;
|
||||
that.getPubKey(function() {
|
||||
////////////////////////////////// 请自行修改以下变量值 ////////////////////////////////////
|
||||
var snapDir = "d:\\SnapDir"; //抓图存储路径
|
||||
var videoDir = "d:\\VideoDir"; //紧急录像或录像剪辑存储路径
|
||||
var layout = "1x1"; //playMode指定模式的布局
|
||||
var enableHTTPS = 1; //是否启用HTTPS协议与综合安防管理平台交互,是为1,否为0
|
||||
var encryptedFields = "secret"; //加密字段,默认加密领域为secret
|
||||
var showToolbar = 1; //是否显示工具栏,0-不显示,非0-显示
|
||||
var showSmart = 1; //是否显示智能信息(如配置移动侦测后画面上的线框),0-不显示,非0-显示
|
||||
var buttonIDs = "0,16,256,257,258,259,260,512,515,516,517,768,769"; //自定义工具条按钮
|
||||
////////////////////////////////// 请自行修改以上变量值 ////////////////////////////////////
|
||||
that.secret = that.setEncrypt(import.meta.env.VITE_APP_HAIKANG_SECRET);
|
||||
that.oWebControl
|
||||
.JS_RequestInterface({
|
||||
funcName: "init",
|
||||
argument: JSON.stringify({
|
||||
appkey: that.appkey, //API网关提供的appkey
|
||||
secret: that.secret, //API网关提供的secret
|
||||
ip: that.ip, //API网关IP地址
|
||||
playMode: that.playMode, //播放模式(决定显示预览还是回放界面)
|
||||
port: that.port, //端口
|
||||
snapDir: snapDir, //抓图存储路径
|
||||
videoDir: videoDir, //紧急录像或录像剪辑存储路径
|
||||
layout: layout, //布局
|
||||
enableHTTPS: enableHTTPS, //是否启用HTTPS协议
|
||||
encryptedFields: encryptedFields, //加密字段
|
||||
showToolbar: showToolbar, //是否显示工具栏
|
||||
showSmart: showSmart, //是否显示智能信息
|
||||
buttonIDs: buttonIDs //自定义工具条按钮
|
||||
})
|
||||
})
|
||||
.then(function(oData) {
|
||||
that.oWebControl.JS_Resize(that.width, that.height); // 初始化后resize一次,规避firefox下首次显示窗口后插件窗口未与DIV窗口重合问题
|
||||
});
|
||||
});
|
||||
},
|
||||
// 获取公钥
|
||||
getPubKey(callback) {
|
||||
var that = this
|
||||
this.oWebControl.JS_RequestInterface({
|
||||
funcName: 'getRSAPubKey',
|
||||
argument: JSON.stringify({
|
||||
keyLength: 1024
|
||||
})
|
||||
}).then(function(oData) {
|
||||
if (oData.responseMsg.data) {
|
||||
that.pubKey = oData.responseMsg.data
|
||||
that.isInit = true
|
||||
callback()
|
||||
}
|
||||
})
|
||||
},
|
||||
// 设置窗口控制回调
|
||||
setCallbacks() {
|
||||
let that = this;
|
||||
that.oWebControl.JS_SetWindowControlCallback({
|
||||
cbIntegrationCallBack: function (oData) {
|
||||
if(oData.responseMsg.msg.result === 816){
|
||||
that.$emit('close')
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
// RSA加密
|
||||
setEncrypt(value) {
|
||||
let that = this;
|
||||
var encrypt = new JSEncrypt();
|
||||
encrypt.setPublicKey(that.pubKey);
|
||||
return encrypt.encrypt(value);
|
||||
},
|
||||
ready() {
|
||||
let that = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.wait(
|
||||
function() {
|
||||
return that.isInit;
|
||||
},
|
||||
6000 + that.timeOut || 500,
|
||||
100
|
||||
)
|
||||
.then(function() {
|
||||
resolve();
|
||||
})
|
||||
.catch(function(err) {
|
||||
// that.$Message.info({
|
||||
// content: "视频控件加载超时,请检查:" + err,
|
||||
// duration: 3
|
||||
// });
|
||||
// reject("视频控件加载超时,请检查:" + err);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
playBack(codes, startTime, endTime) {
|
||||
let that = this;
|
||||
const startTimeStamp = this.timrStr2Stamp(startTime) + "";
|
||||
const endTimeStamp = this.timrStr2Stamp(endTime) + "";
|
||||
var recordLocation = 0; //录像存储位置:0-中心存储,1-设备存储
|
||||
var transMode = 1; //传输协议:0-UDP,1-TCP
|
||||
var gpuMode = 0; //是否启用GPU硬解,0-不启用,1-启用
|
||||
var wndId = -1; //播放窗口序号(在2x2以上布局下可指定播放窗口)
|
||||
that.oWebControl
|
||||
.JS_RequestInterface({
|
||||
funcName: "startPlayback",
|
||||
argument: JSON.stringify({
|
||||
cameraIndexCode: codes, //监控点编号
|
||||
startTimeStamp: startTimeStamp, //录像查询开始时间戳,单位:秒
|
||||
endTimeStamp: endTimeStamp, //录像结束开始时间戳,单位:秒
|
||||
recordLocation: recordLocation, //录像存储类型:0-中心存储,1-设备存储
|
||||
transMode: transMode, //传输协议:0-UDP,1-TCP
|
||||
gpuMode: gpuMode, //是否启用GPU硬解,0-不启用,1-启用
|
||||
wndId: wndId //可指定播放窗口
|
||||
})
|
||||
})
|
||||
.then(res => {
|
||||
console.log("回放的参数", res);
|
||||
});
|
||||
},
|
||||
timrStr2Stamp(str) {
|
||||
let date = new Date(str);
|
||||
let time = date.getTime();
|
||||
return time / 1000;
|
||||
},
|
||||
stopPlayBack() {
|
||||
this.oWebControl.JS_RequestInterface({
|
||||
funcName: "stopAllPlayback"
|
||||
});
|
||||
},
|
||||
uninit() {
|
||||
console.log(999);
|
||||
|
||||
let that = this;
|
||||
if (that.oWebControl != null) {
|
||||
that.oWebControl.JS_RequestInterface({
|
||||
funcName: "stopAllPreview"
|
||||
});
|
||||
that.oWebControl.JS_HideWnd(); // 先让窗口隐藏,规避可能的插件窗口滞后于浏览器消失问题
|
||||
that.oWebControl.JS_Disconnect().then(
|
||||
function() {
|
||||
// 断开与插件服务连接成功
|
||||
},
|
||||
function() {
|
||||
// 断开与插件服务连接失败
|
||||
console.log("oWebControl close error");
|
||||
}
|
||||
);
|
||||
that.oWebControl = null;
|
||||
}
|
||||
},
|
||||
windowChange() {
|
||||
|
||||
//列表选项在下方
|
||||
this.width = document.getElementsByClassName(
|
||||
this.videoWindowClassName
|
||||
)[0].scrollWidth;
|
||||
var btnHeight = 0;
|
||||
if (document.getElementsByClassName("playBtn").length > 0) {
|
||||
btnHeight = document.getElementsByClassName("playBtn")[0].clientHeight;
|
||||
this.height =
|
||||
document.getElementsByClassName(this.videoWindowClassName)[0]
|
||||
.scrollHeight -
|
||||
btnHeight -
|
||||
10;
|
||||
} else {
|
||||
this.height = document.getElementsByClassName(
|
||||
this.videoWindowClassName
|
||||
)[0].scrollHeight;
|
||||
}
|
||||
if (document.getElementById(this.playWndId)) {
|
||||
document.getElementById(this.playWndId).style.height = this.height + "px";
|
||||
document.getElementById(this.playWndId).style.width = this.width + "px";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
397
src/components/Common/video-comp.vue
Normal file
397
src/components/Common/video-comp.vue
Normal file
@@ -0,0 +1,397 @@
|
||||
<template>
|
||||
|
||||
<!-- 绑定父组件class:videoWindow -->
|
||||
<div
|
||||
:id="`playWnd${item}`"
|
||||
ref="video-preview">
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint-disable */
|
||||
|
||||
export default {
|
||||
name: 'video-preview',
|
||||
data() {
|
||||
return {
|
||||
index: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
secret: import.meta.env.VITE_APP_HAIKANG_SECRET,
|
||||
streamMode: 0, // 主子码流标识 1:子码流 0:主码流
|
||||
transMode: 1, // 传输协议 1:TCP 0:UDP
|
||||
gpuMode: 0, // 是否启用GPU硬解
|
||||
oWebControl: {},
|
||||
initCount: 0,
|
||||
appkey: import.meta.env.VITE_APP_HAIKANG_APPKEY,
|
||||
ip: import.meta.env.VITE_APP_HAIKANG_IP,
|
||||
port: parseInt(import.meta.env.VITE_APP_HAIKANG_PORT),
|
||||
snapDir: 'D:\\SnapDir',
|
||||
videoDir: 'D:\\VideoDir',
|
||||
playMode: 0,
|
||||
szShowToolbar: 1, // 显示工具栏,
|
||||
szShowSmart: 1, // 显示智能信息=
|
||||
btIds: '0,16,256,257,258,259,260,512,513,514,515,516,517,768,769', // 工具条按钮ID集
|
||||
pubKey: '',
|
||||
enableHttps: 1, // 是否启用https
|
||||
showToolbar: 1,
|
||||
showSmart: 1,
|
||||
specialCodes: [
|
||||
],
|
||||
encryptedFields: 'secret',
|
||||
isInit: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
layout: {
|
||||
type: String,
|
||||
default: () => '1x1'
|
||||
},
|
||||
cameraIndexCode: {},
|
||||
timeOut: {
|
||||
type: Number,
|
||||
default: () => 500
|
||||
},
|
||||
item:{
|
||||
type: String,
|
||||
default: () => '0'
|
||||
},
|
||||
iframe:{
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
var that = this
|
||||
this.$nextTick(function() {
|
||||
that.windowChange()
|
||||
setTimeout(() => {
|
||||
that.initPlugin()
|
||||
}, that.timeOut)
|
||||
window.addEventListener('resize', function() {
|
||||
that.onResize()
|
||||
})
|
||||
})
|
||||
},
|
||||
destroyed() {
|
||||
this.uninit()
|
||||
},
|
||||
methods: {
|
||||
onResize(change=true){
|
||||
let that = this
|
||||
if (that.oWebControl) {
|
||||
if(change){
|
||||
that.windowChange()
|
||||
}
|
||||
that.oWebControl.JS_Resize(that.width, that.height)
|
||||
}
|
||||
},
|
||||
wait (fn, timeout, tick) {
|
||||
timeout = timeout || 5000;
|
||||
tick = tick || 250;
|
||||
var timeoutTimer = null;
|
||||
var execTimer = null;
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
|
||||
timeoutTimer = setTimeout(function() {
|
||||
clearTimeout(execTimer);
|
||||
reject(new Error('polling fail because timeout'));
|
||||
}, timeout);
|
||||
|
||||
tickHandler(fn);
|
||||
|
||||
function tickHandler(fn) {
|
||||
var ret = fn();
|
||||
if (!ret) {
|
||||
execTimer = setTimeout(function() {
|
||||
tickHandler(fn);
|
||||
}, tick)
|
||||
} else {
|
||||
clearTimeout(timeoutTimer);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
setLayout(layout = '1x1') {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isInit) {
|
||||
reject('未完成视频插件初始化')
|
||||
}
|
||||
this.oWebControl.JS_RequestInterface({
|
||||
funcName: 'setLayout',
|
||||
argument: JSON.stringify({
|
||||
'layout': layout
|
||||
})
|
||||
}).then(function(oData) {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
},
|
||||
// 初始化plugin
|
||||
initPlugin() {
|
||||
let that = this
|
||||
this.oWebControl = new WebControl({
|
||||
szPluginContainer: `playWnd${that.item}`,
|
||||
iServicePortStart: 15900,
|
||||
iServicePortEnd: 15909,
|
||||
szClassId: '23BF3B0A-2C56-4D97-9C03-0CB103AA8F11', // 用于IE10使用ActiveX的clsid
|
||||
cbConnectSuccess: function() {
|
||||
that.oWebControl.JS_SetWindowControlCallback({
|
||||
cbIntegrationCallBack: function (oData) {
|
||||
if(oData.responseMsg.msg.result === 1024){
|
||||
that.oWebControl.JS_HideWnd();
|
||||
}
|
||||
if(oData.responseMsg.msg.result === 1025){
|
||||
that.oWebControl.JS_ShowWnd();
|
||||
}
|
||||
if(oData.responseMsg.msg.result === 816){
|
||||
that.$emit('close',oData,that.item)
|
||||
}
|
||||
}
|
||||
});
|
||||
that.oWebControl.JS_StartService('window', {
|
||||
dllPath: './VideoPluginConnect.dll'
|
||||
}).then(function() {
|
||||
that.oWebControl.JS_CreateWnd(`playWnd${that.item}`, that.width, that.height).then(function() {
|
||||
console.log('视频plugin创建成功,进行interface初始化')
|
||||
// ue嵌入页面,视频窗口偏移
|
||||
if(that.iframe){
|
||||
that.oWebControl.JS_SetDocOffset ({
|
||||
left: that.iframe.left,
|
||||
top: that.iframe.top
|
||||
})
|
||||
}
|
||||
that.initInterface()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
},
|
||||
cbConnectError: function() {
|
||||
console.log('cbConnectError')
|
||||
this.oWebControl = null
|
||||
WebControl.JS_WakeUp('VideoWebPlugin://')
|
||||
that.initCount++
|
||||
if (that.initCount < 3) {
|
||||
setTimeout(that.initPlugin, 2000)
|
||||
} else {
|
||||
that.isInit = false
|
||||
that.$message.error('插件启动失败,请检查VideoWebPlugin.exe插件是否安装!')
|
||||
}
|
||||
},
|
||||
cbConnectClose: function(bNormalClose) {
|
||||
console.log('cbConnectClose')
|
||||
that.isInit = false
|
||||
that.oWebControl = null
|
||||
}
|
||||
})
|
||||
},
|
||||
setEncrypt(value) {
|
||||
// RSA加密
|
||||
var encrypt = new JSEncrypt()
|
||||
encrypt.setPublicKey(this.pubKey)
|
||||
return encrypt.encrypt(value)
|
||||
},
|
||||
//初始化interface
|
||||
initInterface() {
|
||||
let that = this
|
||||
this.btIds = '0,16,256,257,258,259,260,512,513,514,515,516,517,768,769'
|
||||
this.getPubKey(function() {
|
||||
that.oWebControl.JS_RequestInterface({
|
||||
funcName: 'init',
|
||||
argument: JSON.stringify({
|
||||
appkey: that.appkey,
|
||||
secret: that.secret,
|
||||
ip: that.ip,
|
||||
playMode: that.playMode, // 预览
|
||||
port: that.port,
|
||||
snapDir: that.snapDir,
|
||||
videoDir: that.videoDir,
|
||||
layout: that.layout,
|
||||
enableHTTPS: that.enableHttps,
|
||||
showToolbar: that.showToolbar,
|
||||
showSmart: that.showSmart,
|
||||
buttonIDs: that.btIds,
|
||||
//encryptedFields: that.encryptedFields
|
||||
})
|
||||
}).then(function(oData) {
|
||||
that.isInit = true
|
||||
that.oWebControl.JS_Resize(that.width, that.height) // 初始化后resize一次,规避firefox下首次显示窗口后插件窗口未与DIV窗口重合问题
|
||||
that.handlePreview(that.cameraIndexCode)
|
||||
})
|
||||
})
|
||||
},
|
||||
// 获取公钥
|
||||
getPubKey(callback) {
|
||||
var that = this
|
||||
this.oWebControl.JS_RequestInterface({
|
||||
funcName: 'getRSAPubKey',
|
||||
argument: JSON.stringify({
|
||||
keyLength: 1024
|
||||
})
|
||||
}).then(function(oData) {
|
||||
if (oData.responseMsg.data) {
|
||||
that.pubKey = oData.responseMsg.data
|
||||
callback()
|
||||
}
|
||||
})
|
||||
},
|
||||
multiVideos(videos) {
|
||||
let that = this
|
||||
let last = videos[videos.length - 1]
|
||||
return new Promise((resole, reject) => {
|
||||
let intervalVideo = (clips) => {
|
||||
if (clips instanceof Array && clips.length > 0) {
|
||||
let clipLength = clips.length
|
||||
let clip = clips[0]
|
||||
|
||||
// 子码流特殊处理
|
||||
let streamMode = that.streamMode
|
||||
if(that.specialCodes.indexOf(clip) > -1) streamMode = 1
|
||||
console.log(clip)
|
||||
|
||||
that.oWebControl.JS_RequestInterface({
|
||||
funcName: 'startPreview',
|
||||
argument: JSON.stringify({
|
||||
cameraIndexCode: clip,
|
||||
streamMode: streamMode,
|
||||
transMode: that.transMode,
|
||||
gpuMode: that.gpuMode,
|
||||
wndId: -1
|
||||
})
|
||||
}).then(function(oData) {
|
||||
clips.shift()
|
||||
if (oData.responseMsg.code == 0) {
|
||||
if (clips.length) {
|
||||
setTimeout(() => {
|
||||
intervalVideo(clips)
|
||||
}, 1000)
|
||||
}
|
||||
}else{
|
||||
console.log(oData)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
resole
|
||||
}
|
||||
}
|
||||
|
||||
intervalVideo(videos)
|
||||
})
|
||||
},
|
||||
handlePreview(codes, startIdx = 1) { //startIdx 如果是多个 从第几个窗口开始加载
|
||||
console.log(codes)
|
||||
let that = this
|
||||
if (!codes || codes=='' || codes.length==0)
|
||||
return
|
||||
if (Array.isArray(codes) && codes.length > 0) {
|
||||
// let confs = codes.map((code, index) => {
|
||||
// return {
|
||||
// cameraIndexCode: code,
|
||||
// streamMode: that.streamMode,
|
||||
// transMode: that.transMode,
|
||||
// gpuMode: that.gpuMode,
|
||||
// wndId: startIdx + index //设置不对会报错
|
||||
// }
|
||||
// })
|
||||
// that.oWebControl.JS_RequestInterface({
|
||||
// funcName: 'startMultiPreviewByCameraIndexCode',
|
||||
// argument: JSON.stringify({
|
||||
// list: confs
|
||||
// })
|
||||
// })
|
||||
this.onLoading = true
|
||||
this.multiVideos(codes).then(()=>{
|
||||
this.onLoading = false
|
||||
})
|
||||
} else if (typeof codes == 'string') {
|
||||
// 子码流特殊处理
|
||||
let streamMode = that.streamMode
|
||||
if(that.specialCodes.indexOf(codes)>-1) streamMode = 1
|
||||
|
||||
that.oWebControl.JS_RequestInterface({
|
||||
funcName: 'startPreview',
|
||||
argument: JSON.stringify({
|
||||
cameraIndexCode: codes,
|
||||
streamMode: streamMode,
|
||||
transMode: that.transMode,
|
||||
gpuMode: that.gpuMode,
|
||||
wndId: -1
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
uninit() {
|
||||
let oWebControl = this.oWebControl
|
||||
if (oWebControl != null) {
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: 'stopAllPreview'
|
||||
})
|
||||
oWebControl.JS_HideWnd() // 先让窗口隐藏,规避可能的插件窗口滞后于浏览器消失问题
|
||||
oWebControl.JS_Disconnect()
|
||||
.then(function() {
|
||||
// 断开与插件服务连接成功
|
||||
},
|
||||
function() { // 断开与插件服务连接失败
|
||||
console.log('oWebControl close error')
|
||||
})
|
||||
oWebControl = null
|
||||
}
|
||||
},
|
||||
windowChange() {
|
||||
//列表选项在左侧
|
||||
// this.height = document.getElementsByClassName('videoWindow')[0].scrollHeight
|
||||
// var btnWidth = 0
|
||||
// if (document.getElementsByClassName('playBtn').length > 0) {
|
||||
// btnWidth = document.getElementsByClassName('playBtn')[0].clientWidth
|
||||
// this.width = document.getElementsByClassName('videoWindow')[0].scrollWidth - btnWidth - 5
|
||||
// } else {
|
||||
// this.width = document.getElementsByClassName('videoWindow')[0].scrollWidth
|
||||
// }
|
||||
//列表选项在下方
|
||||
this.width = document.getElementsByClassName(`video-window${this.item}`)[0].scrollWidth
|
||||
var btnHeight = 0
|
||||
if (document.getElementsByClassName('playBtn').length > 0) {
|
||||
btnHeight = document.getElementsByClassName('playBtn')[0].clientHeight
|
||||
this.height = document.getElementsByClassName(`video-window${this.item}`)[0].scrollHeight - btnHeight - 10
|
||||
} else {
|
||||
this.height = document.getElementsByClassName(`video-window${this.item}`)[0].scrollHeight
|
||||
}
|
||||
|
||||
if (document.getElementById(`playWnd${this.item}`)) {
|
||||
document.getElementById(`playWnd${this.item}`).style.height = this.height + 'px'
|
||||
document.getElementById(`playWnd${this.item}`).style.width = this.width + 'px'
|
||||
}
|
||||
},
|
||||
hidePlugin() {
|
||||
this.oWebControl.JS_HideWnd()
|
||||
},
|
||||
showPlugin() {
|
||||
this.oWebControl.JS_ShowWnd()
|
||||
},
|
||||
ready() {
|
||||
let that = this
|
||||
return new Promise((resolve, reject) => {
|
||||
this.wait(function() {
|
||||
return that.isInit
|
||||
}, 6000 + that.timeOut || 500, 100).then(function() {
|
||||
resolve()
|
||||
}).catch(function(err) {
|
||||
that.$message.error({
|
||||
content: '视频控件加载超时,请检查',
|
||||
duration: 3
|
||||
})
|
||||
reject('视频控件加载超时,请检查')
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
83
src/components/Dialog/index.vue
Normal file
83
src/components/Dialog/index.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<el-dialog class="dialog-wrapper" v-model="dialogVisible" :show-close="false" :width="width" :draggable="draggable" align-center :overflow="overflow" :modal="modal" :destroy-on-close="destroyOnClose" :close-on-click-modal="false" :close-on-press-escape="false">
|
||||
<template #header="{ titleId, titleClass }">
|
||||
<div class="title" :id="titleId" :class="titleClass">
|
||||
<span>{{ title }}</span>
|
||||
<div class="icon-group">
|
||||
<el-icon v-if="fold" @click="handleFold"><Fold /></el-icon>
|
||||
<el-icon @click="close"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<slot></slot>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { Fold, Close } from '@element-plus/icons-vue'
|
||||
|
||||
const emit = defineEmits([ 'close' ])
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
default: true,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
title: {
|
||||
default: '',
|
||||
required: false,
|
||||
type: String
|
||||
},
|
||||
width: {
|
||||
default: '1230',
|
||||
required: false,
|
||||
type: [ String, Number ]
|
||||
},
|
||||
draggable: {
|
||||
default: true,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
overflow: {
|
||||
default: true,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
modal: {
|
||||
default: false,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
destroyOnClose: {
|
||||
default: false,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
fold: {
|
||||
default: false,
|
||||
required: false,
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
|
||||
const dialogVisible = computed(() => {
|
||||
return props.visible
|
||||
})
|
||||
|
||||
/**
|
||||
* 点击关闭按钮
|
||||
*/
|
||||
const close = () => {
|
||||
emit('close')
|
||||
}
|
||||
const handleFold = () => {
|
||||
emit('handleFold')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon-group{
|
||||
column-gap: 20px;
|
||||
display: inline-flex
|
||||
}
|
||||
</style>
|
||||
127
src/components/Dialog/largeModel.vue
Normal file
127
src/components/Dialog/largeModel.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<el-dialog class="largemodel-dialog-wrapper" v-model="dialogVisible"
|
||||
:center="center" :show-close="false"
|
||||
:width="width" :draggable="draggable"
|
||||
align-center :overflow="overflow"
|
||||
:modal="modal" :modal-penetrable="penetrable"
|
||||
:destroy-on-close="destroyOnClose"
|
||||
:close-on-click-modal="false" :close-on-press-escape="false">
|
||||
<template #header="{ titleId, titleClass }">
|
||||
<div class="title-wrapper title" :id="titleId" :class="[titleClass,{ 'centered': center }]">
|
||||
<div class="el-dialog-title-container">
|
||||
<span class="el-dialog-title">{{ title }}</span>
|
||||
</div>
|
||||
<div class="icon-group">
|
||||
<img src="@/assets/images/largeModel/icon-close.png" alt="" @click="close">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<slot></slot>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const emit = defineEmits([ 'close' ])
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
default: true,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
center: {
|
||||
default: false,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
title: {
|
||||
default: '',
|
||||
required: false,
|
||||
type: String
|
||||
},
|
||||
width: {
|
||||
default: '1230',
|
||||
required: false,
|
||||
type: [ String, Number ]
|
||||
},
|
||||
draggable: {
|
||||
default: true,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
overflow: {
|
||||
default: true,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
modal: {
|
||||
default: false,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
penetrable: {
|
||||
default: true,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
destroyOnClose: {
|
||||
default: false,
|
||||
required: false,
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
|
||||
const dialogVisible = computed(() => {
|
||||
return props.visible
|
||||
})
|
||||
|
||||
/**
|
||||
* 点击关闭按钮
|
||||
*/
|
||||
const close = () => {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.title-wrapper.title{
|
||||
font-family: 'YouSheBiaoTiHei';
|
||||
font-size: 28px;
|
||||
color: #42D9FF;
|
||||
line-height: 48px;
|
||||
text-align: center;
|
||||
span{
|
||||
margin-left: 22px;
|
||||
}
|
||||
}
|
||||
.el-dialog-title-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title-wrapper.centered .el-dialog-title-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title-wrapper.centered .icon-group {
|
||||
position: absolute;
|
||||
right: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.largemodel-dialog-wrapper{
|
||||
background: transparent;
|
||||
background-image: url('@/assets/images/largeModel/dialog-background.png');
|
||||
background-size: 100% 100%;
|
||||
height: 850px;
|
||||
.el-dialog__body{
|
||||
height: calc(100% - 90px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
124
src/components/Dialog/screen.vue
Normal file
124
src/components/Dialog/screen.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<el-dialog class="screen-dialog-wrapper" v-model="dialogVisible"
|
||||
:center="center" :show-close="false"
|
||||
:width="width" :draggable="draggable"
|
||||
align-center :overflow="overflow"
|
||||
:modal="modal" :modal-penetrable="penetrable"
|
||||
:destroy-on-close="destroyOnClose"
|
||||
:close-on-click-modal="false" :close-on-press-escape="false">
|
||||
<template #header="{ titleId, titleClass }">
|
||||
<div class="title-wrapper title" :id="titleId" :class="[titleClass,{ 'centered': center }]">
|
||||
<div class="el-dialog-title-container">
|
||||
<span class="el-dialog-title">{{ title }}</span>
|
||||
</div>
|
||||
<el-button v-if="btn" @click="handle" type="text">轨迹</el-button>
|
||||
<div class="icon-group">
|
||||
<img src="@/assets/images/common/icon_close.png" alt="" @click="close">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<slot></slot>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const emit = defineEmits([ 'close', 'handle' ])
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
default: true,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
center: {
|
||||
default: false,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
title: {
|
||||
default: '',
|
||||
required: false,
|
||||
type: String
|
||||
},
|
||||
width: {
|
||||
default: '1230',
|
||||
required: false,
|
||||
type: [ String, Number ]
|
||||
},
|
||||
draggable: {
|
||||
default: true,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
overflow: {
|
||||
default: true,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
modal: {
|
||||
default: false,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
penetrable: {
|
||||
default: true,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
destroyOnClose: {
|
||||
default: false,
|
||||
required: false,
|
||||
type: Boolean
|
||||
},
|
||||
btn: {
|
||||
default: false,
|
||||
required: false,
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
|
||||
const dialogVisible = computed(() => {
|
||||
return props.visible
|
||||
})
|
||||
|
||||
/**
|
||||
* 点击关闭按钮
|
||||
*/
|
||||
const close = () => {
|
||||
emit('close')
|
||||
}
|
||||
const handle = () => {
|
||||
emit('handle')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon-group{
|
||||
column-gap: 20px;
|
||||
display: inline-flex
|
||||
}
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-dialog-title-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title-wrapper.centered .el-dialog-title-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-group {
|
||||
column-gap: 20px;
|
||||
display: inline-flex;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.title-wrapper.centered .icon-group {
|
||||
margin-left: auto; /* 保持右侧对齐 */
|
||||
}
|
||||
|
||||
</style>
|
||||
165
src/components/Filter/index.vue
Normal file
165
src/components/Filter/index.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="filter">
|
||||
<el-form :class="theme" :inline="true" :model="model">
|
||||
<el-form-item
|
||||
v-for="(item, index) in filterItems"
|
||||
:key="index"
|
||||
:label="item.label">
|
||||
|
||||
<el-input
|
||||
v-if="item.type === 'input'"
|
||||
v-model="model[item.prop]"
|
||||
:placeholder="item.placeholder ? item.placeholder : '请输入'"
|
||||
:clearable="item.clearable !== false"
|
||||
style="width: 110px">
|
||||
</el-input>
|
||||
<el-select
|
||||
v-if="item.type === 'select'"
|
||||
v-model="model[item.prop]"
|
||||
:clearable="item.clearable !== false"
|
||||
:filterable="item.filterable || false"
|
||||
:placeholder="item.placeholder ? item.placeholder : '请选择'"
|
||||
:multiple="item.multiple || false"
|
||||
:collapse-tags="item.collapseTags || false"
|
||||
:collapse-tags-tooltip="item.collapseTagsTooltip || false"
|
||||
@change="(val) => handle(item.event,val)"
|
||||
style="width: 90px"
|
||||
poper-class="filter-select"
|
||||
:empty-values="item.emptyValues || [ '', undefined]"
|
||||
:value-on-clear="item.valueOnClear || ''">
|
||||
<el-checkbox v-if="item.checkbox || false" v-model="checked" @change='(val)=>selectAll(val,item)'>全选</el-checkbox>
|
||||
<el-option
|
||||
v-for="(opt, index) in item.options"
|
||||
:key="index"
|
||||
:label="opt.label"
|
||||
:value="opt.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<template v-if="item.type === 'datetimerange' && model.time && model.time.length >= 2">
|
||||
<el-date-picker
|
||||
v-model="model[item.prop][0]"
|
||||
type="datetime"
|
||||
:placeholder="item.placeholder ? item.placeholder : '请选择时间'"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
:clearable="item.clearable !== false"
|
||||
style="width: 180px">
|
||||
</el-date-picker>
|
||||
<span class="range"></span>
|
||||
<el-date-picker
|
||||
v-model="model[item.prop][1]"
|
||||
type="datetime"
|
||||
:placeholder="item.placeholder ? item.placeholder : '请选择时间'"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
:clearable="item.clearable !== false"
|
||||
style="width: 180px">
|
||||
</el-date-picker>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="buttons">
|
||||
<template v-for="(item, index) in filterButtons" :key="index">
|
||||
<el-button
|
||||
v-if="item.type === 'button'"
|
||||
:type="item.theme"
|
||||
@click="handle(item.prop)">{{ item.name }}
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
const emit = defineEmits([ 'handle', 'change', 'remote', 'changeDate' ])
|
||||
|
||||
const props = defineProps({
|
||||
'filter-buttons': {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
'filter-items': {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
'filter-model': {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'Form'
|
||||
},
|
||||
disabledDate: {
|
||||
type: Function,
|
||||
required: false
|
||||
},
|
||||
calendarChange: {
|
||||
type: Function,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
|
||||
const model = ref({})
|
||||
const checked = ref(false)
|
||||
|
||||
// 多选下拉框 全选操作
|
||||
const selectAll = (val, item) => {
|
||||
model.value[item.prop] = []
|
||||
if (checked.value) {
|
||||
model.value[item.prop] = item.options.map(i => i.value)
|
||||
} else {
|
||||
model.value[item.prop] = []
|
||||
}
|
||||
}
|
||||
// 修改多选下拉框全选选中状态
|
||||
const changeChecked = (flag) => {
|
||||
checked.value = flag
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击操作按钮
|
||||
* @param type 操作类型
|
||||
*/
|
||||
const handle = (type) => {
|
||||
emit('handle', type, model.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置表单数据
|
||||
*/
|
||||
const reset = () => {
|
||||
model.value = { ...props.filterModel }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
reset()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
reset,
|
||||
changeChecked,
|
||||
model: model
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
.el-form{
|
||||
width: 1060px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
116
src/components/FlvPlayer/index.vue
Normal file
116
src/components/FlvPlayer/index.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="flv-player-wrapper">
|
||||
<video :id="`videoElement${props.id}`" :controls="false" autoplay muted
|
||||
style="width: 100%; height: 100%; object-fit: fill;">
|
||||
</video>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import flvjs from 'flv.js'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
let player = null
|
||||
let flag = 0
|
||||
let retryCount = 0
|
||||
const MAX_RETRIES = 30
|
||||
const initPlayer = () => {
|
||||
if (flvjs.isSupported()) {
|
||||
if (!props.url) {
|
||||
return null
|
||||
}
|
||||
|
||||
const videoElement = document.getElementById(`videoElement${props.id}`)
|
||||
clear()
|
||||
// 调用云台旋转判sign
|
||||
|
||||
const url = props.url
|
||||
|
||||
player = flvjs.createPlayer(
|
||||
{
|
||||
type: 'flv',
|
||||
isLive: true,
|
||||
hasAudio: false,
|
||||
url: url
|
||||
},
|
||||
{
|
||||
// // 启用IO隐藏缓冲区
|
||||
// // 如果需要实时(最小延迟)来进行实时流播放,则设置为false
|
||||
// // 但是如果网络抖动,则可能会停顿
|
||||
// enableStashBuffer: false,
|
||||
// // 之时IO暂存缓冲区的初始大小,默认值为384kb,指出合适的尺寸可以改善视频负载/搜索时间
|
||||
// stashInitialSize: 128
|
||||
|
||||
// 启用缓冲区优化
|
||||
enableStashBuffer: true, // 改为true启用缓冲
|
||||
stashInitialSize: 1024 * 1024, // 设置初始缓冲大小
|
||||
maxBufferLength: 30 // 最大缓冲时长(秒)
|
||||
}
|
||||
)
|
||||
|
||||
player.attachMediaElement(videoElement)
|
||||
player.load()
|
||||
player.play()
|
||||
console.log('player play')
|
||||
flag += 1
|
||||
|
||||
player.on('error', () => {
|
||||
console.log('errorrrrrrrrrrrrrrrrrrrrrrrr')
|
||||
clear()
|
||||
// 重试次数增加,延长时间指数增长,不超过10s
|
||||
const retryDelay = Math.min(10000, 1000 * Math.pow(2, retryCount))
|
||||
retryCount++
|
||||
|
||||
setTimeout(() => {
|
||||
console.log(retryCount, retryDelay, '重连次数......')
|
||||
if (retryCount <= MAX_RETRIES) {
|
||||
initPlayer() // 重新初始化播放器
|
||||
} else {
|
||||
player.pause()
|
||||
if (player) {
|
||||
player = null
|
||||
}
|
||||
console.error('重试超过最大次数')
|
||||
}
|
||||
}, retryDelay)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
if (player) {
|
||||
player.pause()
|
||||
player.destroy()
|
||||
player = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initPlayer()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clear()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.flv-player-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
}
|
||||
</style>
|
||||
292
src/components/HeaderMenu/index.vue
Normal file
292
src/components/HeaderMenu/index.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<div class="screen-border">
|
||||
<div class="header-background"></div>
|
||||
<div class="header-box">
|
||||
<div class="left">
|
||||
<div class="weather">
|
||||
<img
|
||||
src="@/assets/images/weather/100.png">
|
||||
<div class="weather-text">
|
||||
<div>{{model.temp}}℃ {{model.text}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav-list">
|
||||
<li :class="['nav-item', current === item.prop ? 'active' : '']" v-for="(item, index) in navLeft" :key="index"
|
||||
@click="toggle(item)">{{ item.label }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="title" data-text="千帆智瞰">
|
||||
<span>千帆智瞰</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<ul class="nav-list">
|
||||
<li :class="['nav-item', current === item.prop ? 'active' : '']" v-for="(item, index) in navRight" :key="index"
|
||||
@click="toggle(item)">{{ item.label }}</li>
|
||||
</ul>
|
||||
<div class="datetime">
|
||||
<div class="time">{{time[2]}}</div>
|
||||
<div class="particular">
|
||||
<div class="week">{{time[1]}}</div>
|
||||
<div class="date">{{time[0]}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="download" @click="toSetting">
|
||||
<img src="@/assets/images/common/icon-setting.png" alt="" style="width: 16px;height: 16px;">
|
||||
</div>
|
||||
<div class="download" @click="downloadPlugin">
|
||||
<img src="@/assets/images/common/icon-download.png" alt="" style="width: 16px;height: 16px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { parseTime } from '@/utils/common'
|
||||
import { Setting } from '@element-plus/icons-vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const emit = defineEmits([ 'handle' ])
|
||||
|
||||
const current = ref('livePreview')
|
||||
|
||||
const navLeft = [
|
||||
// {
|
||||
// label: '实时预览',
|
||||
// path: '/screen/livePreview',
|
||||
// prop: 'livePreview'
|
||||
// },
|
||||
// {
|
||||
// label: '船舶监测一张图',
|
||||
// path: '/screen/supervision',
|
||||
// prop: 'supervision'
|
||||
// }
|
||||
]
|
||||
|
||||
const navRight = [
|
||||
// {
|
||||
// label: '识别记录',
|
||||
// path: '/screen/identification',
|
||||
// prop: 'identification'
|
||||
// },
|
||||
{
|
||||
label: '插件下载',
|
||||
path: '',
|
||||
prop: 'monitor'
|
||||
}
|
||||
]
|
||||
const navMap = new Map([ ...navLeft, ...navRight ].map(item => [ item.path, item.prop ]))
|
||||
const model = reactive({
|
||||
temp: 30,
|
||||
text: '晴天'
|
||||
})
|
||||
const time = ref([])
|
||||
|
||||
// 根据路由变化tab选中项改变
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
const prop = navMap.get(route.path)
|
||||
if (prop) {
|
||||
current.value = prop
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
const toggle = (nav) => {
|
||||
if(nav.prop === 'monitor') {
|
||||
window.open('/plugin/VideoWebPlugin.exe', '_blank')
|
||||
}
|
||||
}
|
||||
// 右上角时间
|
||||
const updateTime = () => {
|
||||
const result = parseTime(new Date(), '{y}.{m}.{d} 周{a} {h}:{i}:{s}')
|
||||
time.value = result.split(' ')
|
||||
requestAnimationFrame(updateTime)
|
||||
}
|
||||
/**
|
||||
* 点击下拉菜单
|
||||
*/
|
||||
const openMenu = (key) => {
|
||||
switch (key) {
|
||||
case 'system':
|
||||
window.open('/system', 'system_window')
|
||||
break
|
||||
case 'logout':
|
||||
exit()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const exit = () => {
|
||||
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
userStore.logout().then(() => {
|
||||
location.href = '/login'
|
||||
})
|
||||
})
|
||||
}
|
||||
// 跳转到业务管理
|
||||
const toSetting = () => {
|
||||
window.open('/system', 'system_window')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.screen-border {
|
||||
width: 100%;
|
||||
.header-background{
|
||||
background-image: url('@/assets/images/common/header-background.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
width: 50.36vw;
|
||||
height: 12.31vh;
|
||||
line-height: 60px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0px);
|
||||
}
|
||||
.header-box{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0px);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
line-height: 80px;
|
||||
}
|
||||
.title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
font-size: 38px;
|
||||
font-family: 'YouSheBiaoTiHei';
|
||||
letter-spacing: 15px;
|
||||
color: transparent; /* 隐藏原始文本颜色 */
|
||||
}
|
||||
.title::before {
|
||||
content: attr(data-text); /* 使用属性值作为文本内容 */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1; /* 确保渐变在阴影之上 */
|
||||
background: linear-gradient(180deg, #FFFFFF 23%, #E9F8FF 46%, #77BAFF 76%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
}
|
||||
.title::after {
|
||||
content: attr(data-text);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
text-shadow:
|
||||
0px 3px 1px rgba(19,80,143,0.8),
|
||||
0px 0px 14px rgba(130,165,255,0.6),
|
||||
0px 0px 2px rgba(255,255,255,0.4);
|
||||
}
|
||||
.nav-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap:25px;
|
||||
.nav-item {
|
||||
height: 29px;
|
||||
font-family: 'PMZDR';
|
||||
text-align: center;
|
||||
line-height: 29px;
|
||||
background-image: url('@/assets/images/common/nav-background.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
padding: 0 22px;
|
||||
font-size: 15px;
|
||||
color: #DAEDFF;
|
||||
cursor: pointer;
|
||||
&.active {
|
||||
background-image: url('@/assets/images/common/nav-background-active.png');
|
||||
color: #DAEDFF;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
.left,.right{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.weather{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: 'GuangliangRegular';
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
img{
|
||||
width: 30px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
.download{
|
||||
cursor: pointer;
|
||||
line-height: 20px;
|
||||
img{
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
.datetime{
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
color: #FFFFFF;
|
||||
text-align: left;
|
||||
height: 29px;
|
||||
align-items: center;
|
||||
|
||||
.time{
|
||||
font-family: 'D-DIN';
|
||||
font-weight: bold;
|
||||
font-size: 22px;
|
||||
}
|
||||
.particular{
|
||||
line-height: 15px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
.week{
|
||||
font-family: 'GuangliangRegular';
|
||||
font-size: 10px;
|
||||
}
|
||||
.date{
|
||||
font-family: 'D-DIN';
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.icon-exit{
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
.left{
|
||||
gap:51px;
|
||||
margin-left: 61px;
|
||||
}
|
||||
.right{
|
||||
gap:20px;
|
||||
margin-right: 40px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
130
src/components/Map/MapServer/index.vue
Normal file
130
src/components/Map/MapServer/index.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div :class="[{ 'expend-only': expend }, 'screen-map-server',toolBarStore.expand ? 'expand' : '']" @mouseenter="moveEnter" @mouseleave="moveLeave">
|
||||
|
||||
<img alt="ICON_MAP" class="icon-cover" :src="getAssetsFile(`icon-${active}-active.png`)">
|
||||
|
||||
<div v-for="(item, index) in list" class="screen-map-server-item" :key="index" :style="getStyle(item, index)"
|
||||
@click="toggle(item, index)">
|
||||
|
||||
<img v-show="(!expend && !index) || expend" alt="ICON_MAP" class="icon-map"
|
||||
:src="getAssetsFile(`icon-${item.prop}${item.prop == active ? '-active' : ''}.png`)">
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { getAssetsFile } from '@/utils/common'
|
||||
import useToolBarStore from '@/store/modules/toolbar'
|
||||
|
||||
const toolBarStore = useToolBarStore()
|
||||
const emit = defineEmits([ 'toggle' ])
|
||||
|
||||
const active = ref('satellite')
|
||||
const expend = ref(false)
|
||||
|
||||
const list = [
|
||||
{
|
||||
label: '遥感',
|
||||
prop: 'satellite'
|
||||
},
|
||||
{
|
||||
label: '海图',
|
||||
prop: 'sea'
|
||||
},
|
||||
{
|
||||
label: '浅色',
|
||||
prop: 'light'
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 动态改变当前图例的定位
|
||||
* @param e 当前图例信息
|
||||
* @param index 当前图例下标
|
||||
*/
|
||||
const getStyle = (e, index) => ({
|
||||
'background-color': !expend.value && e.color ? e.color : 'transparent', // 未展开样式
|
||||
'border-radius': expend.value ? '0px' : '5px', // 未展开样式
|
||||
right: expend.value ? `${index * 40 + (index + 1) * 4}px` : `${(index + 1) * 4}px`,
|
||||
'transition-duration': expend.value ? `${index * 0.2}s` : '0s',
|
||||
'z-index': e.index
|
||||
})
|
||||
|
||||
const moveEnter = () => {
|
||||
expend.value = true
|
||||
}
|
||||
|
||||
const moveLeave = () => {
|
||||
expend.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 底图切换
|
||||
* @param e 当前图例信息
|
||||
*/
|
||||
const toggle = (e) => {
|
||||
active.value = e.prop
|
||||
emit('toggle', 'toggle-base', e.prop)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.screen-map-server {
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
bottom: 112px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
padding: 4px;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
background-color: transparent;
|
||||
transition-duration: .3s;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
|
||||
&.expend-only {
|
||||
height: 48px;
|
||||
width: 135px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(77, 151, 255, 0.5);
|
||||
|
||||
.icon-cover {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* COVER */
|
||||
.icon-cover {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
/* ITEM */
|
||||
.screen-map-server-item {
|
||||
border-radius: 0;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
width: 40px;
|
||||
z-index: 1;
|
||||
|
||||
.icon-map {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.expand{
|
||||
right: 390px;
|
||||
}
|
||||
</style>
|
||||
451
src/components/Map/index.vue
Normal file
451
src/components/Map/index.vue
Normal file
@@ -0,0 +1,451 @@
|
||||
<template>
|
||||
<div id="map"></div>
|
||||
<div class="map-mask"></div>
|
||||
<!-- 四周边框 -->
|
||||
<div class="map-border"></div>
|
||||
<img class="map-bottom" src="@/assets/images/common/icon-bottom.png" alt="">
|
||||
<img class="AI" src="@/assets/images/common/icon-AI.png" alt="" @click="toModel">
|
||||
<div class="AI AI-text" @click="toModel">智</div>
|
||||
|
||||
<!-- 信息弹窗 -->
|
||||
<InfoWindowComponent/>
|
||||
<!-- 渔船信息弹窗 -->
|
||||
<TrawlerInfoWindowComponent/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import * as maptalks from 'maptalks'
|
||||
import GlobalMap from './js/GlobalMap'
|
||||
import useMapStore from '@/store/modules/map'
|
||||
import { getAssetsFile } from '@/utils/common'
|
||||
import * as BoatUtil from './lbtbox/boatTerminal'
|
||||
import * as $configs from './map-config.js'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { monitors, uavs, stations, environmentals, fences, detailFences } from './js/mock.js'
|
||||
import InfoWindowComponent from '@/components/Map/window/index.vue'
|
||||
import TrawlerInfoWindowComponent from '@/components/Map/window/trawler.vue'
|
||||
|
||||
const mapStore = useMapStore()
|
||||
const UAV = computed(() => mapStore.legend.UAV)
|
||||
const monitor = computed(() => mapStore.legend.monitor)
|
||||
const origin_monitor = computed(() => mapStore.legend.origin_monitor)
|
||||
const ais_station = computed(() => mapStore.legend.ais_station)
|
||||
const environmental = computed(() => mapStore.legend.environmental)
|
||||
const fence = computed(() => mapStore.legend.fence)
|
||||
const sector = computed(() => mapStore.sector)
|
||||
let globalMap = null
|
||||
let vector = {}
|
||||
const geography = {
|
||||
monitor: [], // 监控数据
|
||||
origin_monitor: [], // 原始监控数据
|
||||
UAV: [], // 无人机
|
||||
ais_station: [], // ais基站
|
||||
environmental: [], // 环境监测
|
||||
fence: [] // 电子围栏
|
||||
}
|
||||
let sectorLayer = null
|
||||
const initMap = () => {
|
||||
const mapDom = document.getElementById('map')
|
||||
globalMap = new GlobalMap(mapDom)//, { seamlessZoom: false })
|
||||
}
|
||||
|
||||
const initLayerToMap = (type) => { // 地理图层注册
|
||||
if (vector[type]) {
|
||||
vector[type].remove()
|
||||
vector[type] = null
|
||||
}
|
||||
vector[type] = new maptalks.VectorLayer(type).addTo(globalMap.map)
|
||||
}
|
||||
// 叠加电子围栏数据
|
||||
const addFenceToMap = (type) => {
|
||||
initLayerToMap(type)
|
||||
geography[type].forEach((e) => {
|
||||
if (e.positionInfo) {
|
||||
const params = []
|
||||
const symbol = [
|
||||
{
|
||||
lineColor: e.lineColor,
|
||||
lineWidth: e.lineWidth - 0,
|
||||
lineDasharray: [ 4, 4 ],
|
||||
polygonFill: e.fillColor,
|
||||
polygonOpacity: e.diaphaneity - 0,
|
||||
textFill: 'black',
|
||||
textHaloFill: 'white',
|
||||
textHaloRadius: 2,
|
||||
textName: e.warnAreaName,
|
||||
textSize: {
|
||||
stops: [
|
||||
[ 10, 0 ],
|
||||
[ 11, 12 ]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
// 点位数据处理
|
||||
const arr = e.positionInfo.split(';').map((pair) => {
|
||||
const [ x, y ] = pair.split(',').map(Number)
|
||||
return { x, y }
|
||||
})
|
||||
params.push(arr)
|
||||
params.push({ symbol, zIndex: 1 })
|
||||
|
||||
const geometry = new maptalks.Polygon(...params)
|
||||
vector[type].addGeometry(geometry)
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 叠加监控数据
|
||||
* @param type
|
||||
* @param prop 监控类型
|
||||
*/
|
||||
const addMonitorToMap = () => {
|
||||
initLayerToMap('monitor')
|
||||
initLayerToMap('sectors_monitor')
|
||||
geography.monitor.forEach(item => {
|
||||
if (item.longitude && item.latitude) {
|
||||
const marker = new maptalks.Marker(
|
||||
{
|
||||
x: item.longitude,
|
||||
y: item.latitude
|
||||
},
|
||||
{
|
||||
id: item.id,
|
||||
symbol: $configs.getDevicePointSymbol('_monitor', { ...item, name: item.videoName }),
|
||||
properties: item,
|
||||
zIndex: 1
|
||||
}
|
||||
)
|
||||
marker.addTo(vector.monitor)
|
||||
drawSector('_monitor', [ item.longitude, item.latitude ], 5 * 1000, item.id, 30)
|
||||
marker.on('click', (evt) => {
|
||||
mapStore.updateWindowInfo({ visible: true, type: '_monitor', data: { ...item } })
|
||||
})
|
||||
}
|
||||
})
|
||||
vector.monitor.show()
|
||||
}
|
||||
/**
|
||||
* 叠加无人机数据
|
||||
* @param type
|
||||
* @param prop 无人机类型
|
||||
*/
|
||||
const addUAVToMap = () => {
|
||||
initLayerToMap('UAV')
|
||||
initLayerToMap('sectors_UAV')
|
||||
geography.UAV.forEach(item => {
|
||||
if (item.longitude && item.latitude) {
|
||||
const marker = new maptalks.Marker(
|
||||
{
|
||||
x: item.longitude,
|
||||
y: item.latitude
|
||||
},
|
||||
{
|
||||
id: item.id,
|
||||
symbol: $configs.getDevicePointSymbol('_UAV', { ...item, name: item.videoName }),
|
||||
properties: item,
|
||||
zIndex: 1
|
||||
}
|
||||
)
|
||||
marker.addTo(vector.UAV)
|
||||
drawSector('_UAV', [ item.longitude, item.latitude ], 5 * 1000, item.id)
|
||||
marker.on('click', (evt) => {
|
||||
mapStore.updateWindowInfo({ visible: true, type: '_UAV', data: { ...item } })
|
||||
})
|
||||
}
|
||||
})
|
||||
vector.UAV.show()
|
||||
}
|
||||
// 需要监控的起始角度和结束角度,修改视野范围可以传递经纬度坐标
|
||||
const drawSector = (type, center, radius, id, angle) => {
|
||||
const sectorId = `sector_${type}${id}`
|
||||
// 如果已存在同ID的扇形,则先移除
|
||||
const existingSector = vector['sectors' + type].getGeometryById(sectorId)
|
||||
if (existingSector) {
|
||||
vector['sectors' + type].removeGeometry(existingSector)
|
||||
}
|
||||
|
||||
// 如果已有该监控点的扇形图层,则先移除
|
||||
if (globalMap.map.getLayer(sectorId)) {
|
||||
globalMap.map.getLayer(sectorId).remove()
|
||||
}
|
||||
|
||||
let circle = new maptalks.Circle(center, radius, {
|
||||
id: sectorId,
|
||||
symbol: {
|
||||
lineColor: '#1CA8FF',
|
||||
lineWidth: 1,
|
||||
lineOpacity: 1,
|
||||
polygonFill: '#1ca8ff',
|
||||
polygonOpacity: 0.16
|
||||
}
|
||||
})
|
||||
if(angle) {
|
||||
let ellipse = new maptalks.Sector(center, radius - 1 * 1000, angle - 30, angle + 30, {
|
||||
symbol: {
|
||||
lineColor: '#FF8D1C',
|
||||
polygonFill: '#ff8d1c29'
|
||||
}
|
||||
})
|
||||
let line = new maptalks.Sector(center, radius + 1 * 1000, angle, angle, {
|
||||
symbol: {
|
||||
lineColor: '#FF8D1C',
|
||||
polygonFill: '#ff8d1c29',
|
||||
lineDasharray: [ 5, 10 ]
|
||||
}
|
||||
})
|
||||
vector['sectors' + type].addGeometry([ circle, ellipse, line ])
|
||||
}else{
|
||||
vector['sectors' + type].addGeometry(circle)
|
||||
}
|
||||
// 添加到可视域图层
|
||||
vector['sectors' + type].hide()
|
||||
}
|
||||
/**
|
||||
* 叠加ais基站数据
|
||||
* @param type
|
||||
* @param prop ais基站类型
|
||||
*/
|
||||
const addAisStationToMap = () => {
|
||||
initLayerToMap('ais_station')
|
||||
geography.ais_station.forEach(item => {
|
||||
if (item.longitude && item.latitude) {
|
||||
const marker = new maptalks.Marker(
|
||||
{
|
||||
x: item.longitude,
|
||||
y: item.latitude
|
||||
},
|
||||
{
|
||||
id: item.id,
|
||||
symbol: $configs.getDevicePointSymbol('_ais_station', { ...item }),
|
||||
properties: item,
|
||||
zIndex: 1
|
||||
}
|
||||
)
|
||||
marker.addTo(vector.ais_station)
|
||||
marker.on('click', (evt) => {
|
||||
})
|
||||
}
|
||||
})
|
||||
vector.ais_station.show()
|
||||
}
|
||||
/**
|
||||
* 叠加环境监测数据
|
||||
* @param type
|
||||
* @param prop 环境监测类型
|
||||
*/
|
||||
const addEnvironmentalToMap = () => {
|
||||
initLayerToMap('environmental')
|
||||
geography.environmental.forEach(item => {
|
||||
if (item.longitude && item.latitude) {
|
||||
const marker = new maptalks.Marker(
|
||||
{
|
||||
x: item.longitude,
|
||||
y: item.latitude
|
||||
},
|
||||
{
|
||||
id: item.id,
|
||||
symbol: $configs.getDevicePointSymbol('_environmental', { ...item }),
|
||||
properties: item,
|
||||
zIndex: 1
|
||||
}
|
||||
)
|
||||
marker.addTo(vector.environmental)
|
||||
marker.on('click', (evt) => {
|
||||
})
|
||||
}
|
||||
})
|
||||
vector.environmental.show()
|
||||
}
|
||||
|
||||
const initUAV = () => {
|
||||
geography.UAV = uavs
|
||||
addUAVToMap()
|
||||
}
|
||||
const initMonitor = () => {
|
||||
geography.monitor = monitors
|
||||
addMonitorToMap()
|
||||
}
|
||||
const initAisStation = () => {
|
||||
geography.ais_station = stations
|
||||
addAisStationToMap()
|
||||
}
|
||||
const initEnvironmental = () => {
|
||||
geography.environmental = environmentals
|
||||
addEnvironmentalToMap()
|
||||
|
||||
}
|
||||
const initFence = () => {
|
||||
geography.fence = fences
|
||||
geography.detailFence = detailFences
|
||||
addFenceToMap('fence')
|
||||
addFenceToMap('detailFence')
|
||||
}
|
||||
const toModel = () => {
|
||||
mapStore.updateLargeModel(true)
|
||||
}
|
||||
onMounted(() => {
|
||||
initMap()
|
||||
initFence()
|
||||
initUAV()
|
||||
initMonitor()
|
||||
initAisStation()
|
||||
initEnvironmental()
|
||||
// 渔船链接
|
||||
BoatUtil.init(mapStore)
|
||||
BoatUtil.getShip()
|
||||
// 轨迹图层
|
||||
initLayerToMap('track')
|
||||
})
|
||||
watch(() => UAV.value, () => {
|
||||
if(UAV.value) {
|
||||
initUAV()
|
||||
}else{
|
||||
vector.UAV?.hide()
|
||||
}
|
||||
})
|
||||
watch(() => monitor.value, () => {
|
||||
if(monitor.value) {
|
||||
initMonitor()
|
||||
}else{
|
||||
vector.monitor?.hide()
|
||||
}
|
||||
})
|
||||
watch(() => ais_station.value, () => {
|
||||
if(ais_station.value) {
|
||||
initAisStation()
|
||||
}else{
|
||||
vector.ais_station?.hide()
|
||||
}
|
||||
})
|
||||
watch(() => environmental.value, () => {
|
||||
if(environmental.value) {
|
||||
initEnvironmental()
|
||||
}else{
|
||||
vector.environmental?.hide()
|
||||
}
|
||||
})
|
||||
watch(() => fence.value, () => {
|
||||
if(fence.value) {
|
||||
initFence()
|
||||
}else{
|
||||
vector.fence?.hide()
|
||||
vector.detailFence?.hide()
|
||||
}
|
||||
})
|
||||
watch(() => sector.value.monitor, (newVal) => {
|
||||
if(vector.sectors_monitor) {
|
||||
if(newVal) {
|
||||
vector.sectors_monitor.show()
|
||||
} else {
|
||||
vector.sectors_monitor.hide()
|
||||
}
|
||||
}
|
||||
})
|
||||
watch(() => sector.value.UAV, (newVal) => {
|
||||
if(vector.sectors_UAV) {
|
||||
if(newVal) {
|
||||
vector.sectors_UAV.show()
|
||||
} else {
|
||||
vector.sectors_UAV.hide()
|
||||
}
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
globalMap.destroy()
|
||||
globalMap = null
|
||||
BoatUtil.destroyWebsocket()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#map {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('@/assets/images/common/map-mask.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
pointer-events: none;
|
||||
}
|
||||
.map-border {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: calc(100% - 18px);
|
||||
background-image: url('@/assets/images/common/map-border.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
pointer-events: none;
|
||||
}
|
||||
.map-bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%);
|
||||
}
|
||||
.AI{
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
left: 50%;
|
||||
transform: translate(-50%);
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
}
|
||||
.AI-text{
|
||||
font-family: 'YouSheBiaoTiHei';
|
||||
font-size: 22px;
|
||||
-webkit-text-stroke: 1px rgba(4,176,244,0.4);
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
background-image: linear-gradient(180deg, #FFFFFF 5%, #2DE5FF 100%);
|
||||
-webkit-background-clip: text;
|
||||
bottom: 33px;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
// 弹窗样式
|
||||
:deep(.dialog-wrapper.el-dialog){
|
||||
// height: 900px;
|
||||
background: linear-gradient(90deg, rgba(12, 25, 41, 0.8) 0%, rgba(12, 25, 41, 0.6) 100%);
|
||||
|
||||
border: 1px solid #00C0FF;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
.el-dialog__header{
|
||||
padding: 0 16px;
|
||||
background: linear-gradient(0deg, rgba(10, 169, 255, 0) 0%, rgba(10, 169, 255, 0.5) 100%);
|
||||
.title{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
text-align: left;
|
||||
height: 56px;
|
||||
.icon-group img {
|
||||
cursor: pointer;
|
||||
&:active {
|
||||
opacity: .8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-dialog__body {
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
146
src/components/Map/js/DrawToolInMap.js
Normal file
146
src/components/Map/js/DrawToolInMap.js
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 基于maptalks的绘制操作
|
||||
* 目前支持polygon\circle\rectangle
|
||||
*/
|
||||
import * as maptalks from 'maptalks'
|
||||
|
||||
export default class DrawToolInMap {
|
||||
constructor(map) {
|
||||
this.map = map // 地图
|
||||
this.tool = null // 定义绘制工具
|
||||
this.geometry = null // 绘制的图形信息
|
||||
this._initLayer()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化绘制图层
|
||||
* @param symbol
|
||||
*/
|
||||
_initLayer() {
|
||||
if (this.map.getLayer('draw_tool')) {
|
||||
this.layer = this.map.getLayer('draw_tool')
|
||||
this.layer.clear()
|
||||
} else {
|
||||
this.layer = new maptalks.VectorLayer('draw_tool', {
|
||||
zIndex: 2
|
||||
}).addTo(this.map)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复现图形
|
||||
* @param className
|
||||
* @param params
|
||||
*/
|
||||
initGeometry(className, params) {
|
||||
this.geometry = new maptalks[className](...params)
|
||||
this.layer.clear().addGeometry(this.geometry)
|
||||
return this.geometry
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新样式
|
||||
* @param symbol
|
||||
*/
|
||||
setSymbol(symbol) {
|
||||
if (this.geometry) {
|
||||
this.geometry.setSymbol(symbol)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁绘制工具和绘制图层
|
||||
*/
|
||||
destroy() {
|
||||
this.geometry = null
|
||||
if (this.layer) {
|
||||
this.layer.clear().remove()
|
||||
this.layer = null
|
||||
}
|
||||
if (this.tool) {
|
||||
this.tool.remove()
|
||||
this.tool = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除绘制工具并清空绘制图层
|
||||
*/
|
||||
remove() {
|
||||
this.geometry = null
|
||||
if (this.layer) {
|
||||
this.layer.clear()
|
||||
}
|
||||
if (this.tool) {
|
||||
this.tool.remove()
|
||||
this.tool = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除绘制工具
|
||||
*/
|
||||
removeDraw() {
|
||||
if (this.tool) {
|
||||
this.tool.remove()
|
||||
this.tool = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空绘制图层
|
||||
*/
|
||||
clear() {
|
||||
if (this.layer) {
|
||||
this.layer.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制操作
|
||||
* @param config 配置信息
|
||||
*/
|
||||
toggleDraw(config) {
|
||||
this.remove()
|
||||
// 关闭绘制工具
|
||||
const { drawend, drawstart, mousemove, mode, once, symbol } = config
|
||||
|
||||
this.tool = new maptalks.DrawTool({
|
||||
mode: mode || 'Polygon',
|
||||
symbol,
|
||||
once // 默认false
|
||||
}).addTo(this.map)
|
||||
|
||||
this.tool.on('drawend', (param) => {
|
||||
this.geometry = param.geometry
|
||||
this.layer.addGeometry(param.geometry)
|
||||
|
||||
if (drawend) {
|
||||
drawend(param)
|
||||
}
|
||||
|
||||
const geometries = this.layer.getGeometries()
|
||||
// 当绘制Point时,此逻辑会有问题。由于目前没有点位绘制,所以暂时先这么保留
|
||||
// 多次绘制的时候,只保留最后一次
|
||||
if (geometries.length > 1) {
|
||||
this.layer.removeGeometry(geometries[0])
|
||||
}
|
||||
})
|
||||
|
||||
this.tool.on('drawstart', (param) => {
|
||||
if (drawstart) {
|
||||
drawstart(param)
|
||||
}
|
||||
// 置空
|
||||
this.geometry = null
|
||||
})
|
||||
|
||||
if (mousemove) {
|
||||
this.tool.on('mousemove', (param) => {
|
||||
mousemove(param)
|
||||
})
|
||||
}
|
||||
|
||||
return this.tool
|
||||
}
|
||||
}
|
||||
135
src/components/Map/js/GlobalMap.js
Normal file
135
src/components/Map/js/GlobalMap.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import * as maptalks from 'maptalks'
|
||||
import * as $configs from '@/components/Map/map-config'
|
||||
import { GroupGLLayer } from '@maptalks/gl'
|
||||
// private baseLayer: TileLayer | null = null;
|
||||
// private groupLayer: GroupGLLayer | null = null;
|
||||
// private subLayers: any[] = []; // 存储子图层
|
||||
|
||||
export default class GlobalMap {
|
||||
static instance
|
||||
|
||||
constructor(container, option) {
|
||||
if (!GlobalMap.instance) {
|
||||
this.baseConfig = { ...$configs.baseConfig, ...option }
|
||||
this._initMap(container, this.baseConfig)
|
||||
GlobalMap.instance = this
|
||||
}
|
||||
return GlobalMap.instance
|
||||
}
|
||||
|
||||
_initMap(dom, option) {
|
||||
this.map = new maptalks.Map(dom, option)
|
||||
this.glGroup = new GroupGLLayer('glgroup').addTo(this.map)
|
||||
// this.glGroup.setZIndex(88)
|
||||
if (!Object.prototype.hasOwnProperty.call(option, 'baseLayer')) {
|
||||
this.toggleBaseLayer(option.baseLayerName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 底图切换功能
|
||||
* @principles 以GroupTileLayer为载体,提供有限个底图用于组合,保证图层复用,尽可能减少缓存和实例化
|
||||
* @param opt 底图图层名称 与baseLayerConfig.baseConfig.name匹配
|
||||
*/
|
||||
toggleBaseLayer(key) {
|
||||
let groupTileLayer = this.map.getLayer('base_group_tiles')
|
||||
if (!groupTileLayer) {
|
||||
groupTileLayer = new maptalks.GroupTileLayer('base_group_tiles')
|
||||
this.map.setBaseLayer(groupTileLayer)
|
||||
}
|
||||
// 获取底图名称
|
||||
const names = $configs.baseLayerConfig.baseConfig[key] || []
|
||||
const oldLayers = groupTileLayer.getLayers()
|
||||
// 添加或更新
|
||||
names.forEach((item, index) => {
|
||||
const config = $configs.baseLayerConfig.layerConfig[item]
|
||||
if (oldLayers.length > index) {
|
||||
oldLayers[index].show()
|
||||
oldLayers[index].setOptions(config)
|
||||
oldLayers[index].forceReload()
|
||||
} else {
|
||||
const layerName = `base_layer_${index}` // 索引做基础图层id
|
||||
config.Zindex = index
|
||||
new maptalks.TileLayer(layerName, config).addTo(groupTileLayer)
|
||||
}
|
||||
})
|
||||
// 如果存在的图层数量比较大,就清空隐藏
|
||||
if (names.length < oldLayers.length) {
|
||||
for (let i = names.length; i < oldLayers.length; i++) {
|
||||
oldLayers[i].clear()
|
||||
oldLayers[i].hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加图层
|
||||
* @param layer
|
||||
*/
|
||||
addLayer(layer) {
|
||||
this.map.addLayer(layer)
|
||||
}
|
||||
/**
|
||||
* 放大
|
||||
*/
|
||||
zoomIn() {
|
||||
this.map.zoomIn()
|
||||
}
|
||||
/**
|
||||
* 缩小
|
||||
*/
|
||||
zoomOut() {
|
||||
this.map.zoomOut()
|
||||
}
|
||||
/**
|
||||
* 重置地图视角
|
||||
* @param {*} option 配置项
|
||||
*/
|
||||
resetView(option = {}) {
|
||||
const { center, zoom, pitch, bearing, duration } = option
|
||||
this.map.animateTo(
|
||||
{
|
||||
center: center || this.baseConfig.center,
|
||||
zoom: zoom || this.baseConfig.zoom,
|
||||
pitch: pitch || 0,
|
||||
bearing: bearing || 0
|
||||
}, {
|
||||
duration: duration || 500
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
飞行到地图中心
|
||||
@param center 可选目标中心坐标
|
||||
@param zoom 可选目标缩放级别
|
||||
@param duration 动画时长(毫秒),默认1000ms
|
||||
*/
|
||||
flyToCenter(center, zoom, duration = 1000) {
|
||||
const targetCenter = center
|
||||
const targetZoom = zoom
|
||||
this.map.animateTo(
|
||||
{ center: targetCenter, zoom: targetZoom },
|
||||
{
|
||||
duration: duration // 可根据需要添加其他动画配置
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有图层
|
||||
*/
|
||||
clearAllLayers() {
|
||||
const layers = this.map.getLayers()
|
||||
layers.forEach((layer) => {
|
||||
this.map.removeLayer(layer)
|
||||
})
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.map.remove()
|
||||
this.map = null
|
||||
GlobalMap.instance = null
|
||||
}
|
||||
}
|
||||
117
src/components/Map/js/GridLineInMap.js
Normal file
117
src/components/Map/js/GridLineInMap.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 基于maptalks的经纬网叠加
|
||||
*/
|
||||
import * as maptalks from 'maptalks'
|
||||
import { LineStringLayer, PointLayer } from '@maptalks/vt'
|
||||
|
||||
export default class GridLineInMap {
|
||||
constructor(map) {
|
||||
this.map = map
|
||||
// 经纬线
|
||||
this.line = new LineStringLayer('grid_line').addTo(this.map)
|
||||
// 经纬度文字
|
||||
this.text = new PointLayer('grid_point').addTo(this.map)
|
||||
|
||||
this._watchMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化经纬网格
|
||||
* @private
|
||||
*/
|
||||
_initGridLine() {
|
||||
const zoom = this.map.getZoom()
|
||||
this.line.clear()
|
||||
this.text.clear()
|
||||
|
||||
if (this.line.isVisible() && zoom > 2) {
|
||||
const extent = this.map.getExtent()
|
||||
let xstart = zoom < 8 ? Math.ceil(extent.xmin) : extent.xmin
|
||||
let ystart = zoom < 8 ? Math.ceil(extent.ymin) : extent.ymin
|
||||
|
||||
while (xstart < extent.xmax) {
|
||||
const longitude = new maptalks.LineString(
|
||||
[
|
||||
[ xstart, extent.ymin ],
|
||||
[ xstart, extent.ymax ]
|
||||
],
|
||||
{
|
||||
symbol: {
|
||||
lineColor: 'grey',
|
||||
lineWidth: 1,
|
||||
lineOpacity: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
const longitudeText = new maptalks.Marker([ xstart, extent.ymin ], {
|
||||
symbol: {
|
||||
textName: `${xstart}°`,
|
||||
textSize: 12,
|
||||
textFill: 'grey',
|
||||
textOpacity: 1,
|
||||
textDx: -20,
|
||||
textDy: -50
|
||||
}
|
||||
})
|
||||
this.line.addGeometry(longitude)
|
||||
this.text.addGeometry(longitudeText)
|
||||
xstart += 1
|
||||
}
|
||||
|
||||
while (ystart < extent.ymax) {
|
||||
const latitude = new maptalks.LineString(
|
||||
[
|
||||
[ extent.xmin, ystart ],
|
||||
[ extent.xmax, ystart ]
|
||||
],
|
||||
{
|
||||
symbol: {
|
||||
lineColor: 'grey',
|
||||
lineWidth: 1,
|
||||
lineOpacity: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
const latitudeText = new maptalks.Marker([ extent.xmax, ystart ], {
|
||||
symbol: {
|
||||
textName: `${ystart}°`,
|
||||
textSize: 12,
|
||||
textFill: 'grey',
|
||||
textOpacity: 1,
|
||||
textDx: 200,
|
||||
textDy: 20
|
||||
}
|
||||
})
|
||||
this.line.addGeometry(latitude)
|
||||
this.text.addGeometry(latitudeText)
|
||||
ystart += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 地图事件监听
|
||||
* @private
|
||||
*/
|
||||
_watchMap() {
|
||||
this._initGridLine()
|
||||
|
||||
this.map.on('viewchange', () => {
|
||||
this._initGridLine()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 经纬网显隐控制
|
||||
* @param visible
|
||||
*/
|
||||
toggleVisible(visible) {
|
||||
if (visible) {
|
||||
this.text.show()
|
||||
this.line.show()
|
||||
} else {
|
||||
this.text.hide()
|
||||
this.line.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
323
src/components/Map/js/MeasureToolInMap.js
Normal file
323
src/components/Map/js/MeasureToolInMap.js
Normal file
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* 基于maptalks测量工具(测距、测面积、电子方位线)
|
||||
*/
|
||||
import * as maptalks from 'maptalks'
|
||||
import DrawToolInMap from '@/components/Map/js/DrawToolInMap'
|
||||
|
||||
export default class MeasureToolInMap {
|
||||
constructor(map) {
|
||||
this.map = map
|
||||
this.layer = {
|
||||
area: null,
|
||||
distance: null,
|
||||
bearingLine: null
|
||||
}
|
||||
|
||||
this.drawTool = new DrawToolInMap(map) // 绘制工具(用于电子方位线)
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化电子方位线图层
|
||||
*/
|
||||
_initLayer() {
|
||||
const layer = this.map.getLayer('bearing_line')
|
||||
if (layer) {
|
||||
this.layer.bearingLine = layer
|
||||
} else {
|
||||
this.layer.bearingLine = new maptalks.VectorLayer('bearing_line').addTo(this.map)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制样式匹配
|
||||
* @param type
|
||||
* @return {{clearButtonSymbol: [{markerLineColor: string, markerLineWidth: number, markerHeight: number, markerDx: number, markerFill: string, markerWidth: number, markerType: string}, {markerLineColor: string, markerHeight: number, markerDx: number, markerWidth: number, markerType: string}], symbol: {polygonFill: string, lineColor: string, polygonOpacity: number, lineWidth: number}, vertexSymbol: {markerLineColor: string, markerLineWidth: number, markerHeight: number, markerFill: string, markerWidth: number, markerType: string}, language: string, labelOptions: {textSymbol: {textLineSpacing: number, textFill: string, textHorizontalAlignment: string, textDx: number, textFaceName: string}, boxStyle: {padding: number[], symbol: {markerLineColor: string, markerFill: string, markerFillOpacity: number, markerType: string}}}}|{clearButtonSymbol: [{markerLineColor: string, markerLineWidth: number, markerHeight: number, markerDx: number, markerFill: string, markerWidth: number, markerType: string}, {markerLineColor: string, markerHeight: number, markerDx: number, markerWidth: number, markerType: string}], symbol: {lineColor: string, lineWidth: number}, vertexSymbol: {markerLineColor: string, markerLineWidth: number, markerHeight: number, markerFill: string, markerWidth: number, markerType: string}, language: string, labelOptions: {textSymbol: {markerLineColor: string, textLineSpacing: number, textFill: string, textHorizontalAlignment: string, textDx: number, markerFill: string, textFaceName: string}, boxStyle: {padding: number[], symbol: {markerLineColor: string, markerFill: string, markerFillOpacity: number, markerType: string}}}}}
|
||||
* @private
|
||||
*/
|
||||
_formatSymbol(type) {
|
||||
// 测面积样式
|
||||
const AreaToolConf = {
|
||||
symbol: {
|
||||
lineColor: '#1bbc9b',
|
||||
lineWidth: 2,
|
||||
polygonFill: '#fff',
|
||||
polygonOpacity: 0.3
|
||||
},
|
||||
vertexSymbol: {
|
||||
markerType: 'ellipse',
|
||||
markerFill: '#34495e',
|
||||
markerLineColor: '#1bbc9b',
|
||||
markerLineWidth: 3,
|
||||
markerWidth: 10,
|
||||
markerHeight: 10
|
||||
},
|
||||
labelOptions: {
|
||||
textSymbol: {
|
||||
textFaceName: 'monospace',
|
||||
textFill: '#fff',
|
||||
textLineSpacing: 1,
|
||||
textHorizontalAlignment: 'right',
|
||||
textDx: 15
|
||||
},
|
||||
boxStyle: {
|
||||
padding: [ 6, 2 ],
|
||||
symbol: {
|
||||
markerType: 'square',
|
||||
markerFill: '#000',
|
||||
markerFillOpacity: 0.9,
|
||||
markerLineColor: '#b4b3b3'
|
||||
}
|
||||
}
|
||||
},
|
||||
clearButtonSymbol: [
|
||||
{
|
||||
markerType: 'square',
|
||||
markerFill: '#000',
|
||||
markerLineColor: '#b4b3b3',
|
||||
markerLineWidth: 2,
|
||||
markerWidth: 15,
|
||||
markerHeight: 15,
|
||||
markerDx: 22
|
||||
},
|
||||
{
|
||||
markerType: 'x',
|
||||
markerWidth: 10,
|
||||
markerHeight: 10,
|
||||
markerLineColor: '#fff',
|
||||
markerDx: 22
|
||||
}
|
||||
],
|
||||
language: 'zh-CN'
|
||||
}
|
||||
|
||||
// 测距样式
|
||||
const DistanceToolConf = {
|
||||
symbol: {
|
||||
lineColor: '#34495e',
|
||||
lineWidth: 2
|
||||
},
|
||||
vertexSymbol: {
|
||||
markerType: 'ellipse',
|
||||
markerFill: '#1bbc9b',
|
||||
markerLineColor: '#000',
|
||||
markerLineWidth: 3,
|
||||
markerWidth: 10,
|
||||
markerHeight: 10
|
||||
},
|
||||
labelOptions: {
|
||||
textSymbol: {
|
||||
textFaceName: 'monospace',
|
||||
textFill: '#fff',
|
||||
textLineSpacing: 1,
|
||||
textHorizontalAlignment: 'right',
|
||||
textDx: 15,
|
||||
markerLineColor: '#b4b3b3',
|
||||
markerFill: '#000'
|
||||
},
|
||||
boxStyle: {
|
||||
padding: [ 6, 2 ],
|
||||
symbol: {
|
||||
markerType: 'square',
|
||||
markerFill: '#000',
|
||||
markerFillOpacity: 0.9,
|
||||
markerLineColor: '#b4b3b3'
|
||||
}
|
||||
}
|
||||
},
|
||||
clearButtonSymbol: [
|
||||
{
|
||||
markerType: 'square',
|
||||
markerFill: '#000',
|
||||
markerLineColor: '#b4b3b3',
|
||||
markerLineWidth: 2,
|
||||
markerWidth: 15,
|
||||
markerHeight: 15,
|
||||
markerDx: 20
|
||||
},
|
||||
{
|
||||
markerType: 'x',
|
||||
markerWidth: 10,
|
||||
markerHeight: 10,
|
||||
markerLineColor: '#fff',
|
||||
markerDx: 20
|
||||
}
|
||||
],
|
||||
language: 'zh-CN'
|
||||
}
|
||||
|
||||
return type === 'area' ? AreaToolConf : DistanceToolConf
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁
|
||||
*/
|
||||
destroy() {
|
||||
const layers = this.layer
|
||||
const { area, distance, bearingLine } = layers
|
||||
|
||||
// 移除测面工具
|
||||
if (area) {
|
||||
area.clear().remove()
|
||||
layers.area = null
|
||||
}
|
||||
|
||||
// 移除测距工具
|
||||
if (distance) {
|
||||
distance.clear().remove()
|
||||
layers.distance = null
|
||||
}
|
||||
|
||||
// 清空电子方位线并销毁绘制工具
|
||||
if (bearingLine) {
|
||||
bearingLine.clear().remove()
|
||||
layers.bearingLine = null
|
||||
this.drawTool.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换工具
|
||||
* @param {'area'|'bearingLine'|'distance'} type 类型
|
||||
*/
|
||||
toggleTool(type) {
|
||||
const layers = this.layer
|
||||
let area = layers.area
|
||||
let distance = layers.distance
|
||||
const bearingLine = layers.bearingLine
|
||||
|
||||
switch (type) {
|
||||
case 'area': {
|
||||
// 测面
|
||||
// 关闭测距
|
||||
if (distance) {
|
||||
distance.clear().remove()
|
||||
layers.distance = null
|
||||
}
|
||||
// 关闭电子方位线
|
||||
if (bearingLine) {
|
||||
bearingLine.clear().remove()
|
||||
layers.bearingLine = null
|
||||
this.drawTool.remove()
|
||||
}
|
||||
|
||||
if (area) {
|
||||
area.clear().remove()
|
||||
layers.area = null
|
||||
} else {
|
||||
area = new maptalks.AreaTool(this._formatSymbol(type)).addTo(this.map)
|
||||
layers.area = area
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'bearingLine': {
|
||||
// 电子方位线
|
||||
if (area) {
|
||||
area.clear().remove()
|
||||
layers.area = null
|
||||
}
|
||||
if (distance) {
|
||||
// 关闭测距
|
||||
distance.clear().remove()
|
||||
layers.distance = null
|
||||
}
|
||||
|
||||
if (bearingLine) {
|
||||
bearingLine.clear().remove()
|
||||
layers.bearingLine = null
|
||||
this.drawTool.remove()
|
||||
} else {
|
||||
this._initLayer()
|
||||
let center = null
|
||||
const start = (e) => {
|
||||
center = e.viewPoint
|
||||
}
|
||||
const callback = (e) => {
|
||||
this.drawTool.clear()
|
||||
|
||||
const coordinate = e.geometry.getCoordinates()
|
||||
const _radius = e.geometry.getRadius()
|
||||
const radius = (_radius / 1852).toFixed(3)
|
||||
const current = e.viewPoint
|
||||
const _angle = Math.atan2(
|
||||
current.y - center.y,
|
||||
current.x - center.x
|
||||
)
|
||||
const angle = ((_angle * 360 / 2 / Math.PI + 450) % 360).toFixed(
|
||||
1
|
||||
)
|
||||
const symbol = [
|
||||
{
|
||||
lineColor: 'red',
|
||||
lineWidth: 2,
|
||||
polygonFill: 'white',
|
||||
polygonOpacity: 0.3
|
||||
},
|
||||
{
|
||||
textName: `方位${angle}°,距离${radius}海里`,
|
||||
textSize: 18
|
||||
}
|
||||
]
|
||||
const circle = new maptalks.Circle(coordinate, _radius, {
|
||||
symbol
|
||||
})
|
||||
const point = this.map.viewPointToCoord(current)
|
||||
const line = new maptalks.LineString([ coordinate, point ], {
|
||||
symbol: {
|
||||
lineColor: 'red',
|
||||
lineWidth: 2
|
||||
}
|
||||
})
|
||||
this.geometry = new maptalks.GeometryCollection([ line, circle ])
|
||||
bearingLine.clear().addGeometry(this.geometry)
|
||||
}
|
||||
this.drawTool.toggleDraw({
|
||||
drawend: callback,
|
||||
drawstart: start,
|
||||
mode: 'Circle',
|
||||
mousemove: callback,
|
||||
once: false,
|
||||
symbol: [
|
||||
{
|
||||
lineColor: 'red',
|
||||
lineWidth: 2,
|
||||
polygonFill: 'white',
|
||||
polygonOpacity: 0.1
|
||||
},
|
||||
{
|
||||
textName: '',
|
||||
textSize: 18
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'distance': {
|
||||
// 测距
|
||||
// 关闭测面积
|
||||
if (area) {
|
||||
area.clear().remove()
|
||||
layers.area = null
|
||||
}
|
||||
// 关闭电子方位线
|
||||
if (bearingLine) {
|
||||
bearingLine.clear().remove()
|
||||
layers.bearingLine = null
|
||||
this.drawTool.remove()
|
||||
}
|
||||
|
||||
if (distance) {
|
||||
distance.clear().remove()
|
||||
layers.distance = null
|
||||
} else {
|
||||
distance = new maptalks.DistanceTool(this._formatSymbol(type)).addTo(this.map)
|
||||
layers.distance = distance
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
331
src/components/Map/js/ShipPathInMap.js
Normal file
331
src/components/Map/js/ShipPathInMap.js
Normal file
@@ -0,0 +1,331 @@
|
||||
|
||||
/**
|
||||
* 轨迹回放功能-基于maptalks
|
||||
* 依赖:maptalks.routeplayer
|
||||
* 初始化渲染渔船轨迹
|
||||
*/
|
||||
import * as maptalks from 'maptalks'
|
||||
import { RoutePlayer, formatRouteData } from 'maptalks.routeplayer'
|
||||
import { getAssetsFile } from '@/utils/common'
|
||||
|
||||
export default class ShipPathInMap {
|
||||
constructor(map, layer, list = []) {
|
||||
this.map = map
|
||||
this.list = list // 轨迹数据
|
||||
this.track = layer // 图层
|
||||
this.playerList = [] // 播放器
|
||||
this.addTrackToMap() // 暂时注释
|
||||
}
|
||||
|
||||
// 渔船轨迹叠加
|
||||
addTrackToMap() {
|
||||
// 图层清空
|
||||
this.track.clear()
|
||||
// 如果超过了十个,那么就自动生成一个新的配色方案
|
||||
const colors = [ '#0052D9', '#EBB105', '#00A870', '#0594FA', '#ED7B2F', '#E34D59', '#ED49B4', '#834EC2' ]
|
||||
if (this.list.length > 10) {
|
||||
// 这个地方就要叠加颜色属性了
|
||||
const number = this.list.length - 10
|
||||
for (let i = 0; i < number; i += 1) {
|
||||
const r = Math.round(Math.random() * 255)
|
||||
const g = Math.round(Math.random() * 255)
|
||||
const b = Math.round(Math.random() * 255)
|
||||
colors.push(`rgb(${r}, ${g}, ${b})`)
|
||||
}
|
||||
}
|
||||
|
||||
this.list.forEach((list, iindex) => {
|
||||
const markers = [] // 轨迹数据叠加
|
||||
let lastPoint = null // 为了构造轨迹线
|
||||
const collection = []
|
||||
// 构造线段样式
|
||||
const options = this.formatLineSymbol(colors[iindex])
|
||||
list.map((item, index) => {
|
||||
// 轨迹点构造
|
||||
const marker = new maptalks.Marker([ item.longitude, item.latitude ], {
|
||||
symbol: {
|
||||
markerLineColor: colors[iindex],
|
||||
markerLineWidth: 1,
|
||||
markerFill: colors[iindex],
|
||||
markerType: 'ellipse',
|
||||
markerHeight: 5,
|
||||
markerWidth: 5
|
||||
}
|
||||
})
|
||||
marker.on('mouseenter', (e) => {
|
||||
marker.setInfoWindow({
|
||||
content: `
|
||||
<div class="info-window">渔船名称:${item.boatName}</div>
|
||||
<div class="info-window">渔船编号:${item.terminalCode}</div>
|
||||
<div class="info-window">定位时间:${item.gpsTime}</div>
|
||||
`
|
||||
})
|
||||
|
||||
marker.openInfoWindow()
|
||||
})
|
||||
markers.push(marker)
|
||||
|
||||
// 起始点位叠加
|
||||
if (!index || index === list.length - 1) {
|
||||
const point = new maptalks.Marker([ item.longitude, item.latitude ], {
|
||||
zIndex: 10,
|
||||
symbol: {
|
||||
markerFile: getAssetsFile(
|
||||
`map/track/icon-${!index ? 'end' : 'start'}-point.png`
|
||||
),
|
||||
markerHeight: 30,
|
||||
markerWidth: 30
|
||||
}
|
||||
})
|
||||
markers.push(point)
|
||||
}
|
||||
|
||||
// 轨迹线
|
||||
// 根据时间判断轨迹线
|
||||
let timeStart = ''
|
||||
let timeEnd = ''
|
||||
|
||||
const currentPoint = [ item.longitude, item.latitude ]
|
||||
|
||||
let lineStart = options.line[0]
|
||||
let lineEnd = options.line[0]
|
||||
if (index > 1) {
|
||||
timeStart = list[index - 1].time
|
||||
timeEnd = item.time
|
||||
}
|
||||
const timerange = timeStart - timeEnd
|
||||
if (index > 1) {
|
||||
if (timerange > 1000 * 60 * 60 * 2) {
|
||||
[ lineStart, lineEnd ] = options.line
|
||||
} else {
|
||||
[ lineStart, lineEnd ] = options.straight
|
||||
}
|
||||
} else {
|
||||
[ lineStart, lineEnd ] = options.line
|
||||
}
|
||||
|
||||
if (index) {
|
||||
collection.push(
|
||||
new maptalks.LineString(
|
||||
[
|
||||
currentPoint,
|
||||
[
|
||||
(lastPoint[0] + currentPoint[0]) / 2,
|
||||
(lastPoint[1] + currentPoint[1]) / 2
|
||||
]
|
||||
],
|
||||
lineStart
|
||||
),
|
||||
new maptalks.LineString(
|
||||
[
|
||||
[
|
||||
(lastPoint[0] + currentPoint[0]) / 2,
|
||||
(lastPoint[1] + currentPoint[1]) / 2
|
||||
],
|
||||
lastPoint
|
||||
],
|
||||
lineEnd
|
||||
)
|
||||
)
|
||||
}
|
||||
lastPoint = currentPoint
|
||||
return [ item.longitude, item.latitude ]
|
||||
})
|
||||
this.track.addGeometry([ ...collection, ...markers ])
|
||||
this.track.setZIndex(10)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算线段样式
|
||||
* @param color
|
||||
* @private
|
||||
*/
|
||||
formatLineSymbol(color) {
|
||||
return {
|
||||
line: [
|
||||
{
|
||||
arrowStyle: [ 2, 3 ],
|
||||
arrowPlacement: 'vertex-last',
|
||||
visible: true,
|
||||
zIndex: 10,
|
||||
symbol: {
|
||||
lineColor: color,
|
||||
lineWidth: 1,
|
||||
lineDasharray: [ 4, 2, 4 ],
|
||||
lineOpacity: 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
arrowStyle: null,
|
||||
visible: true,
|
||||
zIndex: 10,
|
||||
symbol: {
|
||||
lineColor: color,
|
||||
lineWidth: 1,
|
||||
lineDasharray: [ 4, 2, 4 ],
|
||||
lineOpacity: 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
straight: [
|
||||
{
|
||||
arrowStyle: [ 2, 3 ],
|
||||
arrowPlacement: 'vertex-last',
|
||||
visible: true,
|
||||
zIndex: 10,
|
||||
symbol: {
|
||||
lineColor: color,
|
||||
lineWidth: 1,
|
||||
lineOpacity: 0.9
|
||||
}
|
||||
},
|
||||
{
|
||||
arrowStyle: null,
|
||||
visible: true,
|
||||
zIndex: 10,
|
||||
symbol: {
|
||||
lineColor: color,
|
||||
lineWidth: 1,
|
||||
lineOpacity: 0.9
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 速度改变
|
||||
* @param {Number} speed
|
||||
*/
|
||||
changeSpeed(speed) {
|
||||
this.playerList.forEach((item) => {
|
||||
const { player } = item
|
||||
player.setSpeed(speed)
|
||||
})
|
||||
}
|
||||
|
||||
// 清除轨迹
|
||||
clearPath() {
|
||||
this.track.clear()
|
||||
}
|
||||
|
||||
// 播放器销毁
|
||||
destroy() {
|
||||
this.playerList.forEach((item) => {
|
||||
const { player } = item
|
||||
player.remove()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化轨迹path
|
||||
* @param {Number} speed
|
||||
* @private
|
||||
* @param path
|
||||
* @return {[any, any, number][]}
|
||||
*/
|
||||
formatDuration(speed, path, time) {
|
||||
// 因为轨迹数据是顺序的,但是播放时需要倒序,所以根据轨迹数据计算出的各个线段的播放时间也要处理成倒序
|
||||
// 计算每线段的持续时间-数组倒序
|
||||
const data = [ ...path ].reverse()
|
||||
const startTime = new Date(time[0]).getTime()
|
||||
const endTime = new Date(time[1]).getTime()
|
||||
const start = {
|
||||
coordinate: [ data[0].longitude, data[0].latitude, 0 ],
|
||||
time: 0
|
||||
}
|
||||
const end = {
|
||||
coordinate: [ path[0].longitude, path[0].latitude, 0 ],
|
||||
time: endTime - startTime
|
||||
}
|
||||
const list = data.map((item) => {
|
||||
const duration = item.time - startTime
|
||||
return {
|
||||
coordinate: [ item.longitude, item.latitude, 0 ],
|
||||
time: duration
|
||||
}
|
||||
})
|
||||
return [ start, ...list, end ]
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前轨迹点位置
|
||||
* @private
|
||||
* @param info 当前轨迹数据
|
||||
* @param marker 标注点
|
||||
*/
|
||||
updateMarkerPosition(info, marker) {
|
||||
const { coordinate } = info
|
||||
if (!marker) {
|
||||
return
|
||||
}
|
||||
marker.setCoordinates(coordinate)
|
||||
}
|
||||
|
||||
// 开始
|
||||
start() {
|
||||
this.playerList.forEach((item) => {
|
||||
const { player } = item
|
||||
player.play()
|
||||
})
|
||||
}
|
||||
|
||||
// 播放完毕后需要自己初始化一下
|
||||
stop() {
|
||||
this.playerList.forEach((item) => {
|
||||
const { player } = item
|
||||
player.cancel()
|
||||
})
|
||||
}
|
||||
|
||||
// 暂停
|
||||
pause() {
|
||||
this.playerList.forEach((item) => {
|
||||
const { player } = item
|
||||
player.pause()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放器构造/更新
|
||||
* @param {Number} speed
|
||||
* @param {Array} time
|
||||
*/
|
||||
updatePlayer(speed, time) {
|
||||
// 首先销毁播放器
|
||||
this.destroy()
|
||||
|
||||
// 播放器构造
|
||||
const players = this.list.map((item) => {
|
||||
const path = formatRouteData(this.formatDuration(speed, item, time))
|
||||
return new RoutePlayer(path, {
|
||||
speed
|
||||
})
|
||||
})
|
||||
this.playerList = []
|
||||
|
||||
players.forEach((player) => {
|
||||
const info = player.getStartInfo()
|
||||
const marker = new maptalks.Marker(info.coordinate, {
|
||||
zIndex: 10,
|
||||
symbol: {
|
||||
markerType: 'ellipse',
|
||||
markerFill: 'red',
|
||||
markerLineWidth: 0,
|
||||
markerHeight: 15,
|
||||
markerWidth: 15
|
||||
}
|
||||
})
|
||||
this.track.addGeometry(marker)
|
||||
this.playerList.push({
|
||||
player,
|
||||
marker
|
||||
})
|
||||
this.updateMarkerPosition(info, marker)
|
||||
player.on('playing', (e) => {
|
||||
this.updateMarkerPosition(e, marker)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
481
src/components/Map/js/mock.js
Normal file
481
src/components/Map/js/mock.js
Normal file
@@ -0,0 +1,481 @@
|
||||
// 无人机数据
|
||||
const uavs = [ {
|
||||
id: 1,
|
||||
videoName: '浙江-温州-鹿城山福临江-无人机',
|
||||
longitude: 120.553107,
|
||||
latitude: 28.133002,
|
||||
'runStatus': '离线'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
videoName: '浙江-温州-永嘉瓯北码头-无人机',
|
||||
longitude: 120.656171,
|
||||
latitude: 28.037441,
|
||||
'runStatus': '离线'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
videoName: '浙江-温州-永嘉梅岙镇-无人机',
|
||||
longitude: 120.583272,
|
||||
latitude: 28.111125,
|
||||
'runStatus': '离线'
|
||||
} ]
|
||||
// 监控数据
|
||||
const monitors = [
|
||||
{
|
||||
'alarmVideoName': '低点抓拍枪型摄像机',
|
||||
'createAt': '2025-09-17 14:58:17',
|
||||
'createBy': 'admin',
|
||||
'delFlag': 0,
|
||||
'departId': '',
|
||||
'departName': '',
|
||||
'id': 2115,
|
||||
'innetType': '政务网',
|
||||
'isAnalyze': 0,
|
||||
'isAr': 0,
|
||||
'isAvailable': '否',
|
||||
'isSdkControl': 1,
|
||||
'latitude': 28.11112500,
|
||||
'layoutArea': '',
|
||||
'longitude': 120.58327200,
|
||||
'playBack': null,
|
||||
'previewUrl': 'rtsp://admin:123456@192.168.1.24/Streaming/channels/301',
|
||||
'rangeInfo': '',
|
||||
'remark': 'DS-2CD7C447MWD-XZG',
|
||||
'runStatus': '离线',
|
||||
'sensorType': '可见光',
|
||||
'sourceType': '海康',
|
||||
'updateAt': '2025-09-29 09:48:53',
|
||||
'updateBy': 'admin',
|
||||
'videoBelong': '永嘉梅岙镇',
|
||||
'videoCode': 'b6571de7c15d487b98ad583cb3b5d787',
|
||||
'videoImage': '',
|
||||
'videoName': '浙江-温州-永嘉梅岙镇',
|
||||
'videoType': '云台',
|
||||
'videosAccount': 'admin',
|
||||
'videosIp': '198.16.74.180',
|
||||
'videosPass': '123456',
|
||||
'videosPort': '8000'
|
||||
},
|
||||
{
|
||||
'alarmVideoName': '中点黑光球机',
|
||||
'createAt': '2025-09-17 14:58:20',
|
||||
'createBy': 'admin',
|
||||
'delFlag': 0,
|
||||
'departId': '',
|
||||
'departName': '',
|
||||
'id': 2116,
|
||||
'innetType': '政务网',
|
||||
'isAnalyze': 0,
|
||||
'isAr': 0,
|
||||
'isAvailable': '否',
|
||||
'isSdkControl': 0,
|
||||
'latitude': 28.02815266,
|
||||
'layoutArea': '',
|
||||
'longitude': 120.60151498,
|
||||
'playBack': null,
|
||||
'previewUrl': 'rtsp://admin:123456@192.168.1.25/Streaming/channels/301',
|
||||
'rangeInfo': '',
|
||||
'remark': 'iDS-2DF9C45318XS-DFW-',
|
||||
'runStatus': '离线',
|
||||
'sensorType': '可见光',
|
||||
'sourceType': '海康',
|
||||
'updateAt': '2025-09-29 09:48:59',
|
||||
'updateBy': 'admin',
|
||||
'videoBelong': '卧旗山三管塔',
|
||||
'videoCode': 'b6571de7c15d487b98ad583cb3b5d786',
|
||||
'videoImage': '',
|
||||
'videoName': '浙江-温州-卧旗山',
|
||||
'videoType': '球机',
|
||||
'videosAccount': 'admin',
|
||||
'videosIp': '198.16.74.182',
|
||||
'videosPass': '123456',
|
||||
'videosPort': '8000'
|
||||
},
|
||||
{
|
||||
'alarmVideoName': '中点黑光球机',
|
||||
'createAt': '2025-09-17 14:58:20',
|
||||
'createBy': 'admin',
|
||||
'delFlag': 0,
|
||||
'departId': '',
|
||||
'departName': '',
|
||||
'id': 2117,
|
||||
'innetType': '政务网',
|
||||
'isAnalyze': 0,
|
||||
'isAr': 0,
|
||||
'isAvailable': '否',
|
||||
'isSdkControl': 0,
|
||||
'latitude': 28.02631300,
|
||||
'layoutArea': '',
|
||||
'longitude': 120.64675000,
|
||||
'playBack': null,
|
||||
'previewUrl': 'rtsp://admin:123456@192.168.1.25/Streaming/channels/301',
|
||||
'rangeInfo': '',
|
||||
'remark': 'iDS-2DF9C45318XS-DFW-',
|
||||
'runStatus': '离线',
|
||||
'sensorType': '可见光',
|
||||
'sourceType': '海康',
|
||||
'updateAt': '2025-09-29 09:49:05',
|
||||
'updateBy': 'admin',
|
||||
'videoBelong': '温州鹿城屯前江润大楼',
|
||||
'videoCode': 'b6571de7c15d487b98ad583cb3b5d786',
|
||||
'videoImage': '',
|
||||
'videoName': '浙江-温州-鹿城屯前江润大楼-球机',
|
||||
'videoType': '球机',
|
||||
'videosAccount': 'admin',
|
||||
'videosIp': '198.16.74.184',
|
||||
'videosPass': '123456',
|
||||
'videosPort': '8000'
|
||||
},
|
||||
{
|
||||
'alarmVideoName': '中点黑光球机',
|
||||
'createAt': '2025-09-17 14:58:20',
|
||||
'createBy': 'admin',
|
||||
'delFlag': 0,
|
||||
'departId': '',
|
||||
'departName': '',
|
||||
'id': 2118,
|
||||
'innetType': '政务网',
|
||||
'isAnalyze': 0,
|
||||
'isAr': 0,
|
||||
'isAvailable': '否',
|
||||
'isSdkControl': 0,
|
||||
'latitude': 28.02631300,
|
||||
'layoutArea': '',
|
||||
'longitude': 120.64675000,
|
||||
'playBack': null,
|
||||
'previewUrl': 'rtsp://admin:123456@192.168.1.25/Streaming/channels/301',
|
||||
'rangeInfo': '',
|
||||
'remark': 'iDS-2DF9C45318XS-DFW-',
|
||||
'runStatus': '离线',
|
||||
'sensorType': '可见光',
|
||||
'sourceType': '海康',
|
||||
'updateAt': '2025-09-29 09:49:07',
|
||||
'updateBy': 'admin',
|
||||
'videoBelong': '温州鹿城屯前江润大楼',
|
||||
'videoCode': 'b6571de7c15d487b98ad583cb3b5d786',
|
||||
'videoImage': '',
|
||||
'videoName': '浙江-温州-鹿城屯前江润大楼-球机',
|
||||
'videoType': '球机',
|
||||
'videosAccount': 'admin',
|
||||
'videosIp': '198.16.74.185',
|
||||
'videosPass': '123456',
|
||||
'videosPort': '8000'
|
||||
},
|
||||
{
|
||||
'alarmVideoName': '中点黑光球机',
|
||||
'createAt': '2025-09-17 14:58:20',
|
||||
'createBy': 'admin',
|
||||
'delFlag': 0,
|
||||
'departId': '',
|
||||
'departName': '',
|
||||
'id': 2119,
|
||||
'innetType': '政务网',
|
||||
'isAnalyze': 0,
|
||||
'isAr': 0,
|
||||
'isAvailable': '否',
|
||||
'isSdkControl': 0,
|
||||
'latitude': 28.02631300,
|
||||
'layoutArea': '',
|
||||
'longitude': 120.64675000,
|
||||
'playBack': null,
|
||||
'previewUrl': 'rtsp://admin:123456@192.168.1.25/Streaming/channels/301',
|
||||
'rangeInfo': '',
|
||||
'remark': 'iDS-2DF9C45318XS-DFW-',
|
||||
'runStatus': '离线',
|
||||
'sensorType': '可见光',
|
||||
'sourceType': '海康',
|
||||
'updateAt': '2025-09-29 09:49:10',
|
||||
'updateBy': 'admin',
|
||||
'videoBelong': '温州鹿城屯前江润大楼',
|
||||
'videoCode': 'b6571de7c15d487b98ad583cb3b5d786',
|
||||
'videoImage': '',
|
||||
'videoName': '浙江-温州-鹿城屯前江润大楼-球机',
|
||||
'videoType': '球机',
|
||||
'videosAccount': 'admin',
|
||||
'videosIp': '198.16.74.186',
|
||||
'videosPass': '123456',
|
||||
'videosPort': '8000'
|
||||
},
|
||||
{
|
||||
'alarmVideoName': '中点黑光球机',
|
||||
'createAt': '2025-09-17 14:58:20',
|
||||
'createBy': 'admin',
|
||||
'delFlag': 0,
|
||||
'departId': '',
|
||||
'departName': '',
|
||||
'id': 2120,
|
||||
'innetType': '政务网',
|
||||
'isAnalyze': 0,
|
||||
'isAr': 0,
|
||||
'isAvailable': '否',
|
||||
'isSdkControl': 0,
|
||||
'latitude': 28.02631300,
|
||||
'layoutArea': '',
|
||||
'longitude': 120.64675000,
|
||||
'playBack': null,
|
||||
'previewUrl': 'rtsp://admin:123456@192.168.1.25/Streaming/channels/301',
|
||||
'rangeInfo': '',
|
||||
'remark': 'iDS-2DF9C45318XS-DFW-',
|
||||
'runStatus': '离线',
|
||||
'sensorType': '可见光',
|
||||
'sourceType': '海康',
|
||||
'updateAt': '2025-09-29 09:49:12',
|
||||
'updateBy': 'admin',
|
||||
'videoBelong': '温州鹿城屯前江润大楼',
|
||||
'videoCode': 'b6571de7c15d487b98ad583cb3b5d786',
|
||||
'videoImage': '',
|
||||
'videoName': '浙江-温州-鹿城屯前江润大楼-云台',
|
||||
'videoType': '云台',
|
||||
'videosAccount': 'admin',
|
||||
'videosIp': '198.16.74.187',
|
||||
'videosPass': '123456',
|
||||
'videosPort': '8000'
|
||||
},
|
||||
{
|
||||
'alarmVideoName': '中点黑光球机',
|
||||
'createAt': '2025-09-17 14:58:20',
|
||||
'createBy': 'admin',
|
||||
'delFlag': 0,
|
||||
'departId': '',
|
||||
'departName': '',
|
||||
'id': 2121,
|
||||
'innetType': '政务网',
|
||||
'isAnalyze': 0,
|
||||
'isAr': 0,
|
||||
'isAvailable': '否',
|
||||
'isSdkControl': 0,
|
||||
'latitude': 28.12544400,
|
||||
'layoutArea': '',
|
||||
'longitude': 120.56137100,
|
||||
'playBack': null,
|
||||
'previewUrl': 'rtsp://admin:123456@192.168.1.25/Streaming/channels/301',
|
||||
'rangeInfo': '',
|
||||
'remark': 'iDS-2DF9C45318XS-DFW-',
|
||||
'runStatus': '离线',
|
||||
'sensorType': '可见光',
|
||||
'sourceType': '海康',
|
||||
'updateAt': '2025-09-29 09:49:19',
|
||||
'updateBy': 'admin',
|
||||
'videoBelong': '永嘉桥下坦头南',
|
||||
'videoCode': 'b6571de7c15d487b98ad583cb3b5d786',
|
||||
'videoImage': '',
|
||||
'videoName': '浙江-温州-永嘉桥下坦头南-球机',
|
||||
'videoType': '球机',
|
||||
'videosAccount': 'admin',
|
||||
'videosIp': '198.16.74.188',
|
||||
'videosPass': '123456',
|
||||
'videosPort': '8000'
|
||||
},
|
||||
{
|
||||
'alarmVideoName': '中点黑光球机',
|
||||
'createAt': '2025-09-17 14:58:20',
|
||||
'createBy': 'admin',
|
||||
'delFlag': 0,
|
||||
'departId': '',
|
||||
'departName': '',
|
||||
'id': 2122,
|
||||
'innetType': '政务网',
|
||||
'isAnalyze': 0,
|
||||
'isAr': 0,
|
||||
'isAvailable': '否',
|
||||
'isSdkControl': 0,
|
||||
'latitude': 28.12544400,
|
||||
'layoutArea': '',
|
||||
'longitude': 120.56137100,
|
||||
'playBack': null,
|
||||
'previewUrl': 'rtsp://admin:123456@192.168.1.25/Streaming/channels/301',
|
||||
'rangeInfo': '',
|
||||
'remark': 'iDS-2DF9C45318XS-DFW-',
|
||||
'runStatus': '离线',
|
||||
'sensorType': '可见光',
|
||||
'sourceType': '海康',
|
||||
'updateAt': '2025-09-29 09:49:20',
|
||||
'updateBy': 'admin',
|
||||
'videoBelong': '永嘉桥下坦头南',
|
||||
'videoCode': 'b6571de7c15d487b98ad583cb3b5d786',
|
||||
'videoImage': '',
|
||||
'videoName': '浙江-温州-永嘉桥下坦头南-球机',
|
||||
'videoType': '球机',
|
||||
'videosAccount': 'admin',
|
||||
'videosIp': '198.16.74.189',
|
||||
'videosPass': '123456',
|
||||
'videosPort': '8000'
|
||||
},
|
||||
{
|
||||
'alarmVideoName': '中点黑光球机',
|
||||
'createAt': '2025-09-17 14:58:20',
|
||||
'createBy': 'admin',
|
||||
'delFlag': 0,
|
||||
'departId': '',
|
||||
'departName': '',
|
||||
'id': 2123,
|
||||
'innetType': '政务网',
|
||||
'isAnalyze': 0,
|
||||
'isAr': 0,
|
||||
'isAvailable': '否',
|
||||
'isSdkControl': 0,
|
||||
'latitude': 28.12544400,
|
||||
'layoutArea': '',
|
||||
'longitude': 120.56137100,
|
||||
'playBack': null,
|
||||
'previewUrl': 'rtsp://admin:123456@192.168.1.25/Streaming/channels/301',
|
||||
'rangeInfo': '',
|
||||
'remark': 'iDS-2DF9C45318XS-DFW-',
|
||||
'runStatus': '离线',
|
||||
'sensorType': '可见光',
|
||||
'sourceType': '海康',
|
||||
'updateAt': '2025-09-29 09:49:22',
|
||||
'updateBy': 'admin',
|
||||
'videoBelong': '永嘉桥下坦头南',
|
||||
'videoCode': 'b6571de7c15d487b98ad583cb3b5d786',
|
||||
'videoImage': '',
|
||||
'videoName': '浙江-温州-永嘉桥下坦头南-球机',
|
||||
'videoType': '球机',
|
||||
'videosAccount': 'admin',
|
||||
'videosIp': '198.16.74.190',
|
||||
'videosPass': '123456',
|
||||
'videosPort': '8000'
|
||||
},
|
||||
{
|
||||
'alarmVideoName': '中点黑光球机',
|
||||
'createAt': '2025-09-17 14:58:20',
|
||||
'createBy': 'admin',
|
||||
'delFlag': 0,
|
||||
'departId': '',
|
||||
'departName': '',
|
||||
'id': 2124,
|
||||
'innetType': '政务网',
|
||||
'isAnalyze': 0,
|
||||
'isAr': 0,
|
||||
'isAvailable': '否',
|
||||
'isSdkControl': 0,
|
||||
'latitude': 28.12544400,
|
||||
'layoutArea': '',
|
||||
'longitude': 120.56137100,
|
||||
'playBack': null,
|
||||
'previewUrl': 'rtsp://admin:123456@192.168.1.25/Streaming/channels/301',
|
||||
'rangeInfo': '',
|
||||
'remark': 'iDS-2DF9C45318XS-DFW-',
|
||||
'runStatus': '离线',
|
||||
'sensorType': '可见光',
|
||||
'sourceType': '海康',
|
||||
'updateAt': '2025-09-29 09:49:25',
|
||||
'updateBy': 'admin',
|
||||
'videoBelong': '永嘉桥下坦头南',
|
||||
'videoCode': 'b6571de7c15d487b98ad583cb3b5d786',
|
||||
'videoImage': '',
|
||||
'videoName': '浙江-温州-永嘉桥下坦头南-云台',
|
||||
'videoType': '云台',
|
||||
'videosAccount': 'admin',
|
||||
'videosIp': '198.16.74.191',
|
||||
'videosPass': '123456',
|
||||
'videosPort': '8000'
|
||||
}
|
||||
]
|
||||
// ais基站数据
|
||||
const stations = [ {
|
||||
id: 5,
|
||||
name: '基站1',
|
||||
longitude: 120.657478,
|
||||
latitude: 28.037785
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '基站2',
|
||||
longitude: 120.682618,
|
||||
latitude: 28.040537
|
||||
} ]
|
||||
// 环境监测设备数据
|
||||
const environmentals = [ {
|
||||
id: 7,
|
||||
name: '环境监测设备1',
|
||||
longitude: 120.685736,
|
||||
latitude: 28.021787
|
||||
} ]
|
||||
// 电子围栏数据
|
||||
const fences = [ {
|
||||
'diaphaneity': '0.5',
|
||||
'fillColor': '#07F6E945',
|
||||
'id': '1967830494361186306',
|
||||
'latitude': '28.022472',
|
||||
'lineColor': '#00FDF0FF',
|
||||
'lineWidth': '3',
|
||||
'longitude': '120.731175',
|
||||
'positionInfo':
|
||||
'120.51471127064674,28.13957549396699;120.51890739842734,28.13587527906429;120.53762858391006,28.139006238454016;120.54795751383156,28.139006238454016;120.55247642067222,28.137867718353533;120.55538143221263,28.132459582652743;120.55376753691239,28.124489200957314;120.55441309503249,28.11566415847298;120.55893200187315,28.10797723934722;120.56667869931427,28.105984244460032;120.57345705957525,28.107123103215212;120.57958986171613,28.10655367534892;120.58604544291708,28.10342176805678;120.58927323351755,28.097157679200365;120.5931465822381,28.0834893071102;120.58991879163763,28.06924956919361;120.58895045445749,28.051019950559496;120.5931465822381,28.041903981997688;120.59766548907875,28.034781593968088;120.61186776772081,28.026803959632165;120.62058280234207,28.02651903318312;120.63123451132361,28.024524526921667;120.64511401090562,28.02651903318312;120.66093018484791,28.0282285805638;120.66835410322899,28.028798423656696;120.68126526563086,28.024524526921667;120.6948219861528,28.021675168156403;120.71160649727524,28.021675168156403;120.72839100839767,28.01141685220725;120.73742882207898,27.999162583129444;120.73871993831916,27.989187149187458;120.76163725158248,27.977215409958912;120.77616230928459,27.97265439781902;120.78713679732618,27.97265439781902;120.79262404134698,27.970088743745798;120.80876299434931,27.968948433462458;120.81747802897057,27.967808111127816;120.83038919137243,27.957259558282324;120.84168645847407,27.952127457120493;120.84555980719463,27.996312553354493;120.83200308667267,27.988332069036062;120.82135137769113,27.986051822143104;120.81102244776964,27.987762011834025;120.79456071570725,27.99488751020321;120.78326344860561,28.019680572148907;120.76099169346239,28.0282285805638;120.73290991523832,28.03079285072198;120.72128986907664,28.03563630542758;120.69256253273248,28.034781593968088;120.67448690536987,28.040479542114323;120.66254408014814,28.03791550279268;120.6580251733075,28.03592120773863;120.64479123184557,28.038200399068412;120.62736116260305,28.03734570797788;120.61735501174161,28.03307215068109;120.60831719806029,28.033926875719207;120.60057050061918,28.039055083369046;120.59831104719885,28.05757157564859;120.60057050061918,28.072952082651348;120.60250717497945,28.087191329724334;120.59540603565843,28.097442418444963;120.58539988479698,28.108831368678235;120.57474817581544,28.113101913313074;120.56764703649439,28.11139371586372;120.56086867623343,28.113956001835078;120.55731810657291,28.12904377736259;120.557963664693,28.14042937156471;120.5434386069909,28.145837104956886;120.53085022364908,28.144983270467232;120.51567960782688,28.14014474645503;120.51471127064674,28.13957549396699'
|
||||
} ]
|
||||
const detailFences = [ {
|
||||
'diaphaneity': '0.5',
|
||||
'fillColor': '#FF8D1C',
|
||||
'id': '1967830494361186306',
|
||||
'latitude': '28.022472',
|
||||
'lineColor': '#FF8D1C',
|
||||
'lineWidth': '3',
|
||||
'longitude': '120.731175',
|
||||
'positionInfo': '120.61794118544928,28.03092241093921;120.6242327329885,28.031831163585036;120.62766448619173,28.033951556581712;120.63624386919979,28.034052526634593;120.64791183009073,28.035365128700334;120.65660560487224,28.03314379274773;120.66541377142717,28.034052526634593;120.67273484492738,28.036374811701013;120.69824421040465,28.031831163585036;120.72832924681956,28.031730191447796;120.7624179953049,28.023349173812985;120.77808966826629,28.016684284398487;120.78506756644616,28.008908058677182;120.79513404250893,27.98800035716066;120.82121536685344,27.983656702662813;120.81778361365019,27.971432922042883;120.80108241472786,27.975473994537417;120.78701222659465,27.975878093461006;120.77122616185983,27.976484239007874;120.7568127984063,27.985373982332725;120.74594557992944,27.995273061233107;120.73748058869482,28.01042295187755;120.72649897844452,28.02203642519428;120.70842507824088,28.025267777614015;120.69481245720144,28.024661906423727;120.68646185774026,28.025267777614015;120.67376437088834,28.028902933141204;120.65797830615352,28.030720464864665;120.64573838639535,28.028499033034315;120.63761657048107,28.0288019582566;120.62755009441828,28.02688341744956;120.61736922658207,28.028398057770744;120.6157677417539,28.030114624367364;120.61794118544928,28.03092241093921'
|
||||
} ]
|
||||
// 应急事件
|
||||
const accidents = [ {
|
||||
id: 1,
|
||||
accidentName: '渔船侧翻',
|
||||
type: '翻船',
|
||||
time: '2025-10-20 03:12:25',
|
||||
boatName: '浙温货565',
|
||||
boatType: '渔船',
|
||||
boatCode: '9525513',
|
||||
homePortName: '永嘉瓯北码头',
|
||||
holderName: '林业玉',
|
||||
holderPhone: '15846325412',
|
||||
source: '翻船',
|
||||
status: '未处置',
|
||||
longitude: 120.71,
|
||||
latitude: 28.03,
|
||||
coordinate: '120.71,28.03',
|
||||
fishingArea: '---'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
accidentName: '船上两名人员落水',
|
||||
type: '落水',
|
||||
time: '2025-10-20 03:12:25',
|
||||
boatName: '浙温渔B526',
|
||||
boatType: '渔船',
|
||||
boatCode: '9212332',
|
||||
homePortName: '永嘉瓯北码头',
|
||||
holderName: '杨逸司',
|
||||
holderPhone: '1336521541',
|
||||
source: '落水',
|
||||
status: '未处置',
|
||||
longitude: 120.69,
|
||||
latitude: 28.03,
|
||||
coordinate: '120.69,28.03',
|
||||
fishingArea: '---'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
accidentName: '渔船侧翻',
|
||||
type: '翻船',
|
||||
time: '2025-10-20 03:12:25',
|
||||
boatName: '浙温渔52526',
|
||||
boatType: '渔船',
|
||||
boatCode: '9524452',
|
||||
homePortName: '永嘉瓯北码头',
|
||||
holderName: '王奕盛',
|
||||
holderPhone: '13985463258',
|
||||
source: '翻船',
|
||||
status: '未处置',
|
||||
longitude: 120.74,
|
||||
latitude: 28.02,
|
||||
coordinate: '120.74,28.02',
|
||||
fishingArea: '---'
|
||||
} ]
|
||||
export{
|
||||
uavs,
|
||||
monitors,
|
||||
stations,
|
||||
environmentals,
|
||||
fences,
|
||||
detailFences,
|
||||
accidents
|
||||
}
|
||||
293
src/components/Map/lbtbox/boatTerminal.js
Normal file
293
src/components/Map/lbtbox/boatTerminal.js
Normal file
@@ -0,0 +1,293 @@
|
||||
|
||||
import * as maptalks from 'maptalks'
|
||||
import { PointLayer } from '@maptalks/vt'
|
||||
import GlobalMap from '../js/GlobalMap.js'
|
||||
import { getAssetsFile } from '@/utils/common'
|
||||
import WebSocketClient from '@/utils/WebSocketClient'
|
||||
import { findByCurrent } from '@/api/map/index.js'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const danger = getAssetsFile('map/boat/danger.png')
|
||||
const passenger = getAssetsFile('map/boat/passenger.png')
|
||||
const trawler = getAssetsFile('map/boat/trawler.png')
|
||||
const other = getAssetsFile('map/boat/other.png')
|
||||
const radar = getAssetsFile('map/boat/radar.png')
|
||||
const netsonde = getAssetsFile('map/boat/netsonde.png')
|
||||
|
||||
let storeInstance = null
|
||||
let globalMap = null
|
||||
let glgroup = null
|
||||
let dynamicBoatInfoWebSocket = null // websock
|
||||
let boatsMarkerlayer = null // 渔船图层
|
||||
let cleanupTimer = null
|
||||
|
||||
const init = (store) => {
|
||||
storeInstance = store
|
||||
globalMap = GlobalMap.instance
|
||||
glgroup = globalMap.glGroup
|
||||
glgroup.setZIndex(8)
|
||||
setStyleToBoatLayer('_trawler_dynamic_boat', 8)
|
||||
}
|
||||
|
||||
// 设置渔船样式
|
||||
const setStyleToBoatLayer = (key, zIndex) => {
|
||||
const style = {
|
||||
style: [
|
||||
{
|
||||
name: 'boatStyle',
|
||||
filter: true,
|
||||
renderPlugin: {
|
||||
dataConfig: {
|
||||
type: 'point',
|
||||
only2D: true
|
||||
},
|
||||
sceneConfig: {
|
||||
fading: true,
|
||||
depthFunc: 'always'
|
||||
},
|
||||
type: 'icon'
|
||||
},
|
||||
symbol: [
|
||||
{
|
||||
markerType: 'triangle',
|
||||
markerWidth: {
|
||||
type: 'interval',
|
||||
stops: [
|
||||
[ 5, 5 ],
|
||||
[ 10, 10 ],
|
||||
[ 11, 0 ]
|
||||
]
|
||||
},
|
||||
markerHeight: {
|
||||
type: 'interval',
|
||||
stops: [
|
||||
[ 5, 10 ],
|
||||
[ 10, 20 ],
|
||||
[ 11, 0 ]
|
||||
]
|
||||
},
|
||||
markerFill: {
|
||||
type: 'categorical',
|
||||
property: 'deviceType',
|
||||
default: '#f4ea2a',
|
||||
// stops: [
|
||||
// [ 'AIS', { type: 'categorical', property: 'boatType', stops: [
|
||||
// [ '危险品货物运输船', '#d81e06' ],
|
||||
// [ '液货船', '#d81e06' ],
|
||||
// [ '客船', '#73f34f' ],
|
||||
// [ '公务船', '#73f34f' ],
|
||||
// [ '拖轮', '#73f34f' ],
|
||||
// [ '渔船', '#fff' ],
|
||||
// [ '其他', '#f4ea2a' ],
|
||||
// [ '网位移', '#e6e6e6' ],
|
||||
// [ '无线索船', '#e6e6e6' ]
|
||||
// ] } ],
|
||||
// [ 'RADAR', radar ]
|
||||
// ],
|
||||
stops: [
|
||||
[ 'AIS', '#f4ea2a' ]
|
||||
]
|
||||
},
|
||||
markerLineColor: [ 0.2, 0.29, 0.39, 1 ],
|
||||
markerLineWidth: 1,
|
||||
markerRotation: {
|
||||
property: 'cog',
|
||||
type: 'identity'
|
||||
}
|
||||
},
|
||||
{
|
||||
markerWidth: {
|
||||
type: 'interval',
|
||||
stops: [
|
||||
[ 10, 0 ],
|
||||
[ 11, 25 ],
|
||||
[ 14, 25 ]
|
||||
]
|
||||
},
|
||||
markerHeight: {
|
||||
type: 'interval',
|
||||
stops: [
|
||||
[ 10, 0 ],
|
||||
[ 11, 30 ],
|
||||
[ 14, 30 ]
|
||||
]
|
||||
},
|
||||
markerRotation: {
|
||||
property: 'cog',
|
||||
type: 'identity'
|
||||
},
|
||||
markerFile: {
|
||||
type: 'categorical',
|
||||
property: 'deviceType',
|
||||
default: other,
|
||||
// stops: [
|
||||
// [ 'AIS', { type: 'categorical', property: 'boatType', stops: [
|
||||
// [ '危险品货物运输船', danger ],
|
||||
// [ '液货船', danger ],
|
||||
// [ '客船', passenger ],
|
||||
// [ '公务船', passenger ],
|
||||
// [ '拖轮', passenger ],
|
||||
// [ '渔船', trawler ],
|
||||
// [ '其他', other ],
|
||||
// [ '网位移', netsonde ],
|
||||
// [ '无线索船', netsonde ]
|
||||
// ] } ],
|
||||
// [ 'RADAR', radar ],
|
||||
// ],
|
||||
stops: [
|
||||
[ 'AIS', other ]
|
||||
]
|
||||
},
|
||||
textName: {
|
||||
property: 'boatName',
|
||||
type: 'identity'
|
||||
},
|
||||
textFill: '#fff',
|
||||
textHaloFill: '#3300cc',
|
||||
textHaloRadius: 2,
|
||||
textHaloBlur: 20,
|
||||
textDy: 10,
|
||||
textSize: {
|
||||
type: 'interval',
|
||||
stops: [
|
||||
[ 14, 0 ],
|
||||
[ 15, 15 ]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
const name = `vt_${key}`
|
||||
if (boatsMarkerlayer) {
|
||||
boatsMarkerlayer.remove()
|
||||
}
|
||||
// 初始化动态渔船图层
|
||||
boatsMarkerlayer = new PointLayer(name, {
|
||||
cursor: 'pointer'
|
||||
})
|
||||
boatsMarkerlayer.setStyle(style)
|
||||
boatsMarkerlayer.addTo(glgroup)
|
||||
glgroup.setZIndex(zIndex)
|
||||
}
|
||||
|
||||
// 单独的清理函数
|
||||
const cleanupExpiredBoats = () => {
|
||||
boatsMarkerlayer.getGeometries().forEach(item => {
|
||||
const properties = item.getProperties()
|
||||
if (dayjs().diff(properties.gpsTime, 'hours') > 24) {
|
||||
item.remove()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getShip = () => {
|
||||
findByCurrent().then(res => {
|
||||
console.log('初始获取')
|
||||
addBoats(res.result)
|
||||
})
|
||||
// ----------船只数据------------------
|
||||
dynamicBoatInfoWebSocket = new WebSocketClient(`${import.meta.env.VITE_WS_BASE_URL}`)
|
||||
dynamicBoatInfoWebSocket.onopen(event => {
|
||||
})
|
||||
dynamicBoatInfoWebSocket.onmessage(event => {
|
||||
console.log('接收数据:', JSON.parse(event.data))
|
||||
if(Array.isArray(JSON.parse(event.data))) {
|
||||
addBoats(JSON.parse(event.data))
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
dynamicBoatInfoWebSocket.onclose(event => {
|
||||
// 可以尝试重新连接
|
||||
dynamicBoatInfoWebSocket.reconnect()
|
||||
})
|
||||
|
||||
dynamicBoatInfoWebSocket.onerror(event => {
|
||||
dynamicBoatInfoWebSocket.close()
|
||||
dynamicBoatInfoWebSocket = null
|
||||
})
|
||||
// 启动定期清理任务(每5分钟检查一次)
|
||||
if (cleanupTimer) {
|
||||
clearInterval(cleanupTimer)
|
||||
}
|
||||
cleanupTimer = setInterval(() => {
|
||||
cleanupExpiredBoats()
|
||||
}, 5 * 60 * 1000) // 5分钟
|
||||
// 初始化连接
|
||||
dynamicBoatInfoWebSocket.connect()
|
||||
}
|
||||
|
||||
const addBoats = (boatList) => {
|
||||
|
||||
boatList.forEach((boat) => {
|
||||
// let types = [ '危险品货物运输船', '液货船', '客船', '公务船', '拖轮', '渔船', '其他', '网位移', '无线索船' ]
|
||||
let { longitude, latitude, cog, boatName, sog, terminalCode, deviceType, gpsTime } = boat
|
||||
let isContain = boatsMarkerlayer.getGeometryById(terminalCode)
|
||||
if (isContain !== null) {
|
||||
isContain.setCoordinates([ longitude, latitude ])
|
||||
isContain.setProperties({
|
||||
cog: 360 - Number(cog),
|
||||
angle: Number(cog),
|
||||
sog: Number(sog),
|
||||
boatName,
|
||||
latitude,
|
||||
longitude,
|
||||
deviceType,
|
||||
gpsTime
|
||||
// boatType: types[Math.floor(Math.random() * types.length)]
|
||||
})
|
||||
} else {
|
||||
let mm = new maptalks.Marker([ longitude, latitude ], {
|
||||
id: terminalCode,
|
||||
properties: {
|
||||
cog: 360 - Number(cog),
|
||||
angle: Number(cog),
|
||||
sog: Number(sog),
|
||||
boatName,
|
||||
latitude,
|
||||
longitude,
|
||||
deviceType,
|
||||
gpsTime
|
||||
// boatType: types[Math.floor(Math.random() * types.length)]
|
||||
},
|
||||
cursor: 'pointer'
|
||||
})
|
||||
mm.on('click', (e) => {
|
||||
storeInstance.updateWindowInfo({
|
||||
visible: true,
|
||||
type: '_trawler_dynamic',
|
||||
data: { ...boat,
|
||||
...e.target.getProperties()
|
||||
// boatType: types[Math.floor(Math.random() * types.length)]
|
||||
}
|
||||
})
|
||||
globalMap.map.animateTo(
|
||||
{
|
||||
zoom: 20,
|
||||
center: [ boat.longitude, boat.latitude ]
|
||||
},
|
||||
{
|
||||
duration: 1000 * 0.5,
|
||||
easing: 'out'
|
||||
}
|
||||
)
|
||||
})
|
||||
mm.addTo(boatsMarkerlayer)
|
||||
}
|
||||
})
|
||||
}
|
||||
const destroyWebsocket = () => {
|
||||
dynamicBoatInfoWebSocket.close()
|
||||
if (cleanupTimer) {
|
||||
clearInterval(cleanupTimer)
|
||||
cleanupTimer = null
|
||||
}
|
||||
}
|
||||
export {
|
||||
destroyWebsocket,
|
||||
init,
|
||||
getShip,
|
||||
boatsMarkerlayer
|
||||
}
|
||||
172
src/components/Map/lbtbox/boatWebSocketManager.js
Normal file
172
src/components/Map/lbtbox/boatWebSocketManager.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import { GetBoatPointReq, BoatPointList } from '../protoFile/boats.js'
|
||||
|
||||
export default class boatWebSocketManager {
|
||||
constructor(url, onMessage) {
|
||||
this.ws = null
|
||||
this.url = url
|
||||
this.queue = [] // 发送队列
|
||||
this.isSending = false // 是否正在发送
|
||||
this.heartbeatInterval = null // 心跳定时器
|
||||
this.reconnectInterval = null // 重连定时器
|
||||
|
||||
this.reconnectIntervalTime = 1000 * 10 // 重连时间间隔
|
||||
this.heartbeatIntervalTime = 1000 * 30 // 心跳时间间隔
|
||||
this.reconnectCount = 0 // 重连次数
|
||||
this.maxReconnectCount = 5 // 最大重连次数
|
||||
this.onMessage = onMessage // 接收消息的回调函数
|
||||
this.isStop = false // 是否停止
|
||||
this.init()
|
||||
}
|
||||
|
||||
// 初始化 WebSocket
|
||||
init() {
|
||||
if (this.reconnectCount === 0) {
|
||||
console.log('websocket', `初始化连接中...${this.url}`)
|
||||
}
|
||||
this.ws = new WebSocket(this.url)
|
||||
this.ws.binaryType = 'arraybuffer'
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.isStop = false
|
||||
// 重置重连次数
|
||||
this.reconnectCount = 0
|
||||
// 在连接成功时停止当前的心跳检测并重新启动
|
||||
this.startHeartbeat()
|
||||
console.log('WebSocket connected')
|
||||
this.processQueue() // 开始处理队列
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
this.handleMessage(event) // 处理消息
|
||||
}
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
if (this.reconnectCount === 0) {
|
||||
console.log('websocket', `连接断开[onclose]...${this.url}`, event)
|
||||
}
|
||||
this.stopHeartbeat()
|
||||
if (!this.isStop) {
|
||||
this.reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
if (this.reconnectCount === 0) {
|
||||
console.log('websocket', `连接异常[onclose]...${this.url},${error}`)
|
||||
}
|
||||
this.stopHeartbeat()
|
||||
}
|
||||
}
|
||||
|
||||
// 启动心跳
|
||||
startHeartbeat() {
|
||||
this.stopHeartbeat()
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
// this.send({ type: 'heartbeat' }, true) // 心跳包优先级高
|
||||
if (this.ws) {
|
||||
// 新增修改,将send数据整个转为二进制
|
||||
const message = GetBoatPointReq.create({
|
||||
type: 'heartbeat'
|
||||
})
|
||||
GetBoatPointReq.verify(message)
|
||||
const binaryData = GetBoatPointReq.encode(message).finish()
|
||||
this.send(binaryData, true) // 心跳包优先级高
|
||||
|
||||
// this.send({ type: 'heartbeat' }, true) // 心跳包优先级高
|
||||
console.log('WebSocket', '发送心跳数据...')
|
||||
} else {
|
||||
console.log('WebSocket', '未连接')
|
||||
}
|
||||
}, this.heartbeatIntervalTime) // 30秒一次心跳
|
||||
}
|
||||
|
||||
// 停止心跳
|
||||
stopHeartbeat() {
|
||||
// clearInterval(this.heartbeatInterval)
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval)
|
||||
this.heartbeatInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息(支持优先级)
|
||||
send(message, isHighPriority = false) {
|
||||
if (isHighPriority) {
|
||||
this.queue.unshift(message) // 高优先级插入队首
|
||||
} else {
|
||||
this.queue.push(message) // 普通消息插入队尾
|
||||
}
|
||||
this.processQueue()
|
||||
}
|
||||
|
||||
// 处理接收到的消息
|
||||
handleMessage(data) {
|
||||
if (this.onMessage) {
|
||||
this.onMessage(data) // 调用外部传入的回调函数
|
||||
}
|
||||
}
|
||||
|
||||
// 处理发送队列
|
||||
processQueue() {
|
||||
if (this.isSending || this.queue.length === 0) {
|
||||
return
|
||||
}
|
||||
this.isSending = true
|
||||
const message = this.queue.shift()
|
||||
|
||||
// 检查 WebSocket 状态
|
||||
if (this.ws.readyState !== WebSocket.OPEN) {
|
||||
console.error('WebSocket not open, message discarded:', message)
|
||||
this.isSending = false
|
||||
return
|
||||
}
|
||||
|
||||
// 检查缓冲区是否空闲
|
||||
if (this.ws.bufferedAmount > 0) {
|
||||
console.log('Buffer busy, retrying...')
|
||||
setTimeout(() => {
|
||||
this.isSending = false
|
||||
this.processQueue()
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
this.ws.send(message)
|
||||
this.isSending = false
|
||||
this.processQueue() // 继续处理下一条
|
||||
}
|
||||
|
||||
// 重连机制
|
||||
reconnect() {
|
||||
if (this.reconnectCount < this.maxReconnectCount) {
|
||||
this.reconnectCount++
|
||||
console.log('WebSocket', `尝试重连(${this.reconnectCount}/${this.maxReconnectCount})...${this.url}`)
|
||||
if(this.reconnectInterval) {
|
||||
clearTimeout(this.reconnectInterval)
|
||||
this.reconnectInterval = null
|
||||
}
|
||||
this.reconnectInterval = setTimeout(() => {
|
||||
console.log('Reconnecting...')
|
||||
this.init()
|
||||
}, this.reconnectIntervalTime)
|
||||
} else {
|
||||
this.stopHeartbeat()
|
||||
console.log('WebSocket', `最大重连失败,终止重连: ${this.url}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭连接
|
||||
close() {
|
||||
if (this.ws) {
|
||||
this.isStop = true
|
||||
this.ws.close()
|
||||
this.ws = null
|
||||
}
|
||||
if(this.reconnectInterval) {
|
||||
clearTimeout(this.reconnectInterval)
|
||||
this.reconnectInterval = null
|
||||
}
|
||||
this.stopHeartbeat()
|
||||
}
|
||||
}
|
||||
178
src/components/Map/legend.vue
Normal file
178
src/components/Map/legend.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
|
||||
<div :class="['screen-legend-left',isFold? 'fold':'']">
|
||||
<div class="header" @click="toggleExpand">
|
||||
<span>图例 </span>
|
||||
<img src="@/assets/images/map/legend/icon-suffix.png" alt="">
|
||||
</div>
|
||||
|
||||
<ul class="list-wrapper">
|
||||
<li :class="['list-item', item.checked ? 'active' : '']" v-for="(item, index) in list" :key="index" @click="handle(item)">
|
||||
<img :src="getAssetsFile(`map/legend/icon_${item.icon}.png`)" alt="">
|
||||
<span class="label">{{ item.label }}</span>
|
||||
<img class="sector-icon" v-if="item.prop === 'monitor' || item.prop === 'UAV'" :src="getSectorIcon(item)" alt="" @click.stop="toggleSector(item)">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { getAssetsFile } from '@/utils/common'
|
||||
import useMapStore from '@/store/modules/map'
|
||||
|
||||
const mapStore = useMapStore()
|
||||
|
||||
const isFold = ref(true)
|
||||
|
||||
const list = ref([
|
||||
{
|
||||
label: '无人机',
|
||||
icon: 'UAV',
|
||||
prop: 'UAV',
|
||||
checked: true,
|
||||
sector: false
|
||||
},
|
||||
{
|
||||
label: '监控点位',
|
||||
icon: 'monitor',
|
||||
prop: 'monitor',
|
||||
checked: true,
|
||||
sector: false
|
||||
},
|
||||
{
|
||||
label: 'AIS基站',
|
||||
icon: 'ais_station',
|
||||
prop: 'ais_station',
|
||||
checked: true
|
||||
},
|
||||
{
|
||||
label: '环境监测设备',
|
||||
icon: 'environmental',
|
||||
prop: 'environmental',
|
||||
checked: true
|
||||
},
|
||||
{
|
||||
label: '电子围栏',
|
||||
icon: 'fence',
|
||||
prop: 'fence',
|
||||
checked: true
|
||||
}
|
||||
])
|
||||
|
||||
const getSectorIcon = (item) => {
|
||||
return getAssetsFile(`login/icon-${item.sector ? 'show' : 'hidden'}.png`)
|
||||
}
|
||||
const toggleSector = (item) => {
|
||||
if (!item.checked) {
|
||||
return
|
||||
}
|
||||
item.sector = !item.sector
|
||||
mapStore.updateSector(item.prop, item.sector)
|
||||
}
|
||||
const toggleExpand = () => {
|
||||
isFold.value = !isFold.value
|
||||
}
|
||||
|
||||
const handle = (item) => {
|
||||
item.checked = !item.checked
|
||||
mapStore.updateLegend(item.prop, item.checked)
|
||||
if(!item.checked && (item.prop === 'monitor' || item.prop === 'UAV')) {
|
||||
item.sector = false
|
||||
mapStore.updateSector(item.prop, item.sector)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.screen-legend-left {
|
||||
bottom: 54px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
left: 476px;
|
||||
position: fixed;
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s;
|
||||
overflow: hidden;
|
||||
background: linear-gradient( 90deg, #0C1929 0%, rgba(12,25,41,0.2) 100%);
|
||||
color: #FFFFFF;
|
||||
border-radius: 6px;
|
||||
.header{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-family: 'SHSCNM';
|
||||
font-size: 14px;
|
||||
width: 94px;
|
||||
height: 28px;
|
||||
background: #0AA9FF;
|
||||
border-radius: 3px;
|
||||
img{
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
.list-wrapper{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
height: auto;
|
||||
.list-item{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
&.active{
|
||||
opacity: 1;
|
||||
}
|
||||
img{
|
||||
max-width: 18px;
|
||||
max-height: 25px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.label{
|
||||
font-size: 12px;
|
||||
font-family: 'SHSCNN';
|
||||
}
|
||||
.sector-icon{
|
||||
margin:0 0 0 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.fold{
|
||||
.list-wrapper{
|
||||
transition: all 0.3s;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.screen-legend-left::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 6px;
|
||||
padding: 1px; /* 控制"边框"宽度 */
|
||||
background: linear-gradient(270deg, rgba(42, 159, 255, 0.2), rgba(42, 159, 255, 0.8));
|
||||
mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
.bottom{
|
||||
bottom: 391px;
|
||||
}
|
||||
.left{
|
||||
left: 85px;
|
||||
}
|
||||
</style>
|
||||
138
src/components/Map/map-config.js
Normal file
138
src/components/Map/map-config.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import { getAssetsFile } from '@/utils/common'
|
||||
// 底图配置
|
||||
const baseLayerConfig = {
|
||||
baseConfig: {
|
||||
satellite: [ 'tdt_image', 'tdt_cia' ],
|
||||
sea: [ 'sea' ],
|
||||
light: [ 'light' ],
|
||||
dark: [ 'tdt_ter', 'tdt_tbo', 'tdt_cta' ]
|
||||
},
|
||||
layerConfig: {
|
||||
light: {
|
||||
max: 18,
|
||||
maxAvailableZoom: 18,
|
||||
urlTemplate:
|
||||
'https://inner.qdlimap.cn:9443/gisAssistant/wmts/grid_tile/tianditu/vec_w/{z}/{y}/{x}'
|
||||
},
|
||||
sea: {
|
||||
max: 18,
|
||||
maxAvailableZoom: 15,
|
||||
urlTemplate:
|
||||
// 'http://125.124.131.105:8157/wmts/seaMap/{z}/{y}/{x}.png'
|
||||
'https://www.yhships.cn:8157/wmts/seaMap/{z}/{y}/{x}.png'
|
||||
},
|
||||
satellite: {
|
||||
max: 18,
|
||||
maxAvailableZoom: 18,
|
||||
urlTemplate:
|
||||
'https://server.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'
|
||||
},
|
||||
tdt_cia: {
|
||||
max: 18,
|
||||
maxAvailableZoom: 18,
|
||||
subdomains: [ '0', '1', '2', '3', '4', '5', '6', '7' ],
|
||||
urlTemplate:
|
||||
'https://inner.qdlimap.cn:9443/gisAssistant/wmts/grid_tile/tianditu/cia_w/{z}/{y}/{x}'
|
||||
},
|
||||
tdt_image: {
|
||||
max: 18,
|
||||
maxAvailableZoom: 18,
|
||||
subdomains: [ '0', '1', '2', '3', '4', '5', '6', '7' ],
|
||||
urlTemplate:
|
||||
'https://inner.qdlimap.cn:9443/gisAssistant/wmts/grid_tile/tianditu/img_w/{z}/{y}/{x}'
|
||||
},
|
||||
tdt_tbo: {
|
||||
max: 18,
|
||||
maxAvailableZoom: 18,
|
||||
subdomains: [ '0', '1', '2', '3', '4', '5', '6', '7' ],
|
||||
urlTemplate:
|
||||
'https://inner.qdlimap.cn:9443/gisAssistant/wmts/grid_tile/tianditu/tbo_w/{z}/{y}/{x}'
|
||||
},
|
||||
tdt_ter: {
|
||||
max: 18,
|
||||
maxAvailableZoom: 18,
|
||||
subdomains: [ '0', '1', '2', '3', '4', '5', '6', '7' ],
|
||||
urlTemplate:
|
||||
'https://inner.qdlimap.cn:9443/gisAssistant/wmts/grid_tile/tianditu/ter_w/{z}/{y}/{x}'
|
||||
},
|
||||
tdt_cta: {
|
||||
max: 18,
|
||||
maxAvailableZoom: 18,
|
||||
subdomains: [ '0', '1', '2', '3', '4', '5', '6', '7' ],
|
||||
urlTemplate:
|
||||
'https://inner.qdlimap.cn:9443/gisAssistant/wmts/grid_tile/tianditu/cta_w/{z}/{y}/{x}'
|
||||
}
|
||||
}
|
||||
}
|
||||
// 定位样式
|
||||
const getLocationSymbol = () => [
|
||||
{
|
||||
markerFile: getAssetsFile('map/icon-focus.png'),
|
||||
markerWidth: {
|
||||
stops: [
|
||||
[ 10, 40 ],
|
||||
[ 11, 40 ]
|
||||
]
|
||||
},
|
||||
markerHeight: {
|
||||
stops: [
|
||||
[ 10, 40 ],
|
||||
[ 11, 40 ]
|
||||
]
|
||||
},
|
||||
markerDx: 0,
|
||||
markerDy: {
|
||||
stops: [
|
||||
[ 10, 20 ],
|
||||
[ 11, 20 ]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
const baseConfig = {
|
||||
center: [ 120.67, 28.01 ],
|
||||
zoom: 12,
|
||||
maxZoom: 20,
|
||||
minZoom: 5,
|
||||
attribution: '',
|
||||
enableInfoWindow: true,
|
||||
baseLayerName: 'satellite',
|
||||
pitch: 0,
|
||||
bearing: 0,
|
||||
dragRotate: false,
|
||||
dragPitch: false,
|
||||
scaleControl: true, // add scale control
|
||||
hideScale: true // 隐藏比例尺
|
||||
}
|
||||
// 设备-渲染位置
|
||||
const getDevicePointSymbol = (type, info) => {
|
||||
const symbol = [
|
||||
{
|
||||
markerFile: getAssetsFile(`map/devices/icon${type}.png`),
|
||||
markerDx: 0,
|
||||
markerDy: 0
|
||||
},
|
||||
{
|
||||
textName: info.name,
|
||||
textFill: '#fff',
|
||||
textSize: {
|
||||
stops: [
|
||||
[ 14, 0 ],
|
||||
[ 14, 13 ]
|
||||
]
|
||||
},
|
||||
textHaloFill: '#0876f9',
|
||||
textHaloRadius: 1,
|
||||
textDy: 10,
|
||||
textVerticalAlignment: 'middle'
|
||||
}
|
||||
]
|
||||
return symbol
|
||||
}
|
||||
|
||||
export {
|
||||
baseLayerConfig,
|
||||
baseConfig,
|
||||
getLocationSymbol,
|
||||
getDevicePointSymbol
|
||||
}
|
||||
1106
src/components/Map/protoFile/boats.js
Normal file
1106
src/components/Map/protoFile/boats.js
Normal file
File diff suppressed because it is too large
Load Diff
28
src/components/Map/protoFile/message.proto
Normal file
28
src/components/Map/protoFile/message.proto
Normal file
@@ -0,0 +1,28 @@
|
||||
syntax = "proto3";
|
||||
//option java_package = "com.yuhuan.point.proto";
|
||||
option java_outer_classname = "BoatPoint";
|
||||
|
||||
message GetBoatPointReq {
|
||||
string type = 1; // 信息类型 data为 数据类型 heartbeat 为心跳
|
||||
string area = 2;// 坐标点 区域 4个坐标度 左经度,下纬度,右经度,上纬度 以英文逗号隔开
|
||||
}
|
||||
message BoatPointList {
|
||||
repeated GetBoatPointResp boatPointList = 1; // 在线点位列表
|
||||
repeated GetBoatPointResp offlineBoatList = 2; // 离线设备列表
|
||||
string respType = 3; // 响应类型 data 为数据类型 heartbeat 为心跳
|
||||
}
|
||||
|
||||
message GetBoatPointResp {
|
||||
string deviceType = 1; // 设备类型
|
||||
string terminalCode = 2; // 设备编号
|
||||
string boatId = 3; //船Id
|
||||
string boatName = 4; //船名
|
||||
double longitude = 5; // 经度
|
||||
double latitude = 6; // 纬度
|
||||
double sog = 7; // 航速
|
||||
double cog = 8; // 航向
|
||||
string gpsTime = 9; // gps时间 yyyy-MM-dd HH:mm:ss
|
||||
string boatType = 10; // 船舶类型
|
||||
string name = 11;
|
||||
double lenth = 12; // 船长
|
||||
}
|
||||
115
src/components/Map/window/flow.vue
Normal file
115
src/components/Map/window/flow.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<DialogComponent title="气象设备" width="395" :draggable="true" :modal="false" @close="$emit('close')">
|
||||
<div class="content-wrapper">
|
||||
|
||||
<ul class="list-wrapper">
|
||||
<li class="list-item" v-for="(item, index) in items" :key="index">
|
||||
<span class="label">{{ item.label }}:</span>
|
||||
<span class="value">{{ model[item.prop] }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</DialogComponent>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive } from 'vue'
|
||||
import DialogComponent from '@/components/Dialog/index.vue'
|
||||
|
||||
const items = [
|
||||
{
|
||||
label: '深度观测站编号',
|
||||
prop: 'prop1'
|
||||
},
|
||||
{
|
||||
label: '雷达水位',
|
||||
prop: 'prop2'
|
||||
},
|
||||
{
|
||||
label: '压力式水位',
|
||||
prop: 'prop3'
|
||||
},
|
||||
{
|
||||
label: '温度',
|
||||
prop: 'prop4'
|
||||
},
|
||||
{
|
||||
label: '倾角',
|
||||
prop: 'prop5'
|
||||
},
|
||||
{
|
||||
label: '当前时间',
|
||||
prop: 'prop6'
|
||||
}
|
||||
]
|
||||
|
||||
const model = reactive({
|
||||
prop1: 'XD111',
|
||||
prop2: '30m',
|
||||
prop3: '30m',
|
||||
prop4: '30℃',
|
||||
prop5: '30°',
|
||||
prop6: '2025-09-17 10:43:05'
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content-wrapper {
|
||||
width: 100%;
|
||||
// height: 300px;
|
||||
overflow: auto;
|
||||
|
||||
.list-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.list-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 18px;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
width: 120px;
|
||||
font-size: 14px;
|
||||
color: #00C0FFFF;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #FFFFFFFF;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
background-color: #00C0FF33;
|
||||
color: #00C0FFFF;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-image: url('@/assets/images/common/icon-triangle.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
56
src/components/Map/window/index.vue
Normal file
56
src/components/Map/window/index.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<DialogComponent @mousedown="drag" :style="{ resize: 'both', overflow: 'auto' }" v-if="visible && option[type]" :title="option[type].title" :width="type==='_UAV' ? 1800:395" :draggable="true" :modal="false" @close="close">
|
||||
<component :is="option[type].component" ref="component"/>
|
||||
</DialogComponent>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import DialogComponent from '@/components/Dialog/screen.vue'
|
||||
import MonitorComponent from './monitor.vue'
|
||||
import UAVComponent from './uav.vue'
|
||||
import MeteorologyComponent from './meteorology.vue'
|
||||
import useMapStore from '@/store/modules/map'
|
||||
import { dragEvent } from '@/utils/common'
|
||||
|
||||
const mapStore = useMapStore()
|
||||
|
||||
const visible = computed(() => mapStore.windowInfo.visible)
|
||||
const type = computed(() => mapStore.windowInfo.type)
|
||||
const data = computed(() => mapStore.windowInfo.data)
|
||||
|
||||
const option = computed(() => ({
|
||||
_monitor: {
|
||||
title: '监控设备',
|
||||
component: MonitorComponent
|
||||
},
|
||||
_UAV: {
|
||||
title: '无人机设备',
|
||||
component: UAVComponent
|
||||
},
|
||||
_environmental: {
|
||||
title: data.value?.videoType || '环境监测设备',
|
||||
component: MeteorologyComponent
|
||||
}
|
||||
}))
|
||||
const component = ref(null)
|
||||
const close = () => {
|
||||
mapStore.updateWindowInfo(false)
|
||||
}
|
||||
/**
|
||||
* 拖拽事件
|
||||
* @param e
|
||||
*
|
||||
*/
|
||||
const drag = (e) => {
|
||||
dragEvent(e, 'el-dialog-title', () => {
|
||||
if(component.value.HikCCTV) {
|
||||
component.value.HikCCTV.initResize(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
218
src/components/Map/window/meteorology.vue
Normal file
218
src/components/Map/window/meteorology.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<div class="content-wrapper">
|
||||
|
||||
<ul class="list-wrapper">
|
||||
<li class="list-item" v-for="(item, index) in items" :key="index">
|
||||
<span class="label">{{ item.label }}:</span>
|
||||
<span class="value">{{ model[item.prop] }}{{ item.unit }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
// import { getWaterwayVelocity, getWaterwayWeather } from '@/api/identification.js'
|
||||
import { dayjs } from 'element-plus'
|
||||
import useMapStore from '@/store/modules/map'
|
||||
|
||||
const mapStore = useMapStore()
|
||||
const data = computed(() => mapStore.windowInfo.data)
|
||||
|
||||
const velocityItems = [
|
||||
{
|
||||
label: '深度观测站编号',
|
||||
prop: 'deepSiteCode'
|
||||
},
|
||||
{
|
||||
label: '雷达流速',
|
||||
prop: 'radarVelocity',
|
||||
unit: 'km/s'
|
||||
},
|
||||
{
|
||||
label: '雷达水位',
|
||||
prop: 'radarLevel',
|
||||
unit: 'm'
|
||||
},
|
||||
{
|
||||
label: '压力式水位',
|
||||
prop: 'pressureLevel',
|
||||
unit: 'm'
|
||||
},
|
||||
{
|
||||
label: '温度',
|
||||
prop: 'temperature',
|
||||
unit: '℃'
|
||||
},
|
||||
{
|
||||
label: '倾角',
|
||||
prop: 'angle',
|
||||
unit: '°'
|
||||
},
|
||||
{
|
||||
label: '当前时间',
|
||||
prop: 'createTime'
|
||||
}
|
||||
]
|
||||
const velocityModel = {
|
||||
deepSiteCode: 0,
|
||||
radarVelocity: 0,
|
||||
radarLevel: 0,
|
||||
pressureLevel: 0,
|
||||
temperature: 0,
|
||||
angle: 0,
|
||||
createTime: null
|
||||
}
|
||||
const weatherItems = [
|
||||
{
|
||||
label: '深度观测站编号',
|
||||
prop: 'deepSiteCode'
|
||||
},
|
||||
{
|
||||
label: '温度',
|
||||
prop: 'temperature',
|
||||
unit: '℃'
|
||||
},
|
||||
{
|
||||
label: '湿度',
|
||||
prop: 'humidity',
|
||||
unit: '%'
|
||||
},
|
||||
{
|
||||
label: '风速',
|
||||
prop: 'windSpeed',
|
||||
unit: 'm/s'
|
||||
},
|
||||
{
|
||||
label: '风向',
|
||||
prop: 'windDirect',
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
label: '大气压',
|
||||
prop: 'pressure',
|
||||
unit: 'hPa'
|
||||
},
|
||||
{
|
||||
label: '降水量',
|
||||
prop: 'rainFall',
|
||||
unit: 'mm'
|
||||
},
|
||||
{
|
||||
label: '光照度',
|
||||
prop: 'illuminance',
|
||||
unit: 'Lux'
|
||||
},
|
||||
{
|
||||
label: '当前时间',
|
||||
prop: 'createTime'
|
||||
}
|
||||
]
|
||||
const weatherModel = {
|
||||
deepSiteCode: 0,
|
||||
temperature: 0,
|
||||
humidity: 0,
|
||||
windSpeed: 0,
|
||||
windDirect: 0,
|
||||
pressure: 0,
|
||||
rainFall: 0,
|
||||
illuminance: 0,
|
||||
createTime: null
|
||||
}
|
||||
const items = ref([])
|
||||
|
||||
const model = reactive({})
|
||||
const init = () => {
|
||||
if(data.value.videoType === '航道流速设备') {
|
||||
items.value = velocityItems
|
||||
const params = {
|
||||
waterwayVelocity: data.value.id
|
||||
}
|
||||
// getWaterwayVelocity(params).then(res => {
|
||||
// Object.keys(velocityModel).forEach(key => {
|
||||
// if(key === 'createTime') {
|
||||
// model[key] = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
// }else{
|
||||
// model[key] = res.result[key]
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
}else if(data.value.videoType === '气象观测站设备') {
|
||||
items.value = weatherItems
|
||||
const params = {
|
||||
waterwayVelocity: data.value.id
|
||||
}
|
||||
// getWaterwayWeather(params).then(res => {
|
||||
// Object.keys(weatherModel).forEach(key => {
|
||||
// if(key === 'createTime') {
|
||||
// model[key] = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
// }else{
|
||||
// model[key] = res.result[key]
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
}
|
||||
}
|
||||
watch(() => data.value, () => {
|
||||
init()
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content-wrapper {
|
||||
width: 100%;
|
||||
// height: 300px;
|
||||
overflow: auto;
|
||||
|
||||
.list-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.list-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 18px;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
width: 120px;
|
||||
font-size: 14px;
|
||||
color: #00C0FFFF;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #FFFFFFFF;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
background-color: #00C0FF33;
|
||||
color: #00C0FFFF;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-image: url('@/assets/images/common/icon-triangle.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
175
src/components/Map/window/monitor.vue
Normal file
175
src/components/Map/window/monitor.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div class="content-wrapper">
|
||||
<div class="monitor-cctv video-windowcctv">
|
||||
<HikPlayerComponent ref="HikCCTV" v-if="visible" :cameraIndexCode="data.videoCode" id="cctv"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import useMapStore from '@/store/modules/map'
|
||||
import HikPlayerComponent from '@/components/Player/HikPlayer.vue'
|
||||
|
||||
const mapStore = useMapStore()
|
||||
const visible = ref(false)
|
||||
const data = computed(() => mapStore.windowInfo.data)
|
||||
const HikCCTV = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
visible.value = true
|
||||
})
|
||||
})
|
||||
defineExpose({
|
||||
HikCCTV
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
.list-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.list-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 18px;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
width: 100px;
|
||||
font-size: 14px;
|
||||
color: #00C0FFFF;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #FFFFFFFF;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.subtitle {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
background-color: #00C0FF33;
|
||||
color: #00C0FFFF;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-image: url('@/assets/images/common/icon-triangle.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
.title{
|
||||
position: relative;
|
||||
.button{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -32px;
|
||||
cursor: pointer;
|
||||
color: #00c0ff;
|
||||
padding: 6px 10px;
|
||||
background: rgba(22, 119, 255, 0.21);
|
||||
border-radius: 2px 2px 2px 2px;
|
||||
border: 1px solid #236ACE;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
.list-wrapper {
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
margin-top: 10px;
|
||||
overflow: auto;
|
||||
.header{
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: linear-gradient( 0deg, rgba(10,167,255,0) 0%, rgba(22,115,255,0.5) 100%);
|
||||
border-radius: 3px 3px 0px 0px;
|
||||
display: flex;
|
||||
|
||||
.header-label{
|
||||
font-size: 12px;
|
||||
color: #F4F9FECC;
|
||||
flex-shrink:0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
overflow: auto;
|
||||
.header{
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: linear-gradient( 0deg, rgba(10,167,255,0) 0%, rgba(22,115,255,0.5) 100%);
|
||||
border-radius: 3px 3px 0px 0px;
|
||||
display: flex;
|
||||
|
||||
.header-label{
|
||||
font-size: 12px;
|
||||
color: #F4F9FECC;
|
||||
flex-shrink:0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.item{
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
|
||||
.row{
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
flex-shrink:0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:nth-of-type(odd){
|
||||
background: #2a64bb2b;
|
||||
}
|
||||
|
||||
&:hover{
|
||||
background-image: url('@/assets/images/common/row-background-active.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
.row.index::before{
|
||||
background: linear-gradient( 89deg, #00F0FF 0%, rgba(4,114,217,0) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.monitor-cctv{
|
||||
height: 289px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>=>
|
||||
322
src/components/Map/window/trawler.vue
Normal file
322
src/components/Map/window/trawler.vue
Normal file
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<!-- 渔船信息 弹窗 -->
|
||||
<DialogComponent v-if="visible && info" :title="data.boatName" width="450" :draggable="true" :modal="false" :btn="true" @handle="handleTrack" @close="closeInfo">
|
||||
<div class="content-wrapper">
|
||||
|
||||
<ul class="list-wrapper">
|
||||
<li class="list-item" v-for="item in columns" :key="item.prop">
|
||||
<span class="label">{{item.label}}:</span>
|
||||
<span class="value">{{data[item.prop]}}{{ item.unit }}</span>
|
||||
</li>
|
||||
<div class="subtitle">监控列表:</div>
|
||||
|
||||
<div class="row" v-for="item in monitors" :key="item.videoName">
|
||||
<div class="left">
|
||||
<img src="@/assets/images/map/devices/icon-monitor.png" alt="">
|
||||
<span class="name" :title="item.videoName">{{ item.videoName }}</span>
|
||||
<span class="distance">({{item.distance}}km)</span>
|
||||
</div>
|
||||
<div class="button" @click="handleFollow(item)">
|
||||
<img src="@/assets/images/map/devices/icon-view.png" alt="">
|
||||
光电联动
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</DialogComponent>
|
||||
<!-- 光电随动视频 -->
|
||||
<DialogComponent class="monitor-follow-dialog" v-if="visible && monitor.visible" :title="monitor.data.videoName" width="450" :draggable="true" :modal="false" @close="closeMonitor" @mousedown="drag">
|
||||
<div class="monitor-follow video-windowfollow">
|
||||
<HikPlayerComponent ref="HikFollow" v-if="monitor.data&&monitor.data.url" :cameraIndexCode="monitor.data.url" id="follow"/>
|
||||
</div>
|
||||
</DialogComponent>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue'
|
||||
import DialogComponent from '@/components/Dialog/screen.vue'
|
||||
import HikPlayerComponent from '@/components/Player/HikPlayer.vue'
|
||||
import { findAISPointPositionByMmsi, getDevicesForServo, getDevicesIsCover, sitMoveByGps, servoByRadar, exitServoByRadar } from '@/api/map'
|
||||
import { dragEvent } from '@/utils/common'
|
||||
import GlobalMap from '@/components/map/js/GlobalMap'
|
||||
import ShipPathInMap from '@/components/map/js/ShipPathInMap'
|
||||
import useMapStore from '@/store/modules/map'
|
||||
|
||||
const mapStore = useMapStore()
|
||||
const visible = computed(() => mapStore.windowInfo.visible)
|
||||
const type = computed(() => mapStore.windowInfo.type)
|
||||
const data = computed(() => mapStore.windowInfo.data)
|
||||
const info = ref(true)
|
||||
const monitor = reactive({
|
||||
visible: true,
|
||||
data: {}
|
||||
})
|
||||
const columns = [
|
||||
{
|
||||
label: 'AIS编号',
|
||||
prop: 'terminalCode'
|
||||
},
|
||||
{
|
||||
label: '船牌号',
|
||||
prop: 'boatName'
|
||||
},
|
||||
{
|
||||
label: '航速',
|
||||
prop: 'sog',
|
||||
unit: '节'
|
||||
},
|
||||
{
|
||||
label: '航向',
|
||||
prop: 'angle',
|
||||
unit: '度'
|
||||
}
|
||||
]
|
||||
const monitors = ref([])
|
||||
const HikFollow = ref(null)
|
||||
// 光电随动控制标识
|
||||
const currentServo = ref('')
|
||||
let globalMap = null
|
||||
|
||||
/**
|
||||
* 拖拽事件
|
||||
* @param e
|
||||
*
|
||||
*/
|
||||
const drag = (e) => {
|
||||
dragEvent(e, 'el-dialog', () => {
|
||||
HikFollow.value.initResize(false)
|
||||
})
|
||||
}
|
||||
const getMonitorList = () => {
|
||||
const params = {
|
||||
gpsX: data.value.longitude,
|
||||
gpsY: data.value.latitude
|
||||
}
|
||||
getDevicesForServo(params).then(res => {
|
||||
monitors.value = res.data
|
||||
if(res.data.length > 0) {
|
||||
// 光电联动 默认预览第一个监控视频
|
||||
handleFollow(res.data[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
const closeInfo = () => {
|
||||
info.value = false
|
||||
if(!monitor.visible) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
const closeMonitor = () => {
|
||||
monitor.visible = false
|
||||
monitor.data = {}
|
||||
if(!info.value) {
|
||||
close()
|
||||
}
|
||||
// 退出随动
|
||||
if(currentServo.value) {
|
||||
handleExitServo()
|
||||
}
|
||||
}
|
||||
const close = () => {
|
||||
mapStore.updateWindowInfo(false)
|
||||
}
|
||||
// 判断监控设备和将要联动的定位目标是否超出可视范围、是否处于视角遮挡区
|
||||
const handleFollow = (item) => {
|
||||
const params = {
|
||||
deviceCode: item.videoCode,
|
||||
gpsX: data.value.longitude,
|
||||
gpsY: data.value.latitude
|
||||
}
|
||||
getDevicesIsCover(params).then(res => {
|
||||
if (res.msg.includes('超出')) {
|
||||
ElMessage.warning(res.msg)
|
||||
}
|
||||
handleSitMoveByGps(item)
|
||||
})
|
||||
}
|
||||
// 联动控制
|
||||
const handleSitMoveByGps = (item) => {
|
||||
closeMonitor()
|
||||
const params = {
|
||||
deviceCode: item.videoCode,
|
||||
terminalCode: data.value.terminalCode,
|
||||
gpsX: data.value.longitude,
|
||||
gpsY: data.value.latitude
|
||||
}
|
||||
sitMoveByGps(params).then(res => {
|
||||
if (res.code === 200) {
|
||||
monitor.visible = true
|
||||
nextTick(() => {
|
||||
monitor.data.videoName = item.videoName
|
||||
monitor.data.url = item.videoCode
|
||||
})
|
||||
}
|
||||
// 光电随动控制
|
||||
handleServoByRadar(item)
|
||||
})
|
||||
}
|
||||
// 光电随动
|
||||
const handleServoByRadar = (item) => {
|
||||
const params = {
|
||||
deviceCode: item.videoCode,
|
||||
terminalCode: data.value.terminalCode,
|
||||
gpsX: data.value.longitude,
|
||||
gpsY: data.value.latitude
|
||||
}
|
||||
|
||||
servoByRadar(params).then(res => {
|
||||
if (res.code === 200) {
|
||||
currentServo.value = data.value.terminalCode
|
||||
}
|
||||
})
|
||||
}
|
||||
// 退出随动
|
||||
const handleExitServo = () => {
|
||||
const params = {
|
||||
terminalCode: currentServo.value
|
||||
}
|
||||
|
||||
exitServoByRadar(params).then(res => {
|
||||
if (res.code === 200) {
|
||||
currentServo.value = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 格式化轨迹数据
|
||||
* @param data 原始数据
|
||||
* @return {Array}
|
||||
*/
|
||||
const formatTrackData = (data) => {
|
||||
const list = []
|
||||
|
||||
const trackObj = data.map((_item) => {
|
||||
const info = { ..._item }
|
||||
info.longitude = Number(_item.longitude.toFixed(6))
|
||||
info.latitude = Number(_item.latitude.toFixed(6))
|
||||
info.time = new Date(_item.gpsTime).getTime()
|
||||
return info
|
||||
}).filter((point) => Math.abs(point.latitude) < 90 && Math.abs(point.longitude) < 180)
|
||||
list.push(trackObj)
|
||||
return list
|
||||
}
|
||||
// 查询轨迹
|
||||
const handleTrack = () => {
|
||||
globalMap = GlobalMap.instance
|
||||
findAISPointPositionByMmsi({ mmsi: data.value.terminalCode }).then(res => {
|
||||
if (res.success) {
|
||||
const arr = res.result[data.value.terminalCode]
|
||||
new ShipPathInMap(globalMap.map, globalMap.map.getLayer('track'), formatTrackData(arr))
|
||||
}
|
||||
}).finally(() => {
|
||||
mapStore.updateWindowInfo(false)
|
||||
})
|
||||
}
|
||||
watch(() => data.value, () => {
|
||||
if(type.value === '_trawler_dynamic') {
|
||||
info.value = true
|
||||
getMonitorList()
|
||||
}else{
|
||||
info.value = false
|
||||
monitor.visible = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.content-wrapper {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
|
||||
.list-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.list-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 18px;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
width: 100px;
|
||||
font-size: 14px;
|
||||
color: #00C0FFFF;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #FFFFFFFF;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
background-color: #00C0FF33;
|
||||
color: #00C0FFFF;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-image: url('@/assets/images/common/icon-triangle.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
.row{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.name{
|
||||
color: rgba(0, 192, 255, 1);
|
||||
margin: 0 5px;
|
||||
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
.distance{
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
.left{
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.button{
|
||||
color: rgba(0, 246, 255, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
img{
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.monitor-follow-dialog{
|
||||
margin-left: calc(50% + 225px) !important;
|
||||
.el-dialog__body{
|
||||
padding: 0 !important;
|
||||
}
|
||||
.monitor-follow{
|
||||
height: 289px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
219
src/components/Map/window/uav.vue
Normal file
219
src/components/Map/window/uav.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<div class="content-wrapper">
|
||||
<div class="left-wrapper">
|
||||
<div class="uav-button" @click="handleAlgorithm">{{algorithmStatus ? '关闭' : '开启'}}算法</div>
|
||||
<div class="Form">
|
||||
<el-checkbox-group v-model="algorithmValue">
|
||||
<el-checkbox
|
||||
v-for="item in algorithms"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value" />
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<div class="flv-container"><FlvPlayerComponent v-if="uavDialog.url" id="uav" :url="uavDialog.url"/></div>
|
||||
</div>
|
||||
<div class="resize-handle" @mousedown="startResize"></div>
|
||||
<div class="right-wrapper">
|
||||
<CockpitCom v-if="uavDialog.visible"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import useMapStore from '@/store/modules/map'
|
||||
import FlvPlayerComponent from '@/components/FlvPlayer/index.vue'
|
||||
import CockpitCom from '@/views/business/drone/cockpit.vue'
|
||||
|
||||
const mapStore = useMapStore()
|
||||
const data = computed(() => mapStore.windowInfo.data)
|
||||
// 算法开启关闭状态
|
||||
const algorithmStatus = ref(false)
|
||||
const algorithms = [
|
||||
{ value: 120000, label: 'AIS挂牌/船牌识别' },
|
||||
{ value: 140000, label: '船型识别' },
|
||||
{ value: 139000, label: '未穿救生衣' },
|
||||
{ value: 140009, label: '未悬挂国旗' },
|
||||
{ value: 140008, label: '未封舱' }
|
||||
]
|
||||
const algorithmValue = ref([])
|
||||
const uavDialog = reactive({
|
||||
visible: false,
|
||||
url: ''
|
||||
})
|
||||
// 添加分割线相关的响应式数据
|
||||
const isResizing = ref(false)
|
||||
const leftWidth = ref('462px')
|
||||
const rightWidth = ref('1391px')
|
||||
const containerWidth = ref(0)
|
||||
let containerBoundsLeft = 0 // 容器左边界
|
||||
|
||||
// 开启/关闭算法
|
||||
const handleAlgorithm = () => {
|
||||
if(algorithmStatus.value) {
|
||||
closeAlgorithm()
|
||||
} else {
|
||||
let enable_orc = false
|
||||
const valueArray = [ ...algorithmValue.value ]
|
||||
const index = valueArray.indexOf(120000)
|
||||
if (index > -1) {
|
||||
valueArray.splice(index, 1)
|
||||
enable_orc = true
|
||||
}
|
||||
const params = {
|
||||
class_codes: valueArray.join(','),
|
||||
enable_orc,
|
||||
status: 'start',
|
||||
sn: data.value.droneSn
|
||||
}
|
||||
// doStartOrStopUavAlgorithm(params).then(res => {
|
||||
// if(res.success) {
|
||||
// algorithmStatus.value = true
|
||||
// ElMessage.success('算法已开启')
|
||||
// }else {
|
||||
// ElMessage.error(res.result)
|
||||
// }
|
||||
// })
|
||||
}
|
||||
}
|
||||
// 添加分割线控制相关方法
|
||||
const startResize = (e) => {
|
||||
isResizing.value = true
|
||||
const containerRect = e.target.parentElement.getBoundingClientRect()
|
||||
containerWidth.value = containerRect.width
|
||||
// 保存容器的左边界位置
|
||||
containerBoundsLeft = containerRect.left
|
||||
document.addEventListener('mousemove', resize)
|
||||
document.addEventListener('mouseup', stopResize)
|
||||
}
|
||||
const resize = (e) => {
|
||||
if (isResizing.value) {
|
||||
// 使用预先保存的容器左边界
|
||||
const offsetX = e.clientX - containerBoundsLeft
|
||||
|
||||
// 计算左侧宽度百分比
|
||||
let leftPercent = offsetX / containerWidth.value * 100
|
||||
// 限制左右两侧的最小宽度
|
||||
const minRightWidth = 1391
|
||||
const minLeftWidth = 200
|
||||
|
||||
// 计算右侧最小百分比
|
||||
const minRightPercent = minRightWidth / containerWidth.value * 100
|
||||
const minLeftPercent = minLeftWidth / containerWidth.value * 100
|
||||
|
||||
// 限制拖动范围
|
||||
if (leftPercent < minLeftPercent) {
|
||||
leftPercent = minLeftPercent
|
||||
} else if (leftPercent > 100 - minRightPercent) {
|
||||
leftPercent = 100 - minRightPercent
|
||||
}
|
||||
|
||||
leftWidth.value = `${leftPercent}%`
|
||||
rightWidth.value = `${100 - leftPercent}%`
|
||||
}
|
||||
}
|
||||
|
||||
const stopResize = () => {
|
||||
isResizing.value = false
|
||||
document.removeEventListener('mousemove', resize)
|
||||
document.removeEventListener('mouseup', stopResize)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 无人机弹窗
|
||||
.content-wrapper{
|
||||
display: flex;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
video{
|
||||
width: 100%;
|
||||
height: 830px;
|
||||
object-fit: fill;
|
||||
}
|
||||
// //播放按钮
|
||||
video::-webkit-media-controls-play-button{display: none;}
|
||||
//进度条
|
||||
video::-webkit-media-controls-timeline{display: none;}
|
||||
//观看的当前时间
|
||||
video::-webkit-media-controls-current-time-display{display: none;}
|
||||
//剩余时间
|
||||
video::-webkit-media-controls-time-remaining-display{display: none;}
|
||||
//音量按钮
|
||||
video::-webkit-media-controls-mute-button{display: none;}
|
||||
video::-webkit-media-controls-toggle-closed-captions-buttonf{display: none;}
|
||||
//1音量的控制条
|
||||
video::-webkit-media-controls-volume-slider{display: none;}
|
||||
video::-webkit-media-controls-enclosuret{display: none;}
|
||||
.uav-button{
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
color: #00c0ff;
|
||||
padding: 6px 10px;
|
||||
background: rgba(22, 119, 255, 0.21);
|
||||
border-radius: 2px 2px 2px 2px;
|
||||
border: 1px solid #236ACE;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
.left-wrapper{
|
||||
// width: 60%;
|
||||
width: v-bind(leftWidth);
|
||||
height: 830px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
.el-checkbox-group{
|
||||
gap: 4px !important;
|
||||
.el-checkbox{
|
||||
width: 150px !important;
|
||||
padding-left: 5px !important;
|
||||
}
|
||||
}
|
||||
.flv-container{
|
||||
flex: 1; // 占据剩余空间
|
||||
overflow: hidden; // 隐藏溢出内容
|
||||
}
|
||||
}
|
||||
.resize-handle {
|
||||
width: 8px;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
cursor: col-resize;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
z-index: 10;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 2px;
|
||||
// height: 40px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
.right-wrapper{
|
||||
// width: 40%;
|
||||
width: v-bind(rightWidth);
|
||||
height: 830px;
|
||||
// position: absolute;
|
||||
// right: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.temporary{
|
||||
position: absolute;
|
||||
right: 0'';
|
||||
right: 0;
|
||||
top: 48px;
|
||||
z-index: 999;
|
||||
width: 1069px;
|
||||
height: 429px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
336
src/components/Player/HikPlayer-back.vue
Normal file
336
src/components/Player/HikPlayer-back.vue
Normal file
@@ -0,0 +1,336 @@
|
||||
<template>
|
||||
|
||||
<!-- 绑定父组件class:videoWindow -->
|
||||
<div
|
||||
:id="playWndId"
|
||||
ref="video-preview">
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint-disable */
|
||||
|
||||
export default {
|
||||
name: 'video-preview',
|
||||
data() {
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
secret: '',
|
||||
oWebControl: null,
|
||||
initCount: 0,
|
||||
appkey: import.meta.env.VITE_APP_HAIKANG_APPKEY,
|
||||
ip: import.meta.env.VITE_APP_HAIKANG_IP,
|
||||
port: parseInt(import.meta.env.VITE_APP_HAIKANG_PORT),
|
||||
playMode: 1,
|
||||
pubKey: '',
|
||||
isInit: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
layout: {
|
||||
type: String,
|
||||
default: () => '1x1'
|
||||
},
|
||||
cameraIndexCode: {},
|
||||
timeOut: {
|
||||
type: Number,
|
||||
default: () => 500
|
||||
},
|
||||
videoWindowClassName: {
|
||||
type: String,
|
||||
default: "videoWindow",
|
||||
required: false
|
||||
},
|
||||
playWndId: {
|
||||
type: String,
|
||||
default: "playWnd",
|
||||
required: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
var that = this
|
||||
this.$nextTick(function() {
|
||||
that.windowChange()
|
||||
setTimeout(() => {
|
||||
that.initPlugin()
|
||||
}, that.timeOut)
|
||||
window.addEventListener('resize', function() {
|
||||
that.onResize()
|
||||
})
|
||||
})
|
||||
},
|
||||
destroyed() {
|
||||
this.uninit()
|
||||
},
|
||||
methods: {
|
||||
onResize(change=true){
|
||||
let that = this
|
||||
if (that.oWebControl) {
|
||||
if(change){
|
||||
that.windowChange()
|
||||
}
|
||||
that.oWebControl.JS_Resize(that.width, that.height)
|
||||
}
|
||||
},
|
||||
wait (fn, timeout, tick) {
|
||||
timeout = timeout || 5000;
|
||||
tick = tick || 250;
|
||||
var timeoutTimer = null;
|
||||
var execTimer = null;
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
|
||||
timeoutTimer = setTimeout(function() {
|
||||
clearTimeout(execTimer);
|
||||
reject(new Error('polling fail because timeout'));
|
||||
}, timeout);
|
||||
|
||||
tickHandler(fn);
|
||||
|
||||
function tickHandler(fn) {
|
||||
var ret = fn();
|
||||
if (!ret) {
|
||||
execTimer = setTimeout(function() {
|
||||
tickHandler(fn);
|
||||
}, tick)
|
||||
} else {
|
||||
clearTimeout(timeoutTimer);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
// 初始化plugin
|
||||
initPlugin() {
|
||||
let that = this
|
||||
this.oWebControl = new WebControl({
|
||||
szPluginContainer: this.playWndId,
|
||||
iServicePortStart: 15900,
|
||||
iServicePortEnd: 15900,
|
||||
szClassId: '23BF3B0A-2C56-4D97-9C03-0CB103AA8F11', // 用于IE10使用ActiveX的clsid
|
||||
cbConnectSuccess: function() {
|
||||
that.setCallbacks();
|
||||
//实例创建成功后需要启动服务
|
||||
that.oWebControl
|
||||
.JS_StartService("window", {
|
||||
dllPath: "./VideoPluginConnect.dll"
|
||||
})
|
||||
.then(
|
||||
function() {
|
||||
that.oWebControl
|
||||
.JS_CreateWnd(that.playWndId, that.width, that.height)
|
||||
.then(function() {
|
||||
//JS_CreateWnd创建视频播放窗口,宽高可设定
|
||||
that.init(); //创建播放实例成功后初始化
|
||||
});
|
||||
},
|
||||
function() {}
|
||||
);
|
||||
},
|
||||
cbConnectError: function() {
|
||||
that.oWebControl = null;
|
||||
document.getElementById(that.playWndId).innerHTML = "插件未启动,正在尝试启动,请稍候...";
|
||||
// $("#" + that.playWndId).html("插件未启动,正在尝试启动,请稍候...");
|
||||
WebControl.JS_WakeUp("VideoWebPlugin://"); //程序未启动时执行error函数,采用wakeup来启动程序
|
||||
that.initCount++;
|
||||
if (that.initCount < 3) {
|
||||
setTimeout(function() {
|
||||
that.initPlugin();
|
||||
}, 2000);
|
||||
} else {
|
||||
document.getElementById(that.playWndId).innerHTML = "插件启动失败,请检查插件是否安装!";
|
||||
// $("#" + that.playWndId).html("插件启动失败,请检查插件是否安装!");
|
||||
}
|
||||
},
|
||||
cbConnectClose: function(bNormalClose) {
|
||||
that.oWebControl = null
|
||||
}
|
||||
})
|
||||
},
|
||||
init() {
|
||||
|
||||
let that = this;
|
||||
that.getPubKey(function() {
|
||||
////////////////////////////////// 请自行修改以下变量值 ////////////////////////////////////
|
||||
var snapDir = "d:\\SnapDir"; //抓图存储路径
|
||||
var videoDir = "d:\\VideoDir"; //紧急录像或录像剪辑存储路径
|
||||
var layout = "1x1"; //playMode指定模式的布局
|
||||
var enableHTTPS = 1; //是否启用HTTPS协议与综合安防管理平台交互,是为1,否为0
|
||||
var encryptedFields = "secret"; //加密字段,默认加密领域为secret
|
||||
var showToolbar = 1; //是否显示工具栏,0-不显示,非0-显示
|
||||
var showSmart = 1; //是否显示智能信息(如配置移动侦测后画面上的线框),0-不显示,非0-显示
|
||||
var buttonIDs = "0,16,256,257,258,259,260,512,515,516,517,768,769"; //自定义工具条按钮
|
||||
var levelType = "24m"; //如未指定或指定值为字符串但是参数无效,使用默认值 24h。该字段取值范围”24h,12h,6h,1h,36m,24m,12m”,超出该取值范围无效。
|
||||
////////////////////////////////// 请自行修改以上变量值 ////////////////////////////////////
|
||||
that.secret = that.setEncrypt(import.meta.env.VITE_APP_HAIKANG_SECRET);
|
||||
that.oWebControl
|
||||
.JS_RequestInterface({
|
||||
funcName: "init",
|
||||
argument: JSON.stringify({
|
||||
appkey: that.appkey, //API网关提供的appkey
|
||||
secret: that.secret, //API网关提供的secret
|
||||
ip: that.ip, //API网关IP地址
|
||||
playMode: that.playMode, //播放模式(决定显示预览还是回放界面)
|
||||
port: that.port, //端口
|
||||
snapDir: snapDir, //抓图存储路径
|
||||
videoDir: videoDir, //紧急录像或录像剪辑存储路径
|
||||
layout: layout, //布局
|
||||
enableHTTPS: enableHTTPS, //是否启用HTTPS协议
|
||||
encryptedFields: encryptedFields, //加密字段
|
||||
showToolbar: showToolbar, //是否显示工具栏
|
||||
showSmart: showSmart, //是否显示智能信息
|
||||
buttonIDs: buttonIDs, //自定义工具条按钮
|
||||
})
|
||||
})
|
||||
.then(function(oData) {
|
||||
that.oWebControl.JS_Resize(that.width, that.height); // 初始化后resize一次,规避firefox下首次显示窗口后插件窗口未与DIV窗口重合问题
|
||||
});
|
||||
});
|
||||
},
|
||||
// 获取公钥
|
||||
getPubKey(callback) {
|
||||
var that = this
|
||||
this.oWebControl.JS_RequestInterface({
|
||||
funcName: 'getRSAPubKey',
|
||||
argument: JSON.stringify({
|
||||
keyLength: 1024
|
||||
})
|
||||
}).then(function(oData) {
|
||||
if (oData.responseMsg.data) {
|
||||
that.pubKey = oData.responseMsg.data
|
||||
that.isInit = true
|
||||
callback()
|
||||
}
|
||||
})
|
||||
},
|
||||
// 设置窗口控制回调
|
||||
setCallbacks() {
|
||||
let that = this;
|
||||
that.oWebControl.JS_SetWindowControlCallback({
|
||||
cbIntegrationCallBack: function (oData) {
|
||||
if(oData.responseMsg.msg.result === 816){
|
||||
that.$emit('close')
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
// RSA加密
|
||||
setEncrypt(value) {
|
||||
let that = this;
|
||||
var encrypt = new JSEncrypt();
|
||||
encrypt.setPublicKey(that.pubKey);
|
||||
return encrypt.encrypt(value);
|
||||
},
|
||||
ready() {
|
||||
let that = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.wait(
|
||||
function() {
|
||||
return that.isInit;
|
||||
},
|
||||
6000 + that.timeOut || 500,
|
||||
100
|
||||
)
|
||||
.then(function() {
|
||||
resolve();
|
||||
})
|
||||
.catch(function(err) {
|
||||
// that.$Message.info({
|
||||
// content: "视频控件加载超时,请检查:" + err,
|
||||
// duration: 3
|
||||
// });
|
||||
// reject("视频控件加载超时,请检查:" + err);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
playBack(codes, startTime, endTime) {
|
||||
let that = this;
|
||||
const startTimeStamp = this.timrStr2Stamp(startTime) + "";
|
||||
const endTimeStamp = this.timrStr2Stamp(endTime) + "";
|
||||
var recordLocation = 0; //录像存储位置:0-中心存储,1-设备存储
|
||||
var transMode = 1; //传输协议:0-UDP,1-TCP
|
||||
var gpuMode = 0; //是否启用GPU硬解,0-不启用,1-启用
|
||||
var wndId = -1; //播放窗口序号(在2x2以上布局下可指定播放窗口)
|
||||
that.oWebControl
|
||||
.JS_RequestInterface({
|
||||
funcName: "startPlayback",
|
||||
argument: JSON.stringify({
|
||||
cameraIndexCode: codes, //监控点编号
|
||||
startTimeStamp: startTimeStamp, //录像查询开始时间戳,单位:秒
|
||||
endTimeStamp: endTimeStamp, //录像结束开始时间戳,单位:秒
|
||||
recordLocation: recordLocation, //录像存储类型:0-中心存储,1-设备存储
|
||||
transMode: transMode, //传输协议:0-UDP,1-TCP
|
||||
gpuMode: gpuMode, //是否启用GPU硬解,0-不启用,1-启用
|
||||
wndId: wndId, //可指定播放窗口
|
||||
|
||||
})
|
||||
})
|
||||
.then(res => {
|
||||
console.log("回放的参数", res);
|
||||
});
|
||||
},
|
||||
timrStr2Stamp(str) {
|
||||
let date = new Date(str);
|
||||
let time = date.getTime();
|
||||
return time / 1000;
|
||||
},
|
||||
stopPlayBack() {
|
||||
this.oWebControl.JS_RequestInterface({
|
||||
funcName: "stopAllPlayback"
|
||||
});
|
||||
},
|
||||
uninit() {
|
||||
console.log(999);
|
||||
|
||||
let that = this;
|
||||
if (that.oWebControl != null) {
|
||||
that.oWebControl.JS_RequestInterface({
|
||||
funcName: "stopAllPreview"
|
||||
});
|
||||
that.oWebControl.JS_HideWnd(); // 先让窗口隐藏,规避可能的插件窗口滞后于浏览器消失问题
|
||||
that.oWebControl.JS_Disconnect().then(
|
||||
function() {
|
||||
// 断开与插件服务连接成功
|
||||
},
|
||||
function() {
|
||||
// 断开与插件服务连接失败
|
||||
console.log("oWebControl close error");
|
||||
}
|
||||
);
|
||||
that.oWebControl = null;
|
||||
}
|
||||
},
|
||||
windowChange() {
|
||||
|
||||
//列表选项在下方
|
||||
this.width = document.getElementsByClassName(
|
||||
this.videoWindowClassName
|
||||
)[0].scrollWidth;
|
||||
var btnHeight = 0;
|
||||
if (document.getElementsByClassName("playBtn").length > 0) {
|
||||
btnHeight = document.getElementsByClassName("playBtn")[0].clientHeight;
|
||||
this.height =
|
||||
document.getElementsByClassName(this.videoWindowClassName)[0]
|
||||
.scrollHeight -
|
||||
btnHeight -
|
||||
10;
|
||||
} else {
|
||||
this.height = document.getElementsByClassName(
|
||||
this.videoWindowClassName
|
||||
)[0].scrollHeight;
|
||||
}
|
||||
if (document.getElementById(this.playWndId)) {
|
||||
document.getElementById(this.playWndId).style.height = this.height + "px";
|
||||
document.getElementById(this.playWndId).style.width = this.width + "px";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
367
src/components/Player/HikPlayer.vue
Normal file
367
src/components/Player/HikPlayer.vue
Normal file
@@ -0,0 +1,367 @@
|
||||
<template>
|
||||
|
||||
<!-- 绑定父组件class:videoWindow -->
|
||||
<div :id="`playWnd${id}`" ref="video-preview">
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
onMounted, ref, reactive, nextTick, onBeforeUnmount,
|
||||
h
|
||||
} from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '1x1'
|
||||
},
|
||||
cameraIndexCode: {
|
||||
type: Array || String,
|
||||
required: false,
|
||||
default() {
|
||||
return '0db7cac6872d4d3abd89e23c17afe4e5'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let initCount = 0
|
||||
|
||||
let oWebControl = null
|
||||
let isInit = false
|
||||
let width = 0
|
||||
let height = 0
|
||||
let pubKey = ''
|
||||
|
||||
const port = import.meta.env.VITE_APP_HAIKANG_PORT - 0
|
||||
const ip = import.meta.env.VITE_APP_HAIKANG_IP
|
||||
const secret = import.meta.env.VITE_APP_HAIKANG_SECRET
|
||||
const appkey = import.meta.env.VITE_APP_HAIKANG_APPKEY
|
||||
const videoDir = 'D:\\VideoDir' // 紧急录像或录像剪辑存储路径
|
||||
const snapDir = 'D:\\SnapDir' // 抓图存储路径,选填
|
||||
const playMode = 0 // 初始播放模式:0-预览,1-回放
|
||||
const szShowToolbar = 1 // 显示工具栏
|
||||
const szShowSmart = 1 // 显示智能信息
|
||||
const showSmart = 1
|
||||
const showToolbar = 1
|
||||
const enableHttps = 1 // 是否启用https
|
||||
|
||||
let streamMode = 0 // 主子码流标识 1:子码流 0:主码流
|
||||
const transMode = 1 // 传输协议 1:TCP 0:UDP
|
||||
const gpuMode = 0 // 是否启用 GPU 硬解,0-不启用,1-启用
|
||||
const specialCodes = []
|
||||
const wndId = -1 // 播放窗口序号(在 2x2 以上布局下可指定播放窗口)
|
||||
|
||||
const timeOut = ''
|
||||
let onLoading = false
|
||||
let btIds = '0,16,256,257,258,259,260,512,513,514,515,516,517,768,769'
|
||||
|
||||
const emit = defineEmits([ 'close' ])
|
||||
const setLayout = (layout = '1x1') => new Promise((resolve, reject) => {
|
||||
if (!isInit) {
|
||||
reject(new Error('未完成视频插件初始化'))
|
||||
}
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: 'setLayout',
|
||||
argument: JSON.stringify({
|
||||
layout
|
||||
})
|
||||
}).then((oData) => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取公钥
|
||||
*/
|
||||
const getPubKey = (callback) => {
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: 'getRSAPubKey',
|
||||
argument: JSON.stringify({
|
||||
keyLength: 1024
|
||||
})
|
||||
}).then((oData) => {
|
||||
if (oData.responseMsg.data) {
|
||||
pubKey = oData.responseMsg.data
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 多路视频播放
|
||||
*/
|
||||
const multiVideos = (videos) => {
|
||||
const last = videos[videos.length - 1]
|
||||
return new Promise((resole, reject) => {
|
||||
const intervalVideo = (clips) => {
|
||||
if (clips instanceof Array && clips.length > 0) {
|
||||
const clipLength = clips.length
|
||||
const clip = clips[0]
|
||||
|
||||
// 子码流特殊处理
|
||||
if (specialCodes.indexOf(clip) > -1) {
|
||||
streamMode = 1
|
||||
}
|
||||
console.log(clip)
|
||||
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: 'startPreview',
|
||||
argument: JSON.stringify({
|
||||
cameraIndexCode: clip,
|
||||
streamMode,
|
||||
transMode,
|
||||
gpuMode,
|
||||
wndId: -1
|
||||
})
|
||||
}).then((oData) => {
|
||||
clips.shift()
|
||||
if (oData.responseMsg.code === 0) {
|
||||
if (clips.length) {
|
||||
setTimeout(() => {
|
||||
intervalVideo(clips)
|
||||
}, 1000)
|
||||
}
|
||||
} else {
|
||||
console.log(oData)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
resole()
|
||||
}
|
||||
}
|
||||
|
||||
intervalVideo(videos)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览视频
|
||||
*/
|
||||
const handlePreview = (codes, startIdx = 1) => { // startIdx 如果是多个 从第几个窗口开始加载
|
||||
if (!codes || codes === '' || codes.length === 0) {
|
||||
return
|
||||
}
|
||||
if (Array.isArray(codes) && codes.length > 0) {
|
||||
onLoading = true
|
||||
multiVideos(codes).then(() => {
|
||||
onLoading = false
|
||||
})
|
||||
} else if (typeof codes === 'string') {
|
||||
// 子码流特殊处理
|
||||
if (specialCodes.indexOf(codes) > -1) {
|
||||
streamMode = 1
|
||||
}
|
||||
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: 'startPreview',
|
||||
argument: JSON.stringify({
|
||||
cameraIndexCode: codes,
|
||||
streamMode,
|
||||
transMode,
|
||||
gpuMode,
|
||||
wndId
|
||||
})
|
||||
}).then((res) => {
|
||||
console.log(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化interface
|
||||
*/
|
||||
const initInterface = () => {
|
||||
btIds = '0,16,256,257,258,259,260,512,513,514,515,516,517,768,769'
|
||||
getPubKey(() => {
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: 'init',
|
||||
argument: JSON.stringify({
|
||||
appkey,
|
||||
secret,
|
||||
ip,
|
||||
playMode, // 预览
|
||||
port,
|
||||
snapDir,
|
||||
videoDir,
|
||||
layout: props.layout,
|
||||
enableHTTPS: 1,
|
||||
showToolbar,
|
||||
showSmart,
|
||||
buttonIDs: btIds
|
||||
// encryptedFields: that.encryptedFields
|
||||
})
|
||||
}).then(() => {
|
||||
isInit = true
|
||||
oWebControl.JS_Resize(width, height) // 初始化后resize一次,规避firefox下首次显示窗口后插件窗口未与DIV窗口重合问题
|
||||
handlePreview(props.cameraIndexCode)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化plugin
|
||||
*/
|
||||
const initPlugin = () => {
|
||||
oWebControl = new WebControl({
|
||||
szPluginContainer: `playWnd${props.id}`,
|
||||
iServicePortStart: 15900,
|
||||
iServicePortEnd: 15900,
|
||||
szClassId: '23BF3B0A-2C56-4D97-9C03-0CB103AA8F11', // 用于IE10使用ActiveX的clsid
|
||||
cbConnectSuccess: () => {
|
||||
oWebControl.JS_SetWindowControlCallback({
|
||||
cbIntegrationCallBack: function (oData) {
|
||||
if(oData.responseMsg.msg.result === 816) {
|
||||
emit('close', oData, props.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
oWebControl.JS_StartService('window', {
|
||||
dllPath: './VideoPluginConnect.dll'
|
||||
}).then(() => {
|
||||
oWebControl.JS_CreateWnd(`playWnd${props.id}`, width, height).then(() => {
|
||||
console.log('视频plugin创建成功,进行interface初始化')
|
||||
initInterface()
|
||||
})
|
||||
})
|
||||
},
|
||||
cbConnectError: () => {
|
||||
console.log('cbConnectError')
|
||||
oWebControl = null
|
||||
WebControl.JS_WakeUp('VideoWebPlugin://')
|
||||
initCount += 1
|
||||
if (initCount < 3) {
|
||||
setTimeout(initPlugin, 2000)
|
||||
} else {
|
||||
isInit = false
|
||||
ElMessage.error('插件启动失败,请检查VideoWebPlugin.exe插件是否安装!')
|
||||
}
|
||||
},
|
||||
cbConnectClose: (bNormalClose) => {
|
||||
console.log('cbConnectClose')
|
||||
isInit = false
|
||||
oWebControl = null
|
||||
if(!bNormalClose) {
|
||||
WebControl.JS_WakeUp('VideoWebPlugin://')
|
||||
initCount += 1
|
||||
if (initCount < 3) {
|
||||
setTimeout(initPlugin, 2000)
|
||||
} else {
|
||||
isInit = false
|
||||
ElMessage.error('插件启动失败,请检查VideoWebPlugin.exe插件是否安装!')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA加密
|
||||
*/
|
||||
const setEncrypt = (value) => {
|
||||
const encrypt = new JSEncrypt()
|
||||
encrypt.setPublicKey(pubKey)
|
||||
return encrypt.encrypt(value)
|
||||
}
|
||||
|
||||
const destroy = () => {
|
||||
if (oWebControl != null) {
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: 'stopAllPreview'
|
||||
})
|
||||
oWebControl.JS_HideWnd() // 先让窗口隐藏,规避可能的插件窗口滞后于浏览器消失问题
|
||||
if(typeof props.id === 'number' || props.id.indexOf('largeModel') === -1) {
|
||||
oWebControl.JS_Disconnect()
|
||||
.then(
|
||||
() => {
|
||||
// 断开与插件服务连接成功
|
||||
},
|
||||
() => { // 断开与插件服务连接失败
|
||||
console.log('oWebControl close error')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
oWebControl = null
|
||||
}
|
||||
}
|
||||
|
||||
const windowChange = () => {
|
||||
// 列表选项在下方
|
||||
width = document.getElementsByClassName(`video-window${props.id}`)[0].scrollWidth
|
||||
let btnHeight = 0
|
||||
if (document.getElementsByClassName('playBtn').length > 0) {
|
||||
btnHeight = document.getElementsByClassName('playBtn')[0].clientHeight
|
||||
height = document.getElementsByClassName(`video-window${props.id}`)[0].scrollHeight - btnHeight - 10
|
||||
} else {
|
||||
height = document.getElementsByClassName(`video-window${props.id}`)[0].scrollHeight
|
||||
}
|
||||
if (document.getElementById(`playWnd${props.id}`)) {
|
||||
document.getElementById(`playWnd${props.id}`).style.height = `${height}px`
|
||||
document.getElementById(`playWnd${props.id}`).style.width = `${width}px`
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const initResize = (change = true) => {
|
||||
if (oWebControl) {
|
||||
if(change) {
|
||||
windowChange()
|
||||
}
|
||||
oWebControl.JS_Resize(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
const hidePlugin = () => {
|
||||
oWebControl.JS_HideWnd()
|
||||
}
|
||||
const showPlugin = () => {
|
||||
oWebControl.JS_ShowWnd()
|
||||
}
|
||||
const cuttingWindow = (left, top, width, height) => {
|
||||
oWebControl.JS_CuttingPartWindow(left, top, width, height)
|
||||
}
|
||||
const repaireWindow = (left, top, width, height) => {
|
||||
oWebControl.JS_RepairPartWindow(left, top, width, height)
|
||||
}
|
||||
|
||||
const ready = () => new Promise((resolve, reject) => {
|
||||
wait(() => isInit, 6000 + timeOut || 500, 100).then(() => {
|
||||
resolve()
|
||||
}).catch((err) => {
|
||||
ElMessage.error('视频控件加载超时,请检查!')
|
||||
reject(new Error('视频控件加载超时,请检查'))
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
windowChange()
|
||||
|
||||
nextTick(() => {
|
||||
initPlugin()
|
||||
})
|
||||
window.addEventListener('resize', initResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
destroy()
|
||||
window.removeEventListener('resize', initResize)
|
||||
})
|
||||
defineExpose({
|
||||
id: props.id,
|
||||
cuttingWindow,
|
||||
repaireWindow,
|
||||
initResize,
|
||||
destroy
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
397
src/components/Player/HikPlayer1.vue
Normal file
397
src/components/Player/HikPlayer1.vue
Normal file
@@ -0,0 +1,397 @@
|
||||
<template>
|
||||
|
||||
<!-- 绑定父组件class:videoWindow -->
|
||||
<div
|
||||
:id="`playWnd${item}`"
|
||||
ref="video-preview">
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint-disable */
|
||||
|
||||
export default {
|
||||
name: 'video-preview',
|
||||
data() {
|
||||
return {
|
||||
index: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
secret: import.meta.env.VITE_APP_HAIKANG_SECRET,
|
||||
streamMode: 0, // 主子码流标识 1:子码流 0:主码流
|
||||
transMode: 1, // 传输协议 1:TCP 0:UDP
|
||||
gpuMode: 0, // 是否启用GPU硬解
|
||||
oWebControl: {},
|
||||
initCount: 0,
|
||||
appkey: import.meta.env.VITE_APP_HAIKANG_APPKEY,
|
||||
ip: import.meta.env.VITE_APP_HAIKANG_IP,
|
||||
port: parseInt(import.meta.env.VITE_APP_HAIKANG_PORT),
|
||||
snapDir: 'D:\\SnapDir',
|
||||
videoDir: 'D:\\VideoDir',
|
||||
playMode: 0,
|
||||
szShowToolbar: 1, // 显示工具栏,
|
||||
szShowSmart: 1, // 显示智能信息=
|
||||
btIds: '0,16,256,257,258,259,260,512,513,514,515,516,517,768,769', // 工具条按钮ID集
|
||||
pubKey: '',
|
||||
enableHttps: 1, // 是否启用https
|
||||
showToolbar: 1,
|
||||
showSmart: 1,
|
||||
specialCodes: [
|
||||
],
|
||||
encryptedFields: 'secret',
|
||||
isInit: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
layout: {
|
||||
type: String,
|
||||
default: () => '1x1'
|
||||
},
|
||||
cameraIndexCode: {},
|
||||
timeOut: {
|
||||
type: Number,
|
||||
default: () => 500
|
||||
},
|
||||
item:{
|
||||
type: String,
|
||||
default: () => '0'
|
||||
},
|
||||
iframe:{
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
var that = this
|
||||
this.$nextTick(function() {
|
||||
that.windowChange()
|
||||
setTimeout(() => {
|
||||
that.initPlugin()
|
||||
}, that.timeOut)
|
||||
window.addEventListener('resize', function() {
|
||||
that.onResize()
|
||||
})
|
||||
})
|
||||
},
|
||||
destroyed() {
|
||||
this.uninit()
|
||||
},
|
||||
methods: {
|
||||
onResize(change=true){
|
||||
let that = this
|
||||
if (that.oWebControl) {
|
||||
if(change){
|
||||
that.windowChange()
|
||||
}
|
||||
that.oWebControl.JS_Resize(that.width, that.height)
|
||||
}
|
||||
},
|
||||
wait (fn, timeout, tick) {
|
||||
timeout = timeout || 5000;
|
||||
tick = tick || 250;
|
||||
var timeoutTimer = null;
|
||||
var execTimer = null;
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
|
||||
timeoutTimer = setTimeout(function() {
|
||||
clearTimeout(execTimer);
|
||||
reject(new Error('polling fail because timeout'));
|
||||
}, timeout);
|
||||
|
||||
tickHandler(fn);
|
||||
|
||||
function tickHandler(fn) {
|
||||
var ret = fn();
|
||||
if (!ret) {
|
||||
execTimer = setTimeout(function() {
|
||||
tickHandler(fn);
|
||||
}, tick)
|
||||
} else {
|
||||
clearTimeout(timeoutTimer);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
setLayout(layout = '1x1') {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isInit) {
|
||||
reject('未完成视频插件初始化')
|
||||
}
|
||||
this.oWebControl.JS_RequestInterface({
|
||||
funcName: 'setLayout',
|
||||
argument: JSON.stringify({
|
||||
'layout': layout
|
||||
})
|
||||
}).then(function(oData) {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
},
|
||||
// 初始化plugin
|
||||
initPlugin() {
|
||||
let that = this
|
||||
this.oWebControl = new WebControl({
|
||||
szPluginContainer: `playWnd${that.item}`,
|
||||
iServicePortStart: 15900,
|
||||
iServicePortEnd: 15909,
|
||||
szClassId: '23BF3B0A-2C56-4D97-9C03-0CB103AA8F11', // 用于IE10使用ActiveX的clsid
|
||||
cbConnectSuccess: function() {
|
||||
that.oWebControl.JS_SetWindowControlCallback({
|
||||
cbIntegrationCallBack: function (oData) {
|
||||
if(oData.responseMsg.msg.result === 1024){
|
||||
that.oWebControl.JS_HideWnd();
|
||||
}
|
||||
if(oData.responseMsg.msg.result === 1025){
|
||||
that.oWebControl.JS_ShowWnd();
|
||||
}
|
||||
if(oData.responseMsg.msg.result === 816){
|
||||
that.$emit('close',oData,that.item)
|
||||
}
|
||||
}
|
||||
});
|
||||
that.oWebControl.JS_StartService('window', {
|
||||
dllPath: './VideoPluginConnect.dll'
|
||||
}).then(function() {
|
||||
that.oWebControl.JS_CreateWnd(`playWnd${that.item}`, that.width, that.height).then(function() {
|
||||
console.log('视频plugin创建成功,进行interface初始化')
|
||||
// ue嵌入页面,视频窗口偏移
|
||||
if(that.iframe){
|
||||
that.oWebControl.JS_SetDocOffset ({
|
||||
left: that.iframe.left,
|
||||
top: that.iframe.top
|
||||
})
|
||||
}
|
||||
that.initInterface()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
},
|
||||
cbConnectError: function() {
|
||||
console.log('cbConnectError')
|
||||
this.oWebControl = null
|
||||
WebControl.JS_WakeUp('VideoWebPlugin://')
|
||||
that.initCount++
|
||||
if (that.initCount < 3) {
|
||||
setTimeout(that.initPlugin, 2000)
|
||||
} else {
|
||||
that.isInit = false
|
||||
that.$message.error('插件启动失败,请检查VideoWebPlugin.exe插件是否安装!')
|
||||
}
|
||||
},
|
||||
cbConnectClose: function(bNormalClose) {
|
||||
console.log('cbConnectClose')
|
||||
that.isInit = false
|
||||
that.oWebControl = null
|
||||
}
|
||||
})
|
||||
},
|
||||
setEncrypt(value) {
|
||||
// RSA加密
|
||||
var encrypt = new JSEncrypt()
|
||||
encrypt.setPublicKey(this.pubKey)
|
||||
return encrypt.encrypt(value)
|
||||
},
|
||||
//初始化interface
|
||||
initInterface() {
|
||||
let that = this
|
||||
this.btIds = '0,16,256,257,258,259,260,512,513,514,515,516,517,768,769'
|
||||
this.getPubKey(function() {
|
||||
that.oWebControl.JS_RequestInterface({
|
||||
funcName: 'init',
|
||||
argument: JSON.stringify({
|
||||
appkey: that.appkey,
|
||||
secret: that.secret,
|
||||
ip: that.ip,
|
||||
playMode: that.playMode, // 预览
|
||||
port: that.port,
|
||||
snapDir: that.snapDir,
|
||||
videoDir: that.videoDir,
|
||||
layout: that.layout,
|
||||
enableHTTPS: that.enableHttps,
|
||||
showToolbar: that.showToolbar,
|
||||
showSmart: that.showSmart,
|
||||
buttonIDs: that.btIds,
|
||||
//encryptedFields: that.encryptedFields
|
||||
})
|
||||
}).then(function(oData) {
|
||||
that.isInit = true
|
||||
that.oWebControl.JS_Resize(that.width, that.height) // 初始化后resize一次,规避firefox下首次显示窗口后插件窗口未与DIV窗口重合问题
|
||||
that.handlePreview(that.cameraIndexCode)
|
||||
})
|
||||
})
|
||||
},
|
||||
// 获取公钥
|
||||
getPubKey(callback) {
|
||||
var that = this
|
||||
this.oWebControl.JS_RequestInterface({
|
||||
funcName: 'getRSAPubKey',
|
||||
argument: JSON.stringify({
|
||||
keyLength: 1024
|
||||
})
|
||||
}).then(function(oData) {
|
||||
if (oData.responseMsg.data) {
|
||||
that.pubKey = oData.responseMsg.data
|
||||
callback()
|
||||
}
|
||||
})
|
||||
},
|
||||
multiVideos(videos) {
|
||||
let that = this
|
||||
let last = videos[videos.length - 1]
|
||||
return new Promise((resole, reject) => {
|
||||
let intervalVideo = (clips) => {
|
||||
if (clips instanceof Array && clips.length > 0) {
|
||||
let clipLength = clips.length
|
||||
let clip = clips[0]
|
||||
|
||||
// 子码流特殊处理
|
||||
let streamMode = that.streamMode
|
||||
if(that.specialCodes.indexOf(clip) > -1) streamMode = 1
|
||||
console.log(clip)
|
||||
|
||||
that.oWebControl.JS_RequestInterface({
|
||||
funcName: 'startPreview',
|
||||
argument: JSON.stringify({
|
||||
cameraIndexCode: clip,
|
||||
streamMode: streamMode,
|
||||
transMode: that.transMode,
|
||||
gpuMode: that.gpuMode,
|
||||
wndId: -1
|
||||
})
|
||||
}).then(function(oData) {
|
||||
clips.shift()
|
||||
if (oData.responseMsg.code == 0) {
|
||||
if (clips.length) {
|
||||
setTimeout(() => {
|
||||
intervalVideo(clips)
|
||||
}, 1000)
|
||||
}
|
||||
}else{
|
||||
console.log(oData)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
resole
|
||||
}
|
||||
}
|
||||
|
||||
intervalVideo(videos)
|
||||
})
|
||||
},
|
||||
handlePreview(codes, startIdx = 1) { //startIdx 如果是多个 从第几个窗口开始加载
|
||||
console.log(codes)
|
||||
let that = this
|
||||
if (!codes || codes=='' || codes.length==0)
|
||||
return
|
||||
if (Array.isArray(codes) && codes.length > 0) {
|
||||
// let confs = codes.map((code, index) => {
|
||||
// return {
|
||||
// cameraIndexCode: code,
|
||||
// streamMode: that.streamMode,
|
||||
// transMode: that.transMode,
|
||||
// gpuMode: that.gpuMode,
|
||||
// wndId: startIdx + index //设置不对会报错
|
||||
// }
|
||||
// })
|
||||
// that.oWebControl.JS_RequestInterface({
|
||||
// funcName: 'startMultiPreviewByCameraIndexCode',
|
||||
// argument: JSON.stringify({
|
||||
// list: confs
|
||||
// })
|
||||
// })
|
||||
this.onLoading = true
|
||||
this.multiVideos(codes).then(()=>{
|
||||
this.onLoading = false
|
||||
})
|
||||
} else if (typeof codes == 'string') {
|
||||
// 子码流特殊处理
|
||||
let streamMode = that.streamMode
|
||||
if(that.specialCodes.indexOf(codes)>-1) streamMode = 1
|
||||
|
||||
that.oWebControl.JS_RequestInterface({
|
||||
funcName: 'startPreview',
|
||||
argument: JSON.stringify({
|
||||
cameraIndexCode: codes,
|
||||
streamMode: streamMode,
|
||||
transMode: that.transMode,
|
||||
gpuMode: that.gpuMode,
|
||||
wndId: -1
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
uninit() {
|
||||
let oWebControl = this.oWebControl
|
||||
if (oWebControl != null) {
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: 'stopAllPreview'
|
||||
})
|
||||
oWebControl.JS_HideWnd() // 先让窗口隐藏,规避可能的插件窗口滞后于浏览器消失问题
|
||||
oWebControl.JS_Disconnect()
|
||||
.then(function() {
|
||||
// 断开与插件服务连接成功
|
||||
},
|
||||
function() { // 断开与插件服务连接失败
|
||||
console.log('oWebControl close error')
|
||||
})
|
||||
oWebControl = null
|
||||
}
|
||||
},
|
||||
windowChange() {
|
||||
//列表选项在左侧
|
||||
// this.height = document.getElementsByClassName('videoWindow')[0].scrollHeight
|
||||
// var btnWidth = 0
|
||||
// if (document.getElementsByClassName('playBtn').length > 0) {
|
||||
// btnWidth = document.getElementsByClassName('playBtn')[0].clientWidth
|
||||
// this.width = document.getElementsByClassName('videoWindow')[0].scrollWidth - btnWidth - 5
|
||||
// } else {
|
||||
// this.width = document.getElementsByClassName('videoWindow')[0].scrollWidth
|
||||
// }
|
||||
//列表选项在下方
|
||||
this.width = document.getElementsByClassName(`video-window${this.item}`)[0].scrollWidth
|
||||
var btnHeight = 0
|
||||
if (document.getElementsByClassName('playBtn').length > 0) {
|
||||
btnHeight = document.getElementsByClassName('playBtn')[0].clientHeight
|
||||
this.height = document.getElementsByClassName(`video-window${this.item}`)[0].scrollHeight - btnHeight - 10
|
||||
} else {
|
||||
this.height = document.getElementsByClassName(`video-window${this.item}`)[0].scrollHeight
|
||||
}
|
||||
|
||||
if (document.getElementById(`playWnd${this.item}`)) {
|
||||
document.getElementById(`playWnd${this.item}`).style.height = this.height + 'px'
|
||||
document.getElementById(`playWnd${this.item}`).style.width = this.width + 'px'
|
||||
}
|
||||
},
|
||||
hidePlugin() {
|
||||
this.oWebControl.JS_HideWnd()
|
||||
},
|
||||
showPlugin() {
|
||||
this.oWebControl.JS_ShowWnd()
|
||||
},
|
||||
ready() {
|
||||
let that = this
|
||||
return new Promise((resolve, reject) => {
|
||||
this.wait(function() {
|
||||
return that.isInit
|
||||
}, 6000 + that.timeOut || 500, 100).then(function() {
|
||||
resolve()
|
||||
}).catch(function(err) {
|
||||
that.$message.error({
|
||||
content: '视频控件加载超时,请检查',
|
||||
duration: 3
|
||||
})
|
||||
reject('视频控件加载超时,请检查')
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
115
src/components/SideMenu/index.vue
Normal file
115
src/components/SideMenu/index.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<aside class="aside-menu">
|
||||
<div class="title" v-if="!systemStore.isCollapse"></div>
|
||||
<div class="title" v-else></div>
|
||||
<el-menu
|
||||
class="el-menu-vertical"
|
||||
:default-active="activeIndex"
|
||||
:collapse="systemStore.isCollapse"
|
||||
active-text-color="#fff"
|
||||
background-color="#264174"
|
||||
text-color="#BFCBD9"
|
||||
>
|
||||
<template v-for="item in menu">
|
||||
<el-sub-menu v-if="!item.hidden" :index="item.value" :key="item.value">
|
||||
<template #title>
|
||||
<el-icon size="17" color="#fff"><Setting /></el-icon>
|
||||
<span>{{item.label}}</span>
|
||||
</template>
|
||||
<template v-for="child in item.children" :key="child.value">
|
||||
<template v-if="!child.hidden">
|
||||
|
||||
<el-sub-menu v-if="child.children && child.children.length" :index="child.value">
|
||||
<template #title>
|
||||
<span>{{child.label}}</span>
|
||||
</template>
|
||||
<template v-for="_child in child.children">
|
||||
|
||||
<el-menu-item v-if="!_child.hidden" :index="_child.value" :key="_child.value" @click="toggleNav(_child)">{{_child.label}}</el-menu-item>
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-menu-item v-else :index="child.value" @click="toggleNav(child)">
|
||||
<span>{{child.label}}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
</el-menu>
|
||||
</aside>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Setting } from '@element-plus/icons-vue'
|
||||
import useSystemStore from '@/store/modules/system'
|
||||
import usePermissionStore from '@/store/modules/permission'
|
||||
|
||||
const router = useRouter()
|
||||
const systemStore = useSystemStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const activeIndex = ref('system')
|
||||
const menu = computed(() => permissionStore.menus)
|
||||
|
||||
/**
|
||||
* 导航跳转
|
||||
* @param nav 菜单项
|
||||
*/
|
||||
const toggleNav = (nav) => {
|
||||
router.push(nav.path)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
aside{
|
||||
background-color: rgb(36, 49, 66);
|
||||
|
||||
.title{
|
||||
height: 60px;
|
||||
border-bottom: 2px solid rgb(65, 75, 91);
|
||||
color: #409eff;
|
||||
font-size: 24px;
|
||||
font-family: 'YouSheRegular';
|
||||
line-height: 60px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:deep(.el-menu){
|
||||
border-right: none;
|
||||
|
||||
&:not(.el-menu--collapse) {
|
||||
background: rgb(36, 49, 66);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.el-sub-menu__title:hover {
|
||||
background: rgb(38, 47, 62);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.el-sub-menu .el-menu {
|
||||
background: rgb(65, 75, 91);
|
||||
}
|
||||
|
||||
.el-menu-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
.el-menu-tree {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
background: rgb(0, 110, 255);
|
||||
color: white;
|
||||
}
|
||||
.el-menu-item:hover {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
70
src/components/SubtItle/index.vue
Normal file
70
src/components/SubtItle/index.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="subtitle">
|
||||
<span class="tittle">{{ title }}</span>
|
||||
<div class="more-button" v-if="more" @click="$emit('handle','more')"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineEmits([ 'handle' ])
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
default: '',
|
||||
required: false,
|
||||
type: String
|
||||
},
|
||||
more: {
|
||||
default: false,
|
||||
required: false,
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.subtitle {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
background-image: url('@/assets/images/common/subtitle-background.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 28px;
|
||||
background-position: center bottom;
|
||||
text-align: left;
|
||||
padding-left: 40px;
|
||||
padding-top: 8px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-left: -10px;
|
||||
margin-top: -10px;
|
||||
span {
|
||||
font-family: 'YouSheBiaoTiHei';
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
background-image: linear-gradient(180deg, #31beff 20%, #FFFFFF 41%, #FFFFFF 81%);
|
||||
-webkit-text-fill-color: transparent;
|
||||
|
||||
}
|
||||
|
||||
.more-button{
|
||||
width: 84px;
|
||||
height: 28px;
|
||||
background-image: url('@/assets/images/common/more-button.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
margin-left: 20px;
|
||||
cursor: pointer;
|
||||
&:active{
|
||||
background-image: url('@/assets/images/common/more-button-active.png');
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
51
src/components/SvgIcon/index.vue
Normal file
51
src/components/SvgIcon/index.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<svg :class="svgClass" aria-hidden="true">
|
||||
<use :xlink:href="iconName" :fill="color" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
}
|
||||
)
|
||||
const iconName = computed(() => `#icon-${props.name}`)
|
||||
const svgClass = computed(() => {
|
||||
if (props.className) {
|
||||
return `svg-icon ${props.className}`
|
||||
}
|
||||
return 'svg-icon'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scope lang="scss">
|
||||
.sub-el-icon,
|
||||
.nav-icon {
|
||||
display: inline-block;
|
||||
font-size: 15px;
|
||||
margin-right: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
position: relative;
|
||||
fill: currentColor;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
</style>
|
||||
231
src/components/Table/index.vue
Normal file
231
src/components/Table/index.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<el-table :class="theme" v-loading="loading" height="100%" :data="data" stripe table-layout="fixed" :size="size" :row-key="config.rowKey" :default-expand-all="config.expandAll" @selection-change="selectChange" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" @row-click="handleRowClick">
|
||||
<el-table-column v-if="config.type === 'index'" label="序号" type="index" align="center" width="60" />
|
||||
|
||||
<el-table-column v-if="config.type === 'selection'" :selectable="config.selectable" type="selection" :reserve-selection="config.reserve" align="left" width="60" />
|
||||
|
||||
<template v-for="(item, index) in columns" :key="index">
|
||||
<el-table-column v-if="item.link" :label="item.label" :prop="item.prop" show-overflow-tooltip :min-width="item.width" align="center">
|
||||
<template #default="scope">
|
||||
<el-button link :type="item.theme" @click="handle(item.prop, scope.row)">{{ item.text }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-else-if="item.image" :label="item.label" :prop="item.prop"
|
||||
:min-width="item.width">
|
||||
<template #default="scope">
|
||||
<el-image v-if="scope.row[item.prop].length>0" style="width:200px; height: 100px" :src="scope.row[item.prop][0]" :preview-src-list="scope.row[item.prop]" preview-teleported/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-else-if="item.progress" :label="item.label" :prop="item.prop"
|
||||
:min-width="item.width">
|
||||
<template #default="scope">
|
||||
<el-progress :text-inside="true" :stroke-width="26" :percentage="scope.row[item.prop]" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-else :label="item.label" :prop="item.prop"
|
||||
:show-overflow-tooltip="item.tooltip !== false" :min-width="item.width" :sortable="item.sortable" :align="item.align">
|
||||
<template v-if="item.filter" #header>
|
||||
<el-checkbox @change="handle('refresh',checked)"
|
||||
v-model="checked"/>
|
||||
<span>{{ item.label }}</span>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<span :class="item.class" v-if="!item.show || (item.show && item.show(scope.row))">{{ scope.row[item.prop] }}</span>
|
||||
<span :class="item.class" v-else></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<el-table-column v-if="operate.length" :align="config.align || 'center'" fixed="right" label="操作" :width="config.width">
|
||||
<template #default="scope">
|
||||
<template v-for="(item, index) in operate">
|
||||
<el-button v-if="!item.show || (item.show && item.show(scope.row))" :key="index" link :type="item.theme" v-hasPermi="item.permission"
|
||||
@click.stop="handle(item.prop, scope.row)">{{ item.label }}</el-button>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
const emit = defineEmits([ 'handle', 'selectChange', 'handleRowClick' ])
|
||||
|
||||
defineProps({
|
||||
columns: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
operate: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {
|
||||
return {
|
||||
type: 'index',
|
||||
width: 90
|
||||
}
|
||||
}
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: () => {
|
||||
return false
|
||||
}
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'dark'
|
||||
}
|
||||
})
|
||||
|
||||
const table = ref(null)
|
||||
const checked = ref(true)
|
||||
|
||||
/**
|
||||
* 点击操作按钮
|
||||
* @param type 操作类型
|
||||
* @param data 当前行数据
|
||||
*/
|
||||
const handle = (type, data) => {
|
||||
emit('handle', type, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格重新布局
|
||||
*/
|
||||
const doLayout = () => {
|
||||
table.value.doLayout()
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中内容变化
|
||||
* @param data 选中数据
|
||||
*/
|
||||
const selectChange = (data) => {
|
||||
emit('selectChange', data)
|
||||
}
|
||||
/**
|
||||
* 点击某行
|
||||
* @param data 选中数据
|
||||
*/
|
||||
const handleRowClick = (data) => {
|
||||
emit('handleRowClick', data)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
doLayout
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dark{
|
||||
background-color: #093A9B00;
|
||||
:deep(.el-table__inner-wrapper){
|
||||
&::before{
|
||||
background-color: #0acccc33;
|
||||
}
|
||||
|
||||
.el-table__header{
|
||||
.cell{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.el-checkbox{
|
||||
margin-right: 3px;
|
||||
}
|
||||
tr{
|
||||
background-color: #1677ff85;
|
||||
|
||||
th{
|
||||
background-color: #00ffff00;
|
||||
border-bottom: 1px solid #0acccc1a;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.el-table-fixed-column--right{
|
||||
background-color: #125dce;
|
||||
}
|
||||
}
|
||||
.el-table__body{
|
||||
tr{
|
||||
background-color: #093A9B00;
|
||||
.el-table-fixed-column--right{
|
||||
background: #0f419d
|
||||
}
|
||||
&.el-table__row--striped{
|
||||
td.el-table__cell{
|
||||
background-color: #047BE929;
|
||||
color: #ffffffe6;
|
||||
border-bottom: 1px solid #0acccc1a;
|
||||
}
|
||||
// fixed
|
||||
td.el-table-fixed-column--right{
|
||||
background: #0e4baa;
|
||||
}
|
||||
}
|
||||
|
||||
&.el-table__row{
|
||||
&:hover{
|
||||
.el-table__cell{
|
||||
background-color: #0acccc1a;
|
||||
}
|
||||
.el-table-fixed-column--right{
|
||||
background: #0d5ca7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hover-row{
|
||||
background-color: #0acccc1a;
|
||||
|
||||
td.el-table__cell{
|
||||
background-color: #0acccc1a;
|
||||
}
|
||||
}
|
||||
|
||||
td.el-table__cell{
|
||||
color: #ffffffe6;
|
||||
border-bottom: 1px solid #0acccc1a;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.el-loading-mask){
|
||||
background: rgba(0,0,0,0.3) !important;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.el-image-viewer__wrapper{
|
||||
.el-image-viewer__img{
|
||||
width: 80% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
129
src/components/Table/index2.vue
Normal file
129
src/components/Table/index2.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<el-table :class="theme" v-loading="loading" height="100%" :data="data" stripe table-layout="fixed" :size="size" :row-key="config.rowKey" :default-expand-all="config.expandAll" @selection-change="selectChange" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" @row-click="handleRowClick">
|
||||
<el-table-column v-if="config.type === 'index'" class-name="index-column" label="序号" type="index" align="center" width="80" />
|
||||
|
||||
<el-table-column v-if="config.type === 'selection'" :selectable="config.selectable" type="selection" :reserve-selection="config.reserve" align="left" width="60" />
|
||||
|
||||
<template v-for="(item, index) in columns" :key="index">
|
||||
<el-table-column v-if="item.sign" :label="item.label" :prop="item.prop" :show-overflow-tooltip="item.tooltip !== false" :min-width="item.width">
|
||||
<template #default="scope">
|
||||
<span v-for="item in scope.row[item.prop]" :key="item.label" :class="['sign', item.value]">{{ item.label }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-else :label="item.label" :prop="item.prop"
|
||||
:show-overflow-tooltip="item.tooltip !== false" :min-width="item.width" :sortable="item.sortable" :align="item.align">
|
||||
<template #default="scope">
|
||||
<span :class="item.class" v-if="!item.show || (item.show && item.show(scope.row))">{{ scope.row[item.prop] }}</span>
|
||||
<span :class="item.class" v-else></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<el-table-column v-if="operate.length" :align="config.align || 'center'" fixed="right" :label="config.label || '操作'" :width="config.width">
|
||||
<template #default="scope">
|
||||
<template v-for="(item, index) in operate">
|
||||
<el-button :class="item.prop === 'delete' ? 'deleteBtn' : ''" v-if="!item.show || (item.show && item.show(scope.row))" :key="index" link :type="item.theme"
|
||||
@click.stop="handle(item.prop, scope.row)">{{ item.label }}</el-button>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
const emit = defineEmits([ 'handle', 'selectChange', 'handleRowClick' ])
|
||||
|
||||
defineProps({
|
||||
columns: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
operate: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {
|
||||
return {
|
||||
type: 'index',
|
||||
width: 90
|
||||
}
|
||||
}
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: () => {
|
||||
return false
|
||||
}
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'Table'
|
||||
}
|
||||
})
|
||||
|
||||
const table = ref(null)
|
||||
|
||||
/**
|
||||
* 点击操作按钮
|
||||
* @param type 操作类型
|
||||
* @param data 当前行数据
|
||||
*/
|
||||
const handle = (type, data) => {
|
||||
emit('handle', type, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格重新布局
|
||||
*/
|
||||
const doLayout = () => {
|
||||
table.value.doLayout()
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中内容变化
|
||||
* @param data 选中数据
|
||||
*/
|
||||
const selectChange = (data) => {
|
||||
emit('selectChange', data)
|
||||
}
|
||||
/**
|
||||
* 点击某行
|
||||
* @param data 选中数据
|
||||
*/
|
||||
const handleRowClick = (data) => {
|
||||
emit('handleRowClick', data)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
doLayout
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
37
src/components/Title/index.vue
Normal file
37
src/components/Title/index.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="title">
|
||||
<div class="text">{{ title }}</div>
|
||||
<div class="arrow"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
title: {
|
||||
default: '',
|
||||
required: false,
|
||||
type: String
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.title{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
background: rgba(0,192,255,0.2);
|
||||
padding: 0 12px 0 4px;
|
||||
font-family: 'SHSCNB';
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #00C0FF;
|
||||
.arrow{
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #FFC000;
|
||||
clip-path: polygon(8px 0, 8px 8px, 0 8px);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user