当前位置: 首页 > 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>
http://www.xdnf.cn/news/934327.html

相关文章:

  • Vue中虚拟DOM的原理与作用
  • DAY 25 异常处理
  • ChatterBox - 轻巧快速的语音克隆与文本转语音模型,支持情感控制 支持50系显卡 一键整合包下载
  • BeanFactory 和 FactoryBean 有何区别与联系?
  • 面试实例题
  • Go 语言中switch case条件分支语句
  • 人生中第一次开源:java版本的supervisor,支持web上管理进程,查看日志
  • 【大模型】【推荐系统】LLM在推荐系统中的应用价值
  • 【论文阅读】YOLOv8在单目下视多车目标检测中的应用
  • Pydantic + Function Calling的结合
  • 从碳基羊驼到硅基LLaMA:开源大模型家族的生物隐喻与技术进化全景
  • wpf在image控件上快速显示内存图像
  • 机器学习方法实现数独矩阵识别器
  • (六)卷积神经网络:深度学习在计算机视觉中的应用
  • 深入​剖析网络IO复用
  • java中static学习笔记
  • Amazon RDS on AWS Outposts:解锁本地化云数据库的混合云新体验
  • (AI) Ollama 部署本地 DeepSeek 大模型
  • 在MobaXterm 打开图形工具firefox
  • JVM 类加载器 详解
  • 深入解析Java21核心新特性(虚拟线程,分代 ZGC,记录模式模式匹配增强)
  • 如何思考?思维篇
  • MyBatis原理剖析(二)
  • 编程实验篇--线性探测哈希表
  • Vue 学习路线图(从零到实战)
  • DROPP算法详解:专为时间序列和空间数据优化的PCA降维方案
  • Docker部署SpringBoot项目
  • window下配置ssh免密登录服务器
  • 深入解析机器学习的心脏:损失函数及其背后的奥秘
  • Ubuntu 上安装 Git LFS