支持通用通道云台控制,支持地图位置编辑

This commit is contained in:
lin
2025-09-24 16:21:02 +08:00
parent 602cd390e0
commit c0ef35c3a0
19 changed files with 2040 additions and 213 deletions

View File

@@ -19,7 +19,9 @@
ref="regionTree"
:edit="false"
:show-header="false"
:show-position="showPosition"
:has-channel="true"
:contextmenu="contextmenu"
@clickEvent="treeNodeClickEvent"
:default-expanded-keys="[]"
/>
@@ -28,7 +30,9 @@
ref="groupTree"
:edit="false"
:show-header="false"
:show-position="showPosition"
:has-channel="true"
:contextmenu="contextmenu"
@clickEvent="treeNodeClickEvent"
:default-expanded-keys="[]"
/>
@@ -56,6 +60,14 @@ export default {
contextMenuEvent: {
type: Function,
default: null
},
showPosition: {
type: Boolean,
default: false
},
contextmenu: {
type: Array,
default: null
}
},
data() {
@@ -135,6 +147,14 @@ export default {
if (data.leaf) {
this.$emit('clickEvent', data.id)
}
},
refresh: function(id) {
console.log(id)
if (this.showRegion) {
this.$refs.regionTree.refresh(id)
}else {
this.$refs.groupTree.refresh(id)
}
}
}
}

View File

