- Add equipmentApi and computersApi to replace legacy machinesApi - Add controller vendor/model fields to Equipment model and forms - Fix map marker navigation to use plugin-specific IDs (equipmentid, computerid, printerid, networkdeviceid) instead of assetid - Fix search to use unified Asset table with correct plugin IDs - Remove legacy printer search that used non-existent field names - Enable optional JWT auth for detail endpoints (public read access) - Clean up USB plugin models (remove unused checkout model) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
141 lines
3.1 KiB
Vue
141 lines
3.1 KiB
Vue
<template>
|
|
<div class="embedded-map" ref="mapContainer"></div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
|
import L from 'leaflet'
|
|
import 'leaflet/dist/leaflet.css'
|
|
import { currentTheme } from '../stores/theme'
|
|
|
|
const props = defineProps({
|
|
left: { type: Number, default: null },
|
|
top: { type: Number, default: null },
|
|
markerColor: { type: String, default: '#ff0000' },
|
|
markerLabel: { type: String, default: '' }
|
|
})
|
|
|
|
const mapContainer = ref(null)
|
|
let map = null
|
|
let marker = null
|
|
|
|
// Map dimensions
|
|
const MAP_WIDTH = 3300
|
|
const MAP_HEIGHT = 2550
|
|
const bounds = [[0, 0], [MAP_HEIGHT, MAP_WIDTH]]
|
|
|
|
function initMap() {
|
|
if (!mapContainer.value || props.left === null || props.top === null) return
|
|
|
|
map = L.map(mapContainer.value, {
|
|
crs: L.CRS.Simple,
|
|
minZoom: -3,
|
|
maxZoom: 2,
|
|
attributionControl: false,
|
|
zoomControl: true
|
|
})
|
|
|
|
const blueprintUrl = currentTheme.value === 'light'
|
|
? '/static/images/sitemap2025-light.png'
|
|
: '/static/images/sitemap2025-dark.png'
|
|
|
|
L.imageOverlay(blueprintUrl, bounds).addTo(map)
|
|
|
|
// Convert database coordinates to Leaflet (y is inverted)
|
|
const leafletY = MAP_HEIGHT - props.top
|
|
const leafletX = props.left
|
|
|
|
// Create marker
|
|
const icon = L.divIcon({
|
|
html: `<div class="location-marker-dot" style="background: ${props.markerColor};"></div>`,
|
|
iconSize: [20, 20],
|
|
iconAnchor: [10, 10],
|
|
className: 'location-marker'
|
|
})
|
|
|
|
marker = L.marker([leafletY, leafletX], { icon })
|
|
|
|
if (props.markerLabel) {
|
|
marker.bindTooltip(props.markerLabel, {
|
|
permanent: true,
|
|
direction: 'top',
|
|
offset: [0, -10],
|
|
className: 'location-label'
|
|
})
|
|
}
|
|
|
|
marker.addTo(map)
|
|
|
|
// Center on marker with appropriate zoom
|
|
map.setView([leafletY, leafletX], -1)
|
|
map.setMaxBounds(bounds)
|
|
}
|
|
|
|
onMounted(() => {
|
|
initMap()
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (map) {
|
|
map.remove()
|
|
map = null
|
|
}
|
|
})
|
|
|
|
watch([() => props.left, () => props.top], () => {
|
|
if (map) {
|
|
map.remove()
|
|
map = null
|
|
}
|
|
initMap()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.embedded-map {
|
|
width: 100%;
|
|
height: 300px;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
background: var(--bg);
|
|
}
|
|
|
|
:deep(.location-marker) {
|
|
background: transparent !important;
|
|
border: none !important;
|
|
}
|
|
|
|
:deep(.location-marker-dot) {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
border: 3px solid #fff;
|
|
box-shadow: 0 0 0 2px rgba(0,0,0,0.3), 0 2px 8px rgba(0,0,0,0.4);
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% {
|
|
box-shadow: 0 0 0 2px rgba(0,0,0,0.3), 0 2px 8px rgba(0,0,0,0.4);
|
|
}
|
|
50% {
|
|
box-shadow: 0 0 0 6px rgba(255,0,0,0.2), 0 2px 8px rgba(0,0,0,0.4);
|
|
}
|
|
}
|
|
|
|
:deep(.location-label) {
|
|
background: rgba(0, 0, 0, 0.85);
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
:deep(.location-label::before) {
|
|
border-top-color: rgba(0, 0, 0, 0.85);
|
|
}
|
|
</style>
|