Browse Source

feat: 监控地图工具条、比例尺与绑定虚线

支持卫星/矢量底图切换、fitAll、动态比例尺;选中飞行关系时绘制机巢到无人机虚线。
main
xiaosi 3 weeks ago
parent
commit
b90eebe6e0
  1. 24
      src/config/map.js
  2. 1
      src/styles/prototype.css
  3. 111
      src/views/MonitorView/MonitorView.vue

24
src/config/map.js

@ -9,7 +9,7 @@ export default {
zoom: Number(import.meta.env.APP_MAP_ZOOM) || 5,
}
/** 天地图影像 + 注记(WGS84 lk 系列) */
/** 天地图影像/矢量 + 注记(Web 墨卡托 w 系列) */
export function createTiandituLayerOptions(tk) {
return {
img: {
@ -20,5 +20,27 @@ export function createTiandituLayerOptions(tk) {
urlTemplate: `https://t{s}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tk}`,
subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
},
vec: {
urlTemplate: `https://t{s}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tk}`,
subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
},
cva: {
urlTemplate: `https://t{s}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tk}`,
subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
},
}
}
export function createTiandituBaseLayer(maptalks, tk, mode = 'satellite') {
const layers = createTiandituLayerOptions(tk)
if (mode === 'street') {
return new maptalks.GroupTileLayer('tianditu-base', [
new maptalks.TileLayer('tdt-vec', layers.vec),
new maptalks.TileLayer('tdt-cva', layers.cva),
])
}
return new maptalks.GroupTileLayer('tianditu-base', [
new maptalks.TileLayer('tdt-img', layers.img),
new maptalks.TileLayer('tdt-cia', layers.cia),
])
}

1
src/styles/prototype.css

@ -175,6 +175,7 @@ svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width:
.binding-line text { fill: #fff; font-size: 2.4px; text-anchor: middle; paint-order: stroke; stroke: rgba(34,77,111,.88); stroke-width: .8px; }
.map-view-all { height: 34px; padding: 0 10px; border-left: 1px solid var(--line); color: var(--blue); background: #fff; font-size: 11px; }
.map-view-all:hover { background: #f3f8fc; }
.map-scale { position: absolute; z-index: 4; right: 12px; bottom: 14px; color: #fff; font-size: 10px; border-bottom: 2px solid #fff; padding: 0 16px 2px; text-shadow: 0 1px 3px #000; pointer-events: none; }
@keyframes marker-pulse { 50% { filter: drop-shadow(0 0 6px rgba(204,61,67,.7)); } }
.video-unavailable { color: var(--muted); font-size: 10px; }

111
src/views/MonitorView/MonitorView.vue

@ -162,11 +162,9 @@
<div class="map-panel">
<div class="map-toolbar">
<span class="map-type active">天地图</span>
<button class="icon-button" title="放大" @click="zoomIn"><svg><use href="#i-plus" /></svg></button>
<button class="icon-button" title="缩小" @click="zoomOut"><svg><use href="#i-minus" /></svg></button>
<button class="icon-button" title="刷新" @click="reload"><svg><use href="#i-refresh" /></svg></button>
<button class="map-view-all" type="button" @click="clearSelection">查看全部设备</button>
<button class="map-type" type="button" :class="{ active: mapMode === 'satellite' }" @click="setMapMode('satellite')">卫星</button>
<button class="map-type" type="button" :class="{ active: mapMode === 'street' }" @click="setMapMode('street')">地图</button>
<button class="icon-button" title="查看全部设备" @click="fitAll"><svg><use href="#i-maximize" /></svg></button>
</div>
<div class="map-legend">
@ -175,6 +173,7 @@
<span><i class="red"></i>告警</span>
<span><i class="gray"></i>离线</span>
</div>
<div class="map-scale">{{ mapScaleText }}</div>
</div>
<aside class="detail-panel" :class="{ 'drone-selected': isDrone }">
@ -369,11 +368,12 @@ import { useUiStore } from '@/stores/modules/uiStore'
import { useUserStore } from '@/stores/modules/userStore'
import commonRefs from '@/utils/commonRefs'
import mapHelper from '@/core/mapHelper'
import mapConfig from '@/config/map'
import mapConfig, { createTiandituBaseLayer } from '@/config/map'
import LivePlayer from '@/components/LivePlayer.vue'
import { getLivePlayURL, heartbeatLive, joinLive, leaveLive } from '@/api/live'
const LAYER_ID = 'monitor-devices'
const BINDING_LAYER_ID = 'monitor-binding'
const STATUS_COLORS = {
online: '#21a06a',
@ -402,6 +402,10 @@ const selectedDockIds = reactive(new Set())
let mapInstance = null
let deviceLayer = null
let bindingLayer = null
let bindingLine = null
const mapMode = ref('satellite')
const mapScaleText = ref('2 km')
let suppressMapClick = false
const markerById = new Map()
@ -502,6 +506,88 @@ function syncMarkers() {
markerById.delete(id)
}
}
syncBindingLine()
}
function updateMapScale() {
if (!mapInstance) return
const zoom = mapInstance.getZoom()
// rough WebMercator meters-per-pixel at equator * 100px bar
const meters = (156543.03392 / (2 ** zoom)) * 100
if (meters >= 1000) mapScaleText.value = `${Math.round(meters / 1000)} km`
else if (meters >= 100) mapScaleText.value = `${Math.round(meters / 10) * 10} m`
else mapScaleText.value = `${Math.max(1, Math.round(meters))} m`
}
function bindingEndpoints() {
if (!selectedId.value) return null
const asset = devices.asset(selectedId.value)
if (!asset) return null
let dock = null
let drone = null
if (asset.type === 'dock') {
dock = asset
const child = (asset.children || []).map((c) => devices.asset(c.id || c)).find((a) => a && !a.inDock && a.hasCoordinates)
drone = child || null
} else {
drone = asset.inDock ? null : asset
dock = asset.parent ? devices.asset(asset.parent) : null
}
if (!dock?.hasCoordinates || !drone?.hasCoordinates) return null
if (drone.inDock) return null
const a = [Number(dock.longitude), Number(dock.latitude)]
const b = [Number(drone.longitude), Number(drone.latitude)]
if (!a.every(Number.isFinite) || !b.every(Number.isFinite)) return null
return [a, b]
}
function syncBindingLine() {
if (!bindingLayer) return
const ends = bindingEndpoints()
if (!ends) {
if (bindingLine) {
bindingLine.remove()
bindingLine = null
}
return
}
if (!bindingLine) {
bindingLine = new maptalks.LineString(ends, {
symbol: {
lineColor: 'rgba(117,191,246,0.95)',
lineWidth: 2,
lineDasharray: [8, 6],
lineOpacity: 0.95,
},
properties: { label: '绑定设备' },
})
bindingLine.addTo(bindingLayer)
} else {
bindingLine.setCoordinates(ends)
}
}
function setMapMode(mode) {
if (!mapInstance || mapMode.value === mode) {
mapMode.value = mode
return
}
const tk = mapConfig.token
if (!tk) {
ui.toast('缺少天地图 token,无法切换底图')
return
}
mapMode.value = mode
mapInstance.setBaseLayer(createTiandituBaseLayer(maptalks, tk, mode))
}
function fitAll() {
if (selectedId.value) {
clearSelection()
return
}
fitAllMarkers()
}
function fitAllMarkers() {
@ -557,10 +643,17 @@ async function setupMap() {
const existing = map.getLayer(LAYER_ID)
if (existing) existing.remove()
const existingBinding = map.getLayer(BINDING_LAYER_ID)
if (existingBinding) existingBinding.remove()
deviceLayer = new maptalks.VectorLayer(LAYER_ID, [], { zIndex: 120, forceRenderOnMoving: true })
deviceLayer.addTo(map)
bindingLayer = new maptalks.VectorLayer(BINDING_LAYER_ID, [], { zIndex: 110, forceRenderOnMoving: true })
bindingLayer.addTo(map)
map.on('click', onMapBlankClick)
map.on('zoomend', updateMapScale)
map.on('moveend', updateMapScale)
updateMapScale()
syncMarkers()
if (selectedId.value) flyToSelected()
@ -570,10 +663,16 @@ async function setupMap() {
function teardownMap() {
if (mapInstance) {
mapInstance.off('click', onMapBlankClick)
mapInstance.off('zoomend', updateMapScale)
mapInstance.off('moveend', updateMapScale)
const layer = mapInstance.getLayer(LAYER_ID)
if (layer) layer.remove()
const binding = mapInstance.getLayer(BINDING_LAYER_ID)
if (binding) binding.remove()
}
markerById.clear()
bindingLine = null
bindingLayer = null
deviceLayer = null
mapInstance = null
commonRefs.clearPendingList('map')

Loading…
Cancel
Save