@@ -65,6 +65,7 @@
style=" padding-left: 1px"
:title="node.data.deviceId"
>{{ node.label }}</span>
<span v-if="node.data.longitude && showPosition" class="iconfont icon-gps"></span>
</span>
</template>
</vue-easy-tree>
@@ -130,7 +131,7 @@ export default {
GbChannelSelect,
VueEasyTree, groupEdit, gbDeviceSelect
},
props: ['edit', 'enableAddChannel', 'showHeader', 'hasChannel', 'addChannelToGroup', 'treeHeight'],
props: ['edit', 'enableAddChannel', 'onChannelChange', 'showHeader', 'hasChannel', 'addChannelToGroup', 'treeHeight', 'showPosition', 'contextmenu'],
data() {
return {
props: {
@@ -231,90 +232,111 @@ export default {
this.$forceUpdate()
},
contextmenuEventHandler: function(event, data, node, element) {
if (!this.edit) {
if (!this.edit && !this.contextmenu) {
return
}
const allMenuItem = []
if (node.data.type === 0) {
const menuItem = [
{
label: '刷新节点',
icon: 'el-icon-refresh',
disabled: false,
onClick: () => {
this.refreshNode(node)
}
},
{
label: '新建节点',
icon: 'el-icon-plus',
disabled: false,
onClick: () => {
this.addGroup(data.id, node)
}
},
{
label: '编辑节点',
icon: 'el-icon-edit',
disabled: node.level === 1,
onClick: () => {
this.editGroup(data, node)
}
},
{
label: '删除节点',
icon: 'el-icon-delete',
disabled: node.level === 1,
divided: true,
onClick: () => {
this.$confirm('确定删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.removeGroup(data.id, node)
}).catch(() => {
if (this.edit) {
const menuItem = [
{
label: '刷新节点',
icon: 'el-icon-refresh',
disabled: false,
onClick: () => {
this.refreshNode(node)
}
},
{
label: '新建节点',
icon: 'el-icon-plus',
disabled: false,
onClick: () => {
this.addGroup(data.id, node)
}
},
{
label: '编辑节点',
icon: 'el-icon-edit',
disabled: node.level === 1,
onClick: () => {
this.editGroup(data, node)
}
},
{
label: '删除节点',
icon: 'el-icon-delete',
disabled: node.level === 1,
divided: true,
onClick: () => {
this.$confirm('确定删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.removeGroup(data.id, node)
}).catch(() => {
})
}
}
]
if (this.enableAddChannel) {
menuItem.push(
{
label: '添加设备',
icon: 'el-icon-plus',
disabled: node.level <= 2,
onClick: () => {
this.addChannelFormDevice(data.id, node)
}
}
)
menuItem.push(
{
label: '移除设备',
icon: 'el-icon-delete',
disabled: node.level <= 2,
divided: true,
onClick: () => {
this.removeChannelFormDevice(data.id, node)
}
}
)
menuItem.push(
{
label: '添加通道',
icon: 'el-icon-plus',
disabled: node.level <= 2,
onClick: () => {
this.addChannel(data.id, node)
}
}
)
}
allMenuItem.push(...menuItem)
}
if (this.contextmenu) {
for (let i = 0; i < this.contextmenu.length; i++) {
let item = this.contextmenu[i]
if (item.type === node.data.type) {
allMenuItem.push({
label: item.label,
icon: item.icon,
onClick: () => {
item.onClick(event, data, node)
}
})
}
}
]
if (this.enableAddChannel) {
menuItem.push(
{
label: '添加设备',
icon: 'el-icon-plus',
disabled: node.level <= 2,
onClick: () => {
this.addChannelFormDevice(data.id, node)
}
}
)
menuItem.push(
{
label: '移除设备',
icon: 'el-icon-delete',
disabled: node.level <= 2,
divided: true,
onClick: () => {
this.removeChannelFormDevice(data.id, node)
}
}
)
menuItem.push(
{
label: '添加通道',
icon: 'el-icon-plus',
disabled: node.level <= 2,
onClick: () => {
this.addChannel(data.id, node)
}
}
)
}
if (allMenuItem.length === 0) {
return
}
this.$contextmenu({
items: menuItem,
items: allMenuItem,
event, // 鼠标事件信息
customClass: 'custom-class', // 自定义菜单 class
zIndex: 3000 // 菜单样式 z-index

View File

@@ -17,11 +17,16 @@ import View from 'ol/View'
import Feature from 'ol/Feature'
import Overlay from 'ol/Overlay'
import { Point, LineString } from 'ol/geom'
import { get as getProj, fromLonLat } from 'ol/proj'
import { ZoomSlider, Zoom } from 'ol/control'
import { get as getProj } from 'ol/proj'
import { containsCoordinate } from 'ol/extent'
import {
defaults as defaultInteractions
} from 'ol/interaction'
import DragInteraction from './map/DragInteraction'
import { fromLonLat, toLonLat } from './map/TransformLonLat'
import { v4 } from 'uuid'
import { getUid } from 'ol'
let olMap = null
@@ -30,7 +35,8 @@ export default {
props: [],
data() {
return {
overlayId: null,
dragInteraction: new DragInteraction()
}
},
created() {
@@ -44,34 +50,28 @@ export default {
},
destroyed() {
// if (this.jessibuca) {
// this.jessibuca.destroy();
// }
// this.playing = false;
// this.loaded = false;
// this.performance = "";
},
methods: {
init() {
let center = fromLonLat([116.41020, 39.915119])
if (mapParam.center) {
center = fromLonLat(mapParam.center)
if (window.mapParam.center) {
center = fromLonLat(window.mapParam.center)
}
const view = new View({
center: center,
zoom: mapParam.zoom || 10,
zoom: window.mapParam.zoom || 10,
projection: this.projection,
maxZoom: mapParam.maxZoom || 19,
minZoom: mapParam.minZoom || 1
maxZoom: window.mapParam.maxZoom || 19,
minZoom: window.mapParam.minZoom || 1
})
let tileLayer = null
if (mapParam.tilesUrl) {
if (window.mapParam.tilesUrl) {
tileLayer = new Tile({
source: new XYZ({
projection: getProj('EPSG:3857'),
wrapX: false,
tileSize: 256 || mapParam.tileSize,
url: mapParam.tilesUrl
tileSize: 256 || window.mapParam.tileSize,
url: window.mapParam.tilesUrl
})
})
} else {
@@ -81,18 +81,48 @@ export default {
})
}
olMap = new Map({
interactions: defaultInteractions().extend([this.dragInteraction]),
target: this.$refs.mapContainer, // 容器ID
layers: [tileLayer], // 默认图层
view: view, // 视图
controls: [ // 控件
// new ZoomSlider(),
new Zoom()
]
})
console.log(3222)
olMap.once('loadend', event => {
this.$emit('loaded')
})
olMap.on('click', event => {
let features = {}
let layers = {}
// 单个元素事件传递
olMap.forEachFeatureAtPixel(event.pixel, (featureAtPixel, layerAtPixel) => {
if (layerAtPixel) {
let ol_uid = 'key' + getUid(layerAtPixel)
layers[ol_uid] = layerAtPixel
if (Object.hasOwn(features, ol_uid)) {
features[ol_uid].push(featureAtPixel)
} else {
features[ol_uid] = new Array(featureAtPixel)
}
}
})
// 遍历图层,传递事件
for (const key in layers) {
if (Object.hasOwn(layers, key)) {
var layer = layers[key]
layer.dispatchEvent({ type: 'click', event: event, features: features[key], outParam: { layersCount: Object.keys(layers).length } });
}
}
features = {}
layer = {}
})
},
setCenter(point) {
},
getCenter() {
return toLonLat(olMap.getView().getCenter())
},
zoomIn(zoom) {
@@ -110,23 +140,33 @@ export default {
duration: duration
})
},
panTo(point, zoom) {
const duration = 800
coordinateInView: function(point) {
return containsCoordinate(olMap.getView().calculateExtent(), fromLonLat(point))
},
panTo(point, zoom, endCallback) {
const duration = 1500
var coordinate = fromLonLat(point)
if (containsCoordinate(olMap.getView().calculateExtent(), coordinate)) {
olMap.getView().setCenter(coordinate)
if (endCallback) {
endCallback()
}
return
}
olMap.getView().cancelAnimations()
olMap.getView().animate({
center: fromLonLat(point),
center: coordinate,
duration: duration
})
if (!containsCoordinate(olMap.getView().calculateExtent(), fromLonLat(point))) {
olMap.getView().animate({
zoom: olMap.getView().getZoom() - 1,
duration: duration / 2
}, {
zoom: zoom || olMap.getView().getZoom(),
duration: duration / 2
})
}
olMap.getView().animate({
zoom: 12,
duration: duration / 2
}, {
zoom: zoom || olMap.getView().getZoom(),
duration: duration / 2
})
setTimeout(endCallback, duration + 100)
},
fit(layer) {
const extent = layer.getSource().getExtent()
@@ -138,10 +178,15 @@ export default {
}
},
openInfoBox(position, content, offset) {
if (this.overlayId !== null) {
console.log(this.overlayId)
this.closeInfoBox(this.overlayId)
this.overlayId = null
}
const id = v4()
// let infoBox = document.createElement("div");
// infoBox.innerHTML = content ;
// infoBox.setAttribute("infoBoxId", id)
// let infoBox = document.createElement('div')
// infoBox.setAttribute('id', id)
// infoBox.innerHTML = content
const overlay = new Overlay({
id: id,
autoPan: true,
@@ -150,16 +195,23 @@ export default {
},
element: content,
positioning: 'bottom-center',
offset: offset
offset: offset,
position: fromLonLat(position)
// className:overlayStyle.className
})
olMap.addOverlay(overlay)
overlay.setPosition(fromLonLat(position))
this.overlayId = id
return id
},
closeInfoBox(id) {
olMap.getOverlayById(id).setPosition(undefined)
// olMap.removeOverlay(olMap.getOverlayById(id))
let overlay = olMap.getOverlayById(id)
if (overlay) {
olMap.removeOverlay(overlay)
}
var element = document.getElementById(id)
if (element) {
element.remove()
}
},
/**
* 添加图层
@@ -178,13 +230,13 @@ export default {
* ]
*/
addLayer(data, clickEvent) {
const style = new Style()
if (data.length > 0) {
const features = []
for (let i = 0; i < data.length; i++) {
const feature = new Feature(new Point(fromLonLat(data[i].position)))
feature.setId(data[i].id)
feature.customData = data[i].data
const cloneStyle = style.clone()
const cloneStyle = new Style()
cloneStyle.setImage(new Icon({
anchor: data[i].image.anchor,
crossOrigin: 'Anonymous',
@@ -197,31 +249,88 @@ export default {
source.addFeatures(features)
const vectorLayer = new VectorLayer({
source: source,
style: style,
renderMode: 'image',
declutter: false
})
olMap.addLayer(vectorLayer)
if (typeof clickEvent === 'function') {
olMap.on('click', (event) => {
vectorLayer.getFeatures(event.pixel).then((features) => {
if (features.length > 0) {
const items = []
for (let i = 0; i < features.length; i++) {
items.push(features[i].customData)
}
clickEvent(items)
vectorLayer.on('click', (event) => {
if (event.features.length > 0) {
const items = []
for (let i = 0; i < event.features.length; i++) {
items.push(event.features[i].customData)
}
})
clickEvent(items)
}
})
}
return vectorLayer
}
},
updateLayer(layer, data, postponement) {
console.log(layer)
layer.getSource().clear(true)
const features = []
for (let i = 0; i < data.length; i++) {
const feature = new Feature(new Point(fromLonLat(data[i].position)))
feature.setId(data[i].id)
feature.customData = data[i].data
const cloneStyle = new Style()
cloneStyle.setImage(new Icon({
anchor: data[i].image.anchor,
crossOrigin: 'Anonymous',
src: data[i].image.src
}))
feature.setStyle(cloneStyle)
features.push(feature)
}
layer.getSource().addFeatures(features)
if (postponement) {
olMap.removeLayer(layer)
setTimeout(() => {
olMap.addLayer(layer)
}, 100)
}
return layer
},
removeLayer(layer) {
olMap.removeLayer(layer)
},
setFeatureImageById(layer, featureId, image) {
let feature = layer.getSource().getFeatureById(featureId)
if (!feature) {
console.error('更改feature的图标时未找到图标')
return
}
let style = feature.getStyle()
style.setImage(new Icon({
anchor: image.anchor,
crossOrigin: 'Anonymous',
src: image.src
}))
feature.setStyle(style)
olMap.render()
},
setFeaturePositionById(layer, featureId, data) {
let featureOld = layer.getSource().getFeatureById(featureId)
if (featureOld) {
layer.getSource().removeFeature(featureOld)
}
const feature = new Feature(new Point(fromLonLat(data.position)))
feature.setId(data.id)
feature.customData = data.data
const style = new Style()
style.setImage(new Icon({
anchor: data.image.anchor,
crossOrigin: 'Anonymous',
src: data.image.src
}))
feature.setStyle(style)
layer.getSource().addFeature(feature)
},
addLineLayer(positions) {
if (positions.length > 0) {

View File

@@ -56,15 +56,11 @@
class="iconfont icon-shexiangtou2"
/>
<span
v-if="node.data.deviceId !=='' && showCode"
style=" padding-left: 1px"
:title="node.data.deviceId"
>{{ node.label }}编号{{ node.data.deviceId }}</span>
<span
v-if="node.data.deviceId ==='' || !showCode"
style=" padding-left: 1px"
:title="node.data.deviceId"
>{{ node.label }}</span>
<span v-if="node.data.deviceId !=='' && showCode">编号{{ node.data.deviceId }}</span>
<span v-if="node.data.longitude && showPosition" class="iconfont icon-gps"></span>
</span>
</template>
</vue-easy-tree>
@@ -131,7 +127,7 @@ export default {
GbChannelSelect,
VueEasyTree, regionEdit, gbDeviceSelect
},
props: ['edit', 'enableAddChannel', 'showHeader', 'hasChannel', 'addChannelToCivilCode', 'treeHeight'],
props: ['edit', 'enableAddChannel', 'onChannelChange', 'showHeader', 'hasChannel', 'addChannelToCivilCode', 'treeHeight', 'showPosition', 'contextmenu'],
data() {
return {
props: {
@@ -236,95 +232,115 @@ export default {
this.$forceUpdate()
},
contextmenuEventHandler: function(event, data, node, element) {
if (!this.edit) {
if (!this.edit && !this.contextmenu) {
return
}
const allMenuItem = []
if (node.data.type === 0) {
const menuItem = [
{
label: '刷新节点',
icon: 'el-icon-refresh',
disabled: false,
onClick: () => {
this.refreshNode(node)
}
},
{
label: '新建节点',
icon: 'el-icon-plus',
disabled: false,
onClick: () => {
this.addRegion(data.id, node)
}
},
{
label: '编辑节点',
icon: 'el-icon-edit',
disabled: node.level === 1,
onClick: () => {
this.editCatalog(data, node)
}
},
{
label: '删除节点',
icon: 'el-icon-delete',
disabled: node.level === 1,
divided: true,
onClick: () => {
this.$confirm('确定删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.removeRegion(data.id, node)
}).catch(() => {
if (this.edit) {
})
}
}
]
if (this.enableAddChannel) {
menuItem.push(
const menuItem = [
{
label: '添加设备',
label: '刷新节点',
icon: 'el-icon-refresh',
disabled: false,
onClick: () => {
this.refreshNode(node)
}
},
{
label: '新建节点',
icon: 'el-icon-plus',
disabled: false,
onClick: () => {
this.addRegion(data.id, node)
}
},
{
label: '编辑节点',
icon: 'el-icon-edit',
disabled: node.level === 1,
onClick: () => {
this.addChannelFormDevice(data.id, node)
this.editCatalog(data, node)
}
}
)
menuItem.push(
},
{
label: '移除设备',
label: '删除节点',
icon: 'el-icon-delete',
disabled: node.level === 1,
divided: true,
onClick: () => {
this.removeChannelFormDevice(data.id, node)
this.$confirm('确定删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.removeRegion(data.id, node)
}).catch(() => {
})
}
}
)
menuItem.push(
{
label: '添加通道',
icon: 'el-icon-plus',
disabled: node.level === 1,
onClick: () => {
this.addChannel(data.id, node)
]
if (this.enableAddChannel) {
menuItem.push(
{
label: '添加设备',
icon: 'el-icon-plus',
disabled: node.level === 1,
onClick: () => {
this.addChannelFormDevice(data.id, node)
}
}
}
)
)
menuItem.push(
{
label: '移除设备',
icon: 'el-icon-delete',
disabled: node.level === 1,
divided: true,
onClick: () => {
this.removeChannelFormDevice(data.id, node)
}
}
)
menuItem.push(
{
label: '添加通道',
icon: 'el-icon-plus',
disabled: node.level === 1,
onClick: () => {
this.addChannel(data.id, node)
}
}
)
}
allMenuItem.push(...menuItem)
}
this.$contextmenu({
items: menuItem,
event, // 鼠标事件信息
customClass: 'custom-class', // 自定义菜单 class
zIndex: 3000 // 菜单样式 z-index
})
}
if (this.contextmenu) {
for (let i = 0; i < this.contextmenu.length; i++) {
let item = this.contextmenu[i]
if (item.type === node.data.type) {
allMenuItem.push({
label: item.label,
icon: item.icon,
onClick: () => {
item.onClick(event, data, node)
}
})
}
}
}
if (allMenuItem.length === 0) {
return
}
this.$contextmenu({
items: allMenuItem,
event, // 鼠标事件信息
customClass: 'custom-class', // 自定义菜单 class
zIndex: 3000 // 菜单样式 z-index
})
return false
},
removeRegion: function(id, node) {
@@ -396,12 +412,16 @@ export default {
node.expand()
},
refresh: function(id) {
console.log(id)
// 查询node
const node = this.$refs.veTree.getNode(id)
if (node) {
node.loaded = false
node.expand()
if (id.includes('channel') >= 0) {
node.parent.loaded = false
node.parent.expand()
}else {
node.loaded = false
node.expand()
}
}
},
addRegion: function(id, node) {

View File

@@ -0,0 +1,116 @@
import PointerInteraction from 'ol/interaction/Pointer'
import { toLonLat } from './TransformLonLat'
class DragInteraction extends PointerInteraction {
constructor() {
super({
handleDownEvent: (evt) => {
const map = evt.map
const feature = map.forEachFeatureAtPixel(evt.pixel, (feature) => {
if (this.featureIdMap_.has(feature.getId())) {
return feature
}else {
return null
}
})
if (feature) {
this.coordinate_ = evt.coordinate
this.feature_ = feature
let eventCallback = this.featureIdMap_.get(this.feature_.getId())
if (eventCallback && eventCallback.startEvent) {
eventCallback.startEvent(evt)
}
return !!feature
}
},
handleDragEvent: (evt) => {
const deltaX = evt.coordinate[0] - this.coordinate_[0]
const deltaY = evt.coordinate[1] - this.coordinate_[1]
const geometry = this.feature_.getGeometry()
geometry.translate(deltaX, deltaY)
this.coordinate_[0] = evt.coordinate[0]
this.coordinate_[1] = evt.coordinate[1]
let eventCallback = this.featureIdMap_.get(this.feature_.getId())
if (eventCallback && eventCallback.moveEvent) {
eventCallback.moveEvent(evt)
}
},
handleMoveEvent: (evt) => {
if (this.cursor_) {
const map = evt.map
const feature = map.forEachFeatureAtPixel(evt.pixel, function(feature) {
return feature
})
const element = evt.map.getTargetElement()
if (feature) {
if (element.style.cursor != this.cursor_) {
this.previousCursor_ = element.style.cursor
element.style.cursor = this.cursor_
}
} else if (this.previousCursor_ !== undefined) {
element.style.cursor = this.previousCursor_
this.previousCursor_ = undefined
}
}
},
handleUpEvent: (evt) => {
let eventCallback = this.featureIdMap_.get(this.feature_.getId())
if (eventCallback && eventCallback.endEvent) {
evt.lonLat = toLonLat(this.feature_.getGeometry().getCoordinates())
eventCallback.endEvent(evt)
}
this.coordinate_ = null
this.feature_ = null
return false
}
})
/**
* @type {import('../src/ol/coordinate.js').Coordinate}
* @private
*/
this.coordinate_ = null
/**
* @type {string|undefined}
* @private
*/
this.cursor_ = 'pointer'
/**
* @type {Feature}
* @private
*/
this.feature_ = null
/**
* @type {string|undefined}
* @private
*/
this.previousCursor_ = undefined
this.featureIdMap_ = new Map()
this.addFeatureId = (id, moveEndEvent) => {
if (this.featureIdMap_.has(id)) {
return
}
this.featureIdMap_.set(id, moveEndEvent)
}
this.removeFeatureId= (id) => {
this.featureIdMap_.delete(id)
}
this.hasFeatureId= (id) => {
this.featureIdMap_.has(id)
}
}
}
export default DragInteraction

View File

@@ -0,0 +1,9 @@
import { fromLonLat as projFromLonLat, toLonLat as projToLonLat } from 'ol/proj'
import gcoord from 'gcoord'
export function fromLonLat(coordinate) {
return projFromLonLat(gcoord.transform(coordinate, gcoord.WGS84, gcoord.GCJ02))
}
export function toLonLat(coordinate) {
return gcoord.transform(projToLonLat(coordinate), gcoord.GCJ02, gcoord.WGS84)
}

433
web/src/views/map/index.vue Executable file
View File

@@ -0,0 +1,433 @@
<template>
<div id="devicePosition" style="height: calc(100vh - 84px);width: 100%;">
<div style="height: 100%; display: grid; grid-template-columns: 360px auto">
<DeviceTree ref="deviceTree" @clickEvent="treeChannelClickEvent" :showPosition="true" :contextmenu="getContextmenu()"/>
<MapComponent ref="mapComponent" @loaded="initChannelLayer"></MapComponent>
</div>
<div class="map-tool-box">
<div class="map-tool-btn-group">
<div class="map-tool-btn" @click="initChannelLayer">
<i class="iconfont icon-shuaxin3"></i>
</div>
</div>
<div class="map-tool-btn-group">
<div class="map-tool-btn">
<i class="iconfont icon-plus1"></i>
</div>
<div class="map-tool-btn">
<i class="iconfont icon-minus1"></i>
</div>
</div>
</div>
<div ref="infobox">
<transition name="el-zoom-in-center">
<div class="infobox-content" v-if="channel">
<el-descriptions class="margin-top" :title="channel.gbName" :column="1" :colon="true" size="mini" :labelStyle="labelStyle" >
<el-descriptions-item label="编号" >{{channel.gbDeviceId}}</el-descriptions-item>
<el-descriptions-item label="生产厂商">{{channel.gbManufacture}}</el-descriptions-item>
<el-descriptions-item label="安装地址" >{{channel.gbAddress == null?'未知': channel.gbAddress}}</el-descriptions-item>
</el-descriptions>
<div style="padding-top: 10px; margin: 0 auto; width: fit-content;">
<el-button v-bind:disabled="channel.gbStatus !== 'ON'" type="primary" size="small" title="播放" icon="el-icon-video-play" @click="play(channel)">播放</el-button>
<el-button type="primary" size="small" title="编辑位置" icon="el-icon-edit" @click="edit(channel)">编辑</el-button>
<!-- <el-button type="primary" size="small" title="轨迹查询" icon="el-icon-map-location" @click="getTrace(channel)">轨迹</el-button>-->
</div>
<span class="infobox-close el-icon-close" @click="closeInfoBox"></span>
</div>
</transition>
</div>
<div ref="infoboxForEdit">
<transition name="el-zoom-in-center">
<div class="infobox-edit-content" v-if="dragChannel">
<div style="width: 100%; height: 2rem; line-height: 1.5rem; font-size: 14px">{{dragChannel.gbName}} ({{dragChannel.gbDeviceId}})</div>
<span style="font-size: 14px">经度:</span> <el-input v-model="dragChannel.gbLongitude" placeholder="请输入经度" style="width: 7rem; margin-right: 10px"></el-input>
<span style="font-size: 14px">纬度: </span> <el-input v-model="dragChannel.gbLatitude" placeholder="请输入纬度" style="width: 7rem; "></el-input>
<el-button icon="el-icon-close" size="medium" type="text" @click="cancelEdit(dragChannel)" style="margin-left: 1rem; font-size: 18px; color: #2b2f3a"></el-button>
<el-button icon="el-icon-check" size="medium" type="text" @click="submitEdit(dragChannel)" style="font-size: 18px; color: #0842e2"></el-button>
</div>
</transition>
</div>
<devicePlayer ref="devicePlayer" ></devicePlayer>
<queryTrace ref="queryTrace" ></queryTrace>
</div>
</template>
<script>
import DeviceTree from '../common/DeviceTree.vue'
import queryTrace from './queryTrace.vue'
import MapComponent from '../common/MapComponent.vue'
import devicePlayer from '../common/channelPlayer/index.vue'
export default {
name: 'Map',
components: {
DeviceTree,
devicePlayer,
queryTrace,
MapComponent
},
data() {
return {
layer: null,
channel: null,
dragChannel: {},
feature: null,
device: null,
infoBoxId: null,
channelLayer: null,
labelStyle: {
width: '56px'
},
isLoging: false,
longitudeStr: 'longitude',
latitudeStr: 'latitude'
}
},
created() {
},
destroyed() {
},
methods: {
treeChannelClickEvent: function (id) {
this.closeInfoBox()
this.$store.dispatch('commonChanel/queryOne', id)
.then(data => {
if (!data.gbLongitude || data.gbLongitude < 0) {
return
}
if (this.$refs.mapComponent.coordinateInView([data.gbLongitude, data.gbLatitude])) {
this.showChannelInfo(data)
}else {
this.$refs.mapComponent.panTo([data.gbLongitude, data.gbLatitude], 16, () => {
this.showChannelInfo(data)
})
}
})
},
getContextmenu: function (event) {
return [
{
label: '播放通道',
icon: 'el-icon-video-play',
type: 1,
onClick: (event, data, node) => {
this.$store.dispatch('commonChanel/queryOne', data.id)
.then(data => {
this.play(data)
})
}
},
{
label: '编辑位置',
icon: 'el-icon-edit',
type: 1,
onClick: (event, data, node) => {
this.$store.dispatch('commonChanel/queryOne', data.id)
.then(data => {
this.edit(data)
})
}
}
// ,
// {
// label: '轨迹查询',
// icon: 'el-icon-map-location',
// type: 1,
// onClick: (event, data, node) => {
//
// }
// }
]
},
showChannelInfo: function(data) {
this.channel = data
this.infoBoxId = this.$refs.mapComponent.openInfoBox([data.gbLongitude, data.gbLatitude], this.$refs.infobox, [0, -50])
},
initChannelLayer: function () {
// 获取所有有位置的通道
this.closeInfoBox()
this.$store.dispatch('commonChanel/getAllForMap', {
geoCoordSys: window.mapParam.coordinateSystem
}).then(data => {
let array = []
for (let i = 0; i < data.length; i++) {
let item = data[i]
if (item.gbLongitude && item.gbLatitude) {
array.push({
id: item.gbId,
position: [item.gbLongitude, item.gbLatitude],
data: item,
image: {
anchor: [0.5, 1],
src: this.getImageByChannel(item)
}
})
}
}
this.updateChannelLayer(array)
})
},
updateChannelLayer: function(array) {
if (this.channelLayer) {
this.channelLayer = this.$refs.mapComponent.updateLayer(this.channelLayer, array, true)
}else {
this.channelLayer = this.$refs.mapComponent.addLayer(array, data => {
this.closeInfoBox()
this.$nextTick(() => {
if (data[0].edit) {
this.showEditInfo(data[0])
}else {
this.showChannelInfo(data[0])
}
})
})
}
},
getImageByChannel: function (channel) {
if (channel.gbStatus === 'ON') {
return 'static/images/gis/camera1.png'
} else {
return 'static/images/gis/camera1-offline.png'
}
},
closeInfoBox: function () {
if (this.infoBoxId !== null) {
this.$refs.mapComponent.closeInfoBox(this.infoBoxId)
}
this.channel = null
this.dragChannel = null
},
play: function (channel) {
const loading = this.$loading({
lock: true,
text: '正在请求视频',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
})
this.$store.dispatch('commonChanel/playChannel', channel.gbId)
.then((data) => {
this.$refs.devicePlayer.openDialog('media', channel.gbId, {
streamInfo: data,
hasAudio: channel.hasAudio
})
}).finally(() => {
loading.close()
})
},
edit: function (channel) {
this.closeInfoBox()
// 开启图标可拖动
this.$refs.mapComponent.dragInteraction.addFeatureId(channel.gbId,
{
startEvent: event => {
this.closeInfoBox()
},
endEvent: event => {
channel.gbLongitude = event.lonLat[0]
channel.gbLatitude = event.lonLat[1]
this.showEditInfo(channel)
}
}
)
let position = null
if (!!channel.gbLongitude && !!channel.gbLatitude && channel.gbLongitude > 0 && channel.gbLatitude > 0) {
position = [channel.gbLongitude, channel.gbLatitude]
channel['oldLongitude'] = channel.gbLongitude
channel['oldLatitude'] = channel.gbLatitude
}else {
position = this.$refs.mapComponent.getCenter()
channel['oldLongitude'] = channel.gbLongitude
channel['oldLatitude'] = channel.gbLatitude
channel.gbLongitude = position[0]
channel.gbLatitude = position[1]
}
channel['edit'] = true
if (!this.$refs.mapComponent.coordinateInView(position)) {
this.$refs.mapComponent.panTo(position, 16, () => {
this.showEditInfo(channel)
})
}else {
this.showEditInfo(channel)
}
// 标记可编辑图标为红色
this.$refs.mapComponent.setFeaturePositionById(this.channelLayer, channel.gbId, {
id: channel.gbId,
position: position,
data: channel,
image: {
anchor: [0.5, 1],
src: 'static/images/gis/camera1-red.png'
}
})
},
showEditInfo: function(data) {
this.dragChannel = data
this.infoBoxId = this.$refs.mapComponent.openInfoBox([data.gbLongitude, data.gbLatitude], this.$refs.infoboxForEdit, [0, -50])
},
cancelEdit: function(channel) {
this.closeInfoBox()
this.$refs.mapComponent.dragInteraction.removeFeatureId(channel.gbId)
channel.gbLongitude = channel.oldLongitude
channel.gbLatitude = channel.oldLatitude
channel['edit'] = false
this.$refs.mapComponent.setFeaturePositionById(this.channelLayer, channel.gbId, {
id: channel.gbId,
position: [channel.gbLongitude, channel.gbLatitude],
data: channel,
image: {
anchor: [0.5, 1],
src: this.getImageByChannel(channel)
}
})
},
submitEdit: function(channel) {
this.$store.dispatch('commonChanel/update', channel)
.then(data => {
this.$message.success({
showClose: true,
message: '保存成功'
})
this.closeInfoBox()
channel['edit'] = false
this.$refs.mapComponent.dragInteraction.removeFeatureId(channel.gbId)
this.$refs.mapComponent.setFeaturePositionById(this.channelLayer, channel.gbId, {
id: channel.gbId,
position: [channel.gbLongitude, channel.gbLatitude],
data: channel,
image: {
anchor: [0.5, 1],
src: this.getImageByChannel(channel)
}
})
// 刷星树菜单
this.$refs.deviceTree.refresh('channel' + channel.gbId)
})
},
getTrace: function (data) {
this.clean()
this.$refs.queryTrace.openDialog(data, (channelPositions) => {
if (channelPositions.length === 0) {
this.$message.info({
showClose: true,
message: '未查询到轨迹信息'
})
} else {
let positions = []
for (let i = 0; i < channelPositions.length; i++) {
if (channelPositions[i][this.longitudeStr] * channelPositions[i][this.latitudeStr] > 0) {
positions.push([channelPositions[i][this.longitudeStr], channelPositions[i][this.latitudeStr]])
}
}
if (positions.length === 0) {
this.$message.info({
showClose: true,
message: '未查询到轨迹信息'
})
return
}
}
})
},
clean: function (){
if (this.infoBoxId !== null) {
this.$refs.mapComponent.closeInfoBox(this.infoBoxId)
}
}
}
}
</script>
<style>
.map-tool-box {
position: absolute;
right: 20px;
bottom: 20px;
}
.map-tool-btn-group {
background-color: #FFFFFF;
border-radius: 3px;
user-select: none;
box-shadow: 0 2px 2px rgba(0, 0, 0, .15);
margin-bottom: 10px;
}
.map-tool-btn {
border-bottom: 1px #dfdfdf solid;
width: 33px;
height: 36px;
cursor: pointer;
text-align: center;
line-height: 36px;
font-size: 14px;
}
.map-tool-btn i {
font-size: 14px;
}
.map-tool-btn-group:last-child {
border-bottom: none;
}
.infobox-content{
width: 270px;
background-color: #FFFFFF;
padding: 10px;
border-radius: 10px;
border: 1px solid #868686;
}
.infobox-content::after {
position: absolute;
bottom: -11px;
left: calc(50% - 8px);
display: block;
content: "";
width: 16px;
height: 16px;
background: url('/static/images/arrow.png') no-repeat center;
}
.infobox-edit-content{
width: 400px;
background-color: #FFFFFF;
padding: 10px;
border-radius: 10px;
border: 1px solid #868686;
}
.infobox-edit-content::after {
position: absolute;
bottom: -11px;
left: calc(50% - 8px);
display: block;
content: "";
width: 16px;
height: 16px;
background: url('/static/images/arrow.png') no-repeat center;
}
.infobox-close {
position: absolute;
right: 1rem;
top: 1rem;
color: #000000;
cursor:pointer
}
.el-descriptions__title {
font-size: 1rem;
font-weight: 700;
padding: 20px 20px 0px 23px;
text-align: center;
width: 100%;
}
</style>

105
web/src/views/map/queryTrace.vue Executable file
View File

@@ -0,0 +1,105 @@
<template>
<div id="queryTrace" >
<el-dialog
title="查询轨迹"
width="40%"
top="2rem"
:close-on-click-modal="false"
:visible.sync="showDialog"
:destroy-on-close="true"
@close="close()"
>
<div v-loading="isLoging">
<el-date-picker v-model="searchFrom" type="datetime" placeholder="选择开始日期时间" default-time="00:00:00" value-format="yyyy-MM-dd HH:mm:ss" size="mini" style="width: 11rem;" align="right" :picker-options="pickerOptions"></el-date-picker>
<el-date-picker v-model="searchTo" type="datetime" placeholder="选择结束日期时间" default-time="00:00:00" value-format="yyyy-MM-dd HH:mm:ss" size="mini" style="width: 11rem;" align="right" :picker-options="pickerOptions"></el-date-picker>
<el-button icon="el-icon-search" size="mini" type="primary" @click="onSubmit">查询</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'deviceEdit',
props: [],
computed: {},
created() {},
data() {
return {
pickerOptions: {
shortcuts: [{
text: '今天',
onClick(picker) {
picker.$emit('pick', new Date())
}
}, {
text: '昨天',
onClick(picker) {
const date = new Date()
date.setTime(date.getTime() - 3600 * 1000 * 24)
picker.$emit('pick', date)
}
}, {
text: '一周前',
onClick(picker) {
const date = new Date()
date.setTime(date.getTime() - 3600 * 1000 * 24 * 7)
picker.$emit('pick', date)
}
}]
},
searchFrom: null,
searchTo: null,
listChangeCallback: null,
showDialog: false,
isLoging: false,
channel: null,
callback: null
}
},
methods: {
openDialog: function (channel, callback) {
console.log(channel)
this.showDialog = true
this.callback = callback
this.channel = channel
},
onSubmit: function () {
console.log('onSubmit')
this.isLoging = true
let url = `/api/position/history/${this.channel.deviceId}?start=${this.searchFrom}&end=${this.searchTo}`
if (this.channel.channelId) {
url+='&channelId=${this.channel.channelId}'
}
this.$axios.get(url, {
}).then((res) => {
this.isLoging = false
if (typeof this.callback === 'function') {
if (res.data.code == 0) {
this.callback(res.data.data)
this.close()
}else {
this.$message.error({
showClose: true,
message: res.data.msg
})
}
}
}).catch(function (error) {
this.isLoging = false
console.error(error)
})
},
close: function () {
this.showDialog = false
this.isLoging = false
this.callback = null
this.channel = null
}
}
}
</script>