第三篇:Vue-Leaflet地图控件与高级样式设计
1. 专业级比例尺组件实现
1.1 比例尺控件集成
import { LControl } from "@vue-leaflet/vue-leaflet" ;
< l- control- scale
position= "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>