第三篇:Vue-Leaflet地图控件与高级样式设计

1. 专业级比例尺组件实现
1.1 比例尺控件集成
import { LControl } from "@vue-leaflet/vue-leaflet";
<l-control-scaleposition="bottomleft":imperial="false":metric="true":maxWidth="200":updateWhenIdle="true"
/>
:deep(.leaflet-control-scale) {background-color: rgba(255, 255, 255, 0.8);padding: 5px 10px;border-radius: 4px;box-shadow: 0 1px 5px rgba(0,0,0,0.2);border: 1px solid #ddd;.leaflet-control-scale-line {border: 2px solid #333;border-top: none;color: #333;font-size: 12px;text-align: center;margin: 2px 0;}
}
1.2 动态比例尺组件
<template><l-control position="bottomleft" class="custom-scale-control"><div class="scale-container"><div class="scale-line" :style="scaleStyle"></div><div class="scale-text">{{ scaleText }}</div></div></l-control>
</template><script setup>
import { ref, onMounted, watch } from 'vue';
import { useMap } from '@vue-leaflet/vue-leaflet';const map = useMap();
const scaleText = ref('0 m');
const scaleStyle = ref({ width: '100px' });const updateScale = () => {const zoom = map.value?.getZoom();if (!zoom) return;// 根据缩放级别计算比例尺const metersPerPixel = 156543.03392 * Math.cos(0) / Math.pow(2, zoom);const scaleWidthMeters = 100 * metersPerPixel;// 自动选择合适单位if (scaleWidthMeters >= 1000) {scaleText.value = `${(scaleWidthMeters / 1000).toFixed(1)} km`;} else {scaleText.value = `${Math.round(scaleWidthMeters)} m`;}scaleStyle.value = { width: '100px' };
};// 监听地图缩放事件
onMounted(() => {map.value?.on('zoomend', updateScale);updateScale();
});
</script><style scoped>
.custom-scale-control {background: rgba(255, 255, 255, 0.8);padding: 6px;border-radius: 4px;box-shadow: 0 1px 5px rgba(0,0,0,0.2);border: 1px solid #ddd;
}.scale-container {display: flex;flex-direction: column;align-items: center;
}.scale-line {height: 4px;background: #333;margin-bottom: 2px;
}.scale-text {font-size: 12px;color: #333;font-weight: bold;
}
</style>
2. 增强版图例系统

