first commit

This commit is contained in:
2025-12-24 18:19:05 +08:00
commit 78407f1cbd
283 changed files with 170690 additions and 0 deletions

View 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>

View File

@@ -0,0 +1,332 @@
<template>
<!-- 绑定父组件classvideoWindow -->
<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-UDP1-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-UDP1-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>

View File

@@ -0,0 +1,397 @@
<template>
<!-- 绑定父组件classvideoWindow -->
<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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
}
}

View 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
}
}

View 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()
}
}
}

View 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
}
}
}
}

View 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)
})
})
}
}

View 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
}

View 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
}

View 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()
}
}

View 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>

View 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
}

File diff suppressed because it is too large Load Diff

View 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; // 船长
}

View 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>

View 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>

View 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>

View 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>=>

View 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>

View 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>

View File

@@ -0,0 +1,336 @@
<template>
<!-- 绑定父组件classvideoWindow -->
<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。该字段取值范围”24h12h6h1h36m24m12m”超出该取值范围无效。
////////////////////////////////// 请自行修改以上变量值 ////////////////////////////////////
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-UDP1-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-UDP1-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>

View File

@@ -0,0 +1,367 @@
<template>
<!-- 绑定父组件classvideoWindow -->
<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>

View File

@@ -0,0 +1,397 @@
<template>
<!-- 绑定父组件classvideoWindow -->
<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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>