当前位置: 首页 > news >正文

Vue-Leaflet地图组件开发(三)地图控件与高级样式设计

第三篇: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;}}
}/* 修复Leaflet图标路径 */
: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>

相关文章:

  • 广东注册公司代办seo顾问服务深圳
  • 上海搬家公司排名前十名电话seo词库排行
  • 2015网站建设百度云盘官网
  • 如何让百度收录我的网站百度seo快速见效方法
  • mac做网站改html文件知名的网络推广
  • 六安网站制作金狮怎么搜索网站
  • Python学习——排序
  • Java严格模式withResolverStyle解析日期错误及解决方案
  • AI架构师修炼之道
  • 深入解析Java21核心新特性(虚拟线程,分代 ZGC,记录模式模式匹配增强)
  • 指针的使用——字符、字符串、字符串数组(char*)
  • Cesium快速入门到精通系列教程八:时间系统
  • Razor编程RenderXXX相关方法大全
  • ChatterBox - 轻巧快速的语音克隆与文本转语音模型,支持情感控制 支持50系显卡 一键整合包下载
  • Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement
  • 异步跟栈 webpack
  • 【Elasticsearch】映射:fielddata 详解
  • Linux云原生架构:从内核到分布式系统的进化之路
  • 深入解析 Qwen3-Embedding 的模型融合技术:球面线性插值(Slerp)的应用
  • 信息收集:从图像元数据(隐藏信息收集)到用户身份的揭秘 --- 7000
  • 第1课、LangChain 介绍
  • 风控系统中常用的概念和架构学习
  • uni-app学习笔记三十三--触底加载更多和下拉刷新的实现
  • Linux性能调优:从内核到应用的极致优化
  • <3>-MySQL表的操作
  • unity ngui button按钮点击时部分区域响应,部分区域不响应