Browse Source

feat: 监控地图改水滴 symbol 并默认全球总览相机

加载状态 pin;清选中/查看全部回中国居中;去掉进页 fitBounds。
main
xiaosi 3 weeks ago
parent
commit
e03b317289
  1. 160
      src/views/MonitorView/MonitorView.vue

160
src/views/MonitorView/MonitorView.vue

@ -362,28 +362,29 @@
<script setup> <script setup>
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue' import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import mapboxgl from 'mapbox-gl'
import { useDeviceStore } from '@/stores/modules/devicesStore' import { useDeviceStore } from '@/stores/modules/devicesStore'
import { useUiStore } from '@/stores/modules/uiStore' import { useUiStore } from '@/stores/modules/uiStore'
import { useUserStore } from '@/stores/modules/userStore' import { useUserStore } from '@/stores/modules/userStore'
import commonRefs from '@/utils/commonRefs' import commonRefs from '@/utils/commonRefs'
import mapHelper from '@/core/mapHelper' import mapHelper from '@/core/mapHelper'
import mapConfig, { MAP_STYLES } from '@/config/map'
import mapConfig, { MAP_STYLES, MAP_OVERVIEW, MAP_VIEW } from '@/config/map'
import LivePlayer from '@/components/LivePlayer.vue' import LivePlayer from '@/components/LivePlayer.vue'
import { getLivePlayURL, heartbeatLive, joinLive, leaveLive } from '@/api/live' import { getLivePlayURL, heartbeatLive, joinLive, leaveLive } from '@/api/live'
const DEVICE_SOURCE = 'monitor-devices' const DEVICE_SOURCE = 'monitor-devices'
const DEVICE_CIRCLE = 'monitor-devices-circle'
const DEVICE_SYMBOL = 'monitor-devices-symbol'
const DEVICE_HALO = 'monitor-devices-halo'
const DEVICE_LABEL = 'monitor-devices-label' const DEVICE_LABEL = 'monitor-devices-label'
const BINDING_SOURCE = 'monitor-binding' const BINDING_SOURCE = 'monitor-binding'
const BINDING_LINE = 'monitor-binding-line' const BINDING_LINE = 'monitor-binding-line'
const STATUS_COLORS = {
online: '#21a06a',
mission: '#2d82da',
alarm: '#cc3d43',
offline: '#84909b',
pending: '#c48a2a',
const PIN_KINDS = ['dock', 'drone']
const PIN_STATUSES = ['online', 'mission', 'alarm', 'offline', 'pending']
function pinImageId(kind, status) {
const s = PIN_STATUSES.includes(status) ? status : 'offline'
const k = PIN_KINDS.includes(kind) ? kind : 'dock'
return `${k}-pin-${s}`
} }
const devices = useDeviceStore() const devices = useDeviceStore()
@ -442,9 +443,6 @@ const mapMarkers = computed(() =>
Object.values(devices.assets).filter((a) => a.hasCoordinates && !a.inDock) Object.values(devices.assets).filter((a) => a.hasCoordinates && !a.inDock)
) )
function statusColor(statusClass) {
return STATUS_COLORS[statusClass] || STATUS_COLORS.offline
}
function devicesFeatureCollection() { function devicesFeatureCollection() {
return { return {
@ -455,6 +453,7 @@ function devicesFeatureCollection() {
const lat = Number(asset.latitude) const lat = Number(asset.latitude)
if (!Number.isFinite(lon) || !Number.isFinite(lat)) return null if (!Number.isFinite(lon) || !Number.isFinite(lat)) return null
const selected = selectedId.value === asset.id const selected = selectedId.value === asset.id
const status = asset.statusClass || 'offline'
return { return {
type: 'Feature', type: 'Feature',
// asset.id numeric Feature.id setData // asset.id numeric Feature.id setData
@ -462,9 +461,9 @@ function devicesFeatureCollection() {
assetId: String(asset.id), assetId: String(asset.id),
name: asset.name || '', name: asset.name || '',
kind: asset.type, kind: asset.type,
status,
selected: selected ? 1 : 0, selected: selected ? 1 : 0,
color: statusColor(asset.statusClass),
radius: asset.type === 'drone' ? (selected ? 10 : 8) : (selected ? 12 : 10),
icon: pinImageId(asset.type, status),
}, },
geometry: { type: 'Point', coordinates: [lon, lat] }, geometry: { type: 'Point', coordinates: [lon, lat] },
} }
@ -490,14 +489,40 @@ function bindingFeatureCollection() {
} }
} }
function ensureMonitorLayers() {
async function ensurePinImages(map) {
if (!map) return
const jobs = []
for (const kind of PIN_KINDS) {
for (const status of PIN_STATUSES) {
const id = pinImageId(kind, status)
if (map.hasImage(id)) continue
const url = `/assets/icons/${id}.svg`
jobs.push(
new Promise((resolve) => {
map.loadImage(url, (err, image) => {
if (!err && image && !map.hasImage(id)) {
map.addImage(id, image, { pixelRatio: 2 })
}
resolve()
})
}),
)
}
}
await Promise.all(jobs)
}
async function ensureMonitorLayers() {
if (!mapInstance || !mapInstance.isStyleLoaded()) return if (!mapInstance || !mapInstance.isStyleLoaded()) return
await ensurePinImages(mapInstance)
if (!mapInstance.getSource(DEVICE_SOURCE)) { if (!mapInstance.getSource(DEVICE_SOURCE)) {
mapInstance.addSource(DEVICE_SOURCE, { type: 'geojson', data: devicesFeatureCollection() }) mapInstance.addSource(DEVICE_SOURCE, { type: 'geojson', data: devicesFeatureCollection() })
} }
if (!mapInstance.getSource(BINDING_SOURCE)) { if (!mapInstance.getSource(BINDING_SOURCE)) {
mapInstance.addSource(BINDING_SOURCE, { type: 'geojson', data: bindingFeatureCollection() }) mapInstance.addSource(BINDING_SOURCE, { type: 'geojson', data: bindingFeatureCollection() })
} }
if (!mapInstance.getLayer(BINDING_LINE)) { if (!mapInstance.getLayer(BINDING_LINE)) {
mapInstance.addLayer({ mapInstance.addLayer({
id: BINDING_LINE, id: BINDING_LINE,
@ -511,33 +536,51 @@ function ensureMonitorLayers() {
}, },
}) })
} }
if (!mapInstance.getLayer(DEVICE_CIRCLE)) {
if (!mapInstance.getLayer(DEVICE_HALO)) {
mapInstance.addLayer({ mapInstance.addLayer({
id: DEVICE_CIRCLE,
id: DEVICE_HALO,
type: 'circle', type: 'circle',
source: DEVICE_SOURCE, source: DEVICE_SOURCE,
filter: ['==', ['get', 'selected'], 1],
paint: { paint: {
'circle-radius': ['get', 'radius'],
'circle-color': ['get', 'color'],
'circle-stroke-color': '#fff',
'circle-stroke-width': [
'circle-radius': 22,
'circle-color': 'rgba(45,130,218,0.22)',
'circle-stroke-width': 0,
},
})
}
if (!mapInstance.getLayer(DEVICE_SYMBOL)) {
mapInstance.addLayer({
id: DEVICE_SYMBOL,
type: 'symbol',
source: DEVICE_SOURCE,
layout: {
'icon-image': ['get', 'icon'],
'icon-size': [
'case', 'case',
['==', ['get', 'selected'], 1], ['==', ['get', 'selected'], 1],
3,
2,
1.25,
1,
], ],
'icon-anchor': 'bottom',
'icon-allow-overlap': true,
'icon-ignore-placement': true,
}, },
}) })
} }
if (!mapInstance.getLayer(DEVICE_LABEL)) { if (!mapInstance.getLayer(DEVICE_LABEL)) {
mapInstance.addLayer({ mapInstance.addLayer({
id: DEVICE_LABEL, id: DEVICE_LABEL,
type: 'symbol', type: 'symbol',
source: DEVICE_SOURCE, source: DEVICE_SOURCE,
minzoom: 4,
layout: { layout: {
'text-field': ['get', 'name'], 'text-field': ['get', 'name'],
'text-size': 11, 'text-size': 11,
'text-offset': [0, 1.4],
'text-offset': [0, 0.6],
'text-anchor': 'top', 'text-anchor': 'top',
'text-allow-overlap': false, 'text-allow-overlap': false,
}, },
@ -548,12 +591,13 @@ function ensureMonitorLayers() {
}, },
}) })
} }
layersReady = true layersReady = true
} }
function clearMonitorLayers() { function clearMonitorLayers() {
if (!mapInstance) return if (!mapInstance) return
for (const id of [DEVICE_LABEL, DEVICE_CIRCLE, BINDING_LINE]) {
for (const id of [DEVICE_LABEL, DEVICE_SYMBOL, DEVICE_HALO, BINDING_LINE]) {
if (mapInstance.getLayer(id)) mapInstance.removeLayer(id) if (mapInstance.getLayer(id)) mapInstance.removeLayer(id)
} }
for (const id of [DEVICE_SOURCE, BINDING_SOURCE]) { for (const id of [DEVICE_SOURCE, BINDING_SOURCE]) {
@ -623,11 +667,11 @@ function cancelPendingMapHandlers() {
} }
} }
function rebuildAfterStyle() {
async function rebuildAfterStyle() {
pendingStyleHandler = null pendingStyleHandler = null
if (!mapInstance) return if (!mapInstance) return
layersReady = false layersReady = false
ensureMonitorLayers()
await ensureMonitorLayers()
syncMarkers() syncMarkers()
updateMapScale() updateMapScale()
} }
@ -652,33 +696,23 @@ function setMapMode(mode) {
mapInstance.once('style.load', pendingStyleHandler) mapInstance.once('style.load', pendingStyleHandler)
} }
function resetOverviewCamera() {
if (!mapInstance) return
mapInstance.easeTo({
center: MAP_OVERVIEW.center,
zoom: MAP_OVERVIEW.zoom,
pitch: 0,
bearing: 0,
duration: 450,
})
}
function fitAll() { function fitAll() {
if (selectedId.value) { if (selectedId.value) {
clearSelection() clearSelection()
return return
} }
fitAllMarkers()
}
function fitAllMarkers() {
if (!mapInstance) return
const coords = mapMarkers.value
.map((a) => [Number(a.longitude), Number(a.latitude)])
.filter(([lon, lat]) => Number.isFinite(lon) && Number.isFinite(lat))
if (!coords.length) {
mapInstance.jumpTo({ center: mapConfig.center, zoom: mapConfig.zoom })
return
}
if (coords.length === 1) {
mapInstance.easeTo({ center: coords[0], zoom: 16, duration: 450 })
return
}
const bounds = coords.reduce(
(b, c) => b.extend(c),
new mapboxgl.LngLatBounds(coords[0], coords[0]),
)
mapInstance.fitBounds(bounds, { padding: 60, duration: 450 })
resetOverviewCamera()
} }
function flyToSelected() { function flyToSelected() {
@ -690,7 +724,7 @@ function flyToSelected() {
if (!Number.isFinite(lon) || !Number.isFinite(lat)) return if (!Number.isFinite(lon) || !Number.isFinite(lat)) return
mapInstance.easeTo({ mapInstance.easeTo({
center: [lon, lat], center: [lon, lat],
zoom: Math.max(mapInstance.getZoom(), 16),
zoom: Math.max(mapInstance.getZoom(), MAP_VIEW.detailZoom),
duration: 450, duration: 450,
}) })
} }
@ -698,7 +732,7 @@ function flyToSelected() {
function onMapClick(e) { function onMapClick(e) {
if (suppressMapClick) return if (suppressMapClick) return
if (!layersReady) return if (!layersReady) return
const layers = [DEVICE_CIRCLE, DEVICE_LABEL].filter((id) => mapInstance.getLayer(id))
const layers = [DEVICE_SYMBOL, DEVICE_LABEL].filter((id) => mapInstance.getLayer(id))
if (!layers.length) return if (!layers.length) return
const feats = mapInstance.queryRenderedFeatures(e.point, { const feats = mapInstance.queryRenderedFeatures(e.point, {
layers, layers,
@ -713,6 +747,16 @@ function onMapClick(e) {
clearSelection() clearSelection()
} }
function onSymbolEnter() {
if (!mapInstance) return
mapInstance.getCanvas().style.cursor = 'pointer'
}
function onSymbolLeave() {
if (!mapInstance) return
mapInstance.getCanvas().style.cursor = ''
}
async function setupMap() { async function setupMap() {
const map = await commonRefs.getRef('map') const map = await commonRefs.getRef('map')
if (!map) { if (!map) {
@ -722,13 +766,13 @@ async function setupMap() {
mapInstance = map mapInstance = map
mapHelper.toggleMapMode({ is2D: true }) mapHelper.toggleMapMode({ is2D: true })
const onReady = () => {
const onReady = async () => {
pendingLoadHandler = null pendingLoadHandler = null
if (!mapInstance) return if (!mapInstance) return
ensureMonitorLayers()
await ensureMonitorLayers()
syncMarkers() syncMarkers()
if (selectedId.value) flyToSelected() if (selectedId.value) flyToSelected()
else fitAllMarkers()
else resetOverviewCamera()
updateMapScale() updateMapScale()
} }
@ -740,6 +784,8 @@ async function setupMap() {
} }
map.on('click', onMapClick) map.on('click', onMapClick)
map.on('mouseenter', DEVICE_SYMBOL, onSymbolEnter)
map.on('mouseleave', DEVICE_SYMBOL, onSymbolLeave)
map.on('zoom', updateMapScale) map.on('zoom', updateMapScale)
map.on('move', updateMapScale) map.on('move', updateMapScale)
} }
@ -748,6 +794,8 @@ function teardownMap() {
cancelPendingMapHandlers() cancelPendingMapHandlers()
if (mapInstance) { if (mapInstance) {
mapInstance.off('click', onMapClick) mapInstance.off('click', onMapClick)
mapInstance.off('mouseenter', DEVICE_SYMBOL, onSymbolEnter)
mapInstance.off('mouseleave', DEVICE_SYMBOL, onSymbolLeave)
mapInstance.off('zoom', updateMapScale) mapInstance.off('zoom', updateMapScale)
mapInstance.off('move', updateMapScale) mapInstance.off('move', updateMapScale)
clearMonitorLayers() clearMonitorLayers()
@ -783,7 +831,7 @@ watch(mapMarkers, () => {
watch(selectedId, () => { watch(selectedId, () => {
syncMarkers() syncMarkers()
if (selectedId.value) flyToSelected() if (selectedId.value) flyToSelected()
else fitAllMarkers()
else resetOverviewCamera()
}) })
const selectedAsset = computed(() => (selectedId.value ? devices.asset(selectedId.value) : null)) const selectedAsset = computed(() => (selectedId.value ? devices.asset(selectedId.value) : null))
@ -1088,7 +1136,7 @@ async function reload() {
}) })
syncMarkers() syncMarkers()
if (selectedId.value) flyToSelected() if (selectedId.value) flyToSelected()
else fitAllMarkers()
else resetOverviewCamera()
ui.toast('设备状态已刷新') ui.toast('设备状态已刷新')
} catch (e) { } catch (e) {
ui.toast(e.message || '刷新设备状态失败') ui.toast(e.message || '刷新设备状态失败')

Loading…
Cancel
Save