2.1 动态图例组件
<template><div class="legend-control" :class="{ collapsed: isCollapsed }"><div class="legend-header" @click="toggleCollapse"><span>图层图例</span><el-icon :class="collapseIcon"></el-icon></div><div class="legend-content" v-show="!isCollapsed"><div v-for="layer in activeLayers" :key="layer.id" class="legend-item"><div class="layer-title">{{ layer.name }}</div><div v-if="layer.type === 'categorical'"><div v-for="item in layer.legend" :key="item.label" class="legend-category"><div class="legend-symbol" :style="getSymbolStyle(item)"></div><div class="legend-label">{{ item.label }}</div></div></div><div v-else-if="layer.type === 'gradient'"><div class="gradient-bar" :style="getGradientStyle(layer)"></div><div class="gradient-labels"><span>{{ layer.minValue }}</span><span>{{ layer.maxValue }}</span></div></div></div></div></div>
</template><script setup>
import { ref, computed } from 'vue';
import { useLayerStore } from '@/stores/layerStore';const layerStore = useLayerStore();
const isCollapsed = ref(false);const activeLayers = computed(() => {return layerStore.layers.filter(layer => layer.visible).map(layer => ({id: layer.id,name: layer.name,type: layer.legendType,legend: layer.legend,minValue: layer.minValue,maxValue: layer.maxValue}));
});const collapseIcon = computed(() => isCollapsed.value ? 'el-icon-arrow-down' : 'el-icon-arrow-up'
);const toggleCollapse = () => {isCollapsed.value = !isCollapsed.value;
};const getSymbolStyle = (item) => {return {backgroundColor: item.color,border: item.border ? `1px solid ${item.borderColor || '#333'}` : 'none',borderRadius: item.shape === 'circle' ? '50%' : '0'};
};const getGradientStyle = (layer) => {return {background: `linear-gradient(to right, ${layer.colors.join(',')})`,height: '20px'};
};
</script><style scoped>
.legend-control {position: absolute;bottom: 20px;right: 20px;background: white;border-radius: 4px;box-shadow: 0 2px 10px rgba(0,0,0,0.2);z-index: 1000;transition: all 0.3s ease;max-width: 250px;
}.legend-control.collapsed {width: 120px;
}.legend-header {padding: 10px 15px;background: #f5f5f5;cursor: pointer;display: flex;justify-content: space-between;align-items: center;border-radius: 4px 4px 0 0;font-weight: bold;
}.legend-content {padding: 10px;max-height: 60vh;overflow-y: auto;
}.legend-item {margin-bottom: 15px;
}.layer-title {font-weight: bold;margin-bottom: 8px;padding-bottom: 4px;border-bottom: 1px solid #eee;
}.legend-category {display: flex;align-items: center;margin: 5px 0;
}.legend-symbol {width: 20px;height: 20px;margin-right: 8px;
}.gradient-bar {width: 100%;margin: 10px 0;
}.gradient-labels {display: flex;justify-content: space-between;font-size: 12px;color: #666;
}
</style>
3. 专业地图控件设计
3.1 增强版缩放控件
<template><l-control position="topleft" class="custom-zoom-control"><div class="zoom-btn" @click="zoomIn"><el-icon :size="18"><Plus /></el-icon></div><div class="zoom-display">{{ currentZoom }}</div><div class="zoom-btn" @click="zoomOut"><el-icon :size="18"><Minus /></el-icon></div><div class="zoom-home" @click="resetView"><el-icon :size="18"><House /></el-icon></div></l-control>
</template><script setup>
import { ref, watch } from 'vue';
import { useMap } from '@vue-leaflet/vue-leaflet';
import { House, Plus, Minus } from '@element-plus/icons-vue';const map = useMap();
const currentZoom = ref(0);watch(() => map.value?.getZoom(), (zoom) => {currentZoom.value = zoom;
});const zoomIn = () => {map.value?.zoomIn();
};const zoomOut = () => {map.value?.zoomOut();
};const resetView = () => {map.value?.flyTo(center.value, defaultZoom.value);
};
</script><style scoped>
.custom-zoom-control {background: white;border-radius: 4px;box-shadow: 0 1px 5px rgba(0,0,0,0.2);padding: 5px;
}.zoom-btn, .zoom-home {width: 30px;height: 30px;display: flex;align-items: center;justify-content: center;cursor: pointer;background: #f8f8f8;margin: 2px 0;border-radius: 3px;transition: all 0.2s;
}.zoom-btn:hover, .zoom-home:hover {background: #e6f7ff;
}.zoom-display {text-align: center;font-size: 12px;font-weight: bold;padding: 5px 0;
}.zoom-home {margin-top: 5px;border-top: 1px solid #eee;
}
</style>
3.2 地图图层切换控件
<template><l-control position="topright" class="layer-switcher"><el-dropdown trigger="click" @command="handleLayerChange"><div class="layer-switcher-btn"><el-icon :size="20"><Map /></el-icon><span class="current-layer">{{ currentLayerName }}</span></div><template #dropdown><el-dropdown-menu><el-dropdown-item v-for="layer in baseLayers" :key="layer.name":command="layer.name":class="{ active: currentLayerName === layer.name }">{{ layer.name }}</el-dropdown-item></el-dropdown-menu></template></el-dropdown></l-control>
</template><script setup>
import { ref, onMounted } from 'vue';
import { useMap } from '@vue-leaflet/vue-leaflet';
import { Map } from '@element-plus/icons-vue';const map = useMap();
const currentLayerName = ref('电子地图');const baseLayers = [{ name: '电子地图', layer: 'vector' },{ name: '卫星影像', layer: 'satellite' },{ name: '地形图', layer: 'terrain' }
];const handleLayerChange = (layerName) => {currentLayerName.value = layerName;// 这里实现实际图层切换逻辑emit('change-base-layer', layerName);
};
</script><style scoped>
.layer-switcher {background: white;border-radius: 4px;box-shadow: 0 1px 5px rgba(0,0,0,0.2);padding: 5px 10px;
}.layer-switcher-btn {display: flex;align-items: center;cursor: pointer;
}.current-layer {margin-left: 5px;font-size: 14px;
}.active {color: var(--el-color-primary);font-weight: bold;
}
</style>
4. 高级地图样式技巧
4.1 响应式地图容器
.map-container {position: relative;height: 100%;width: 100%;@media (max-width: 768px) {:deep(.leaflet-control) {transform: scale(0.8);transform-origin: 0 0;}.legend-control {max-width: 200px;bottom: 10px;right: 10px;}}
}
:deep(.leaflet-default-icon-path) {background-image: url(https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png);
}
:deep(.leaflet-popup-content-wrapper) {border-radius: 6px;box-shadow: 0 3px 14px rgba(0,0,0,0.2);
}:deep(.leaflet-popup-content) {margin: 12px;min-width: 200px;
}
:deep(.leaflet-tooltip) {background: rgba(255, 255, 255, 0.9);border: 1px solid #ddd;border-radius: 3px;box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
4.2 夜间模式支持
const isDarkMode = ref(false);const toggleDarkMode = () => {isDarkMode.value = !isDarkMode.value;if (isDarkMode.value) {baseLayer.value = darkBaseLayer;document.querySelector('.map-container').classList.add('dark-mode');} else {baseLayer.value = normalBaseLayer;document.querySelector('.map-container').classList.remove('dark-mode');}
};
.dark-mode {:deep(.leaflet-tile) {filter: brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7);}:deep(.leaflet-control) {background-color: #2d3748;color: #e2e8f0;}:deep(.leaflet-bar) {background-color: #2d3748;a {background-color: #2d3748;color: #e2e8f0;border-bottom-color: #4a5568;&:hover {background-color: #4a5568;}}}.legend-control {background-color: #2d3748;color: #e2e8f0;.legend-header {background-color: #1a202c;}.layer-title {border-bottom-color: #4a5568;}}
}
5. 完整控件集成示例
<template><div class="styled-map-container"><!-- 主地图 --><l-map ref="map" :options="mapOptions" @ready="initMap"><!-- 底图 --><l-tile-layer :url="baseLayerUrl" /><!-- 自定义控件 --><custom-zoom-control /><layer-switcher /><advanced-scale-control /><dynamic-legend /><!-- 比例尺 --><l-control-scale position="bottomleft" /><!-- 其他图层 --></l-map><!-- 地图控制面板 --><div class="map-toolbar"><el-button-group><el-button @click="toggleDarkMode"><el-icon><Moon /></el-icon>{{ isDarkMode ? '日间模式' : '夜间模式' }}</el-button><el-button @click="toggleLegend"><el-icon><Picture /></el-icon>图例</el-button></el-button-group></div></div>
</template><script setup>
// 这里集成前面介绍的所有控件和样式
</script><style scoped>
.styled-map-container {position: relative;height: 100%;width: 100%;
}.map-toolbar {position: absolute;top: 80px;right: 20px;z-index: 1000;background: white;padding: 8px;border-radius: 4px;box-shadow: 0 2px 12px rgba(0,0,0,0.1);.dark-mode & {background: #2d3748;color: white;}
}
</style>