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

news2025/6/9 18:11:12

第三篇: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;
    }
  }
}

/* 修复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.coloradmin.cn/o/2405676.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

174页PPT家居制造业集团战略规划和运营管控规划方案

甲方集团需要制定一个清晰的集团价值定位&#xff0c;从“指引多元”、“塑造 能力”以及“强化协同”等方面引领甲方做大做强 集团需要通过管控模式、组织架构及职能、授权界面、关键流程、战略 实施和组织演进路径&#xff0c;平衡风险控制和迅速发展&#xff0c;保证战略落地…

wsl开启即闪退

[ 问题 ]&#xff1a; 在一次电脑卡住&#xff0c;强制关机重启后&#xff0c;遇到打开WSL就闪退的问题在CMD中打开WSL&#xff0c;出现如上图的描述&#xff1a; C:\Users\admin>wsl wsl: 检测到 localhost 代理配置&#xff0c;但未镜像到 WSL。NAT 模式下的 WSL 不支持…

【P2P】直播网络拓扑及编码模式

以下从 P2P 直播的常见拓扑模式出发,分析各种方案的特点与适用场景,并给出推荐。 一、P2P 直播的核心挑战 实时性要求高 直播场景下,延迟必须控制在可接受范围(通常 <2 秒),同时要保证画面连贯、不卡顿。带宽分布不均 每个节点(观众)上传带宽与下载带宽差异较大,且…

Python数据可视化科技图表绘制系列教程(二)

目录 表格风格图 使用Seaborn函数绘图 设置图表风格 设置颜色主题 图表分面 绘图过程 使用绘图函数绘图 定义主题 分面1 分面2 【声明】&#xff1a;未经版权人书面许可&#xff0c;任何单位或个人不得以任何形式复制、发行、出租、改编、汇编、传播、展示或利用本博…

低空城市场景下的多无人机任务规划与动态协调!CoordField:无人机任务分配的智能协调场

作者&#xff1a;Tengchao Zhang 1 ^{1} 1 , Yonglin Tian 2 ^{2} 2 , Fei Lin 1 ^{1} 1, Jun Huang 1 ^{1} 1, Patrik P. Sli 3 ^{3} 3, Rui Qin 2 , 4 ^{2,4} 2,4, and Fei-Yue Wang 5 , 1 ^{5,1} 5,1单位&#xff1a; 1 ^{1} 1澳门科技大学创新工程学院工程科学系&#xff0…

算法-构造题

#include<iostream> #include<bits/stdc.h> using namespace std; typedef long long ll; const ll N 5e5 10; int main() {ll n, k;cin >> n >> k; ll a[N] {0}; // 初始化一个大小为N的数组a&#xff0c;用于存储排列// 构造满足条件的排列for (l…

【Linux】进程的基本概念

目录 概念描述进程-PCB如何查看进程通过系统目录进行查看通过ps指令进行查看 通过系统调用获取进程的PID和PPID(进程标⽰符)通过系统调用创建子进程通过一段代码来介绍fork为什么要有子进程&#xff1f;fork为什么给子进程返回0&#xff0c;给父进程返回子进程的PIDfork函数到底…

设备驱动与文件系统:05 文件使用磁盘的实现

从文件使用磁盘的实现逻辑分享 我们现在讲第30讲&#xff0c;内容是文件使用磁盘的具体实现&#xff0c;也就是相关代码是如何编写的。上一节我们探讨了如何从字符流位置算出盘块号&#xff0c;这是文件操作磁盘的核心。而这节课&#xff0c;我们将深入研究实现这一核心功能的…

AI数据分析在体育中的应用:技术与实践

在现代体育竞技领域&#xff0c;"数据驱动"已不再是一个遥远的概念。尤其随着人工智能&#xff08;AI&#xff09;和大数据分析的不断成熟&#xff0c;从职业俱乐部到赛事直播平台&#xff0c;从运动员训练到球迷观赛体验&#xff0c;AI正以前所未有的方式渗透并改变…

zabbix 6 监控 docker 容器

zabbix 6 监控 docker 容器 1.安装zabbix_agent2 curl -s http://10.26.211.56:8080/centos7-agent2-install.sh | bash2.在zabbix server 端测试 zabbix_get -s 10.26.219.180 -k docker.infoZBX_NOTSUPPORTED: Cannot fetch data: Get "http://1.28/info": dial…

正则持续学习呀

源匹配为 (.*): (.*)$ 替换匹配为 "$1": "$2", 可将headers改为字典 参考 【爬虫军火库】如何优雅地复制请求头 - 知乎

Go基本语法——go语言中的四种变量定义方法

前言 在go语言中&#xff0c;定义一个变量有四种方式&#xff0c;本文单从语法的层面来介绍这几种方式 单变量定义方法 1.var 变量名 类型&#xff0c;不进行初始化 例如&#xff0c;定义一个变量a后为其赋值&#xff0c;并且打印其值&#xff0c;运行结果如下 //1.不进行…

27.【新型数据架构】-数据共享架构

27.【新型数据架构】-数据共享架构:降低数据获取成本,实时数据访问,保持数据新鲜度,促进数据经济发展,打破数据孤岛,标准化数据交换,增强数据安全性,完整审计追踪,合规性保障 一、数据共享架构的本质:打破壁垒的“数字立交桥” 传统企业或组织间的数据往往呈现“烟囱…

virtualbox 如何虚拟机ip固定

1、在网络管理里新建 2、配置网络 3、 进入linux系统&#xff0c;查看 查看 网卡是enp0s8, ifconfig 4、进入网卡配置文件 cd /etc/sysconfig/network-scripts如果没有enp0s8 &#xff0c;则使用mv ifcfg-enp0s3 ifcfg-enp0s8命令 配置项如下 TYPEEthernet PROXY_METHODn…

RKNN3588上部署 RTDETRV2

RT-DETR V2 是由百度研究团队在 2024年 提出的&#xff0c;是其广受好评的实时目标检测模型 RT-DETR 的重大升级版本。它继承了第一代 RT-DETR 利用 Transformer 架构实现端到端目标检测 和 卓越实时性能 的核心优势&#xff0c;并针对模型精度、训练效率和部署灵活性进行了全方…

Python----循环神经网络(BiLSTM:双向长短时记忆网络)

一、LSTM 与 BiLSTM对比 1.1、LSTM LSTM&#xff08;长短期记忆网络&#xff09; 是一种改进的循环神经网络&#xff08;RNN&#xff09;&#xff0c;专门解决传统RNN难以学习长期依赖的问题。它通过遗忘门、输入门和输出门来控制信息的流动&#xff0c;保留重要信息并丢弃无关…

Linux系统编程-DAY10(TCP操作)

一、网络模型 1、服务器/客户端模型 &#xff08;1&#xff09;C/S&#xff1a;client server &#xff08;2&#xff09;B/S&#xff1a;browser server &#xff08;3&#xff09;P2P&#xff1a;peer to peer 2、C/S与B/S区别 &#xff08;1&#xff09;客户端不同&#…

基于eclipse进行Birt报表开发

Birt报表开发最终实现效果&#xff1a; 简洁版的Birt报表开发实现效果&#xff0c;仅供参考&#xff01; 可动态获取采购单ID&#xff0c;来打印出报表&#xff01; 下面开始Birt报表开发教程&#xff1a; 首先&#xff1a;汉化的eclipse及Birt值得拥有&#xff1a;至少感觉上…

GPU虚拟化

引言 现有如下环境&#xff08;注意相关配置&#xff1a;只有一个k8s节点&#xff0c;且该节点上只有一张GPU卡&#xff09;&#xff1a; // k8s版本 $ kubectl version Client Version: version.Info{Major:"1", Minor:"22", GitVersion:"v1.22.7&…

LabVIEW工业级多任务实时测控系统

采用LabVIEW构建了一套适用于工业自动化领域的多任务实时测控系统。系统采用分布式架构&#xff0c;集成高精度数据采集、实时控制、网络通信及远程监控等功能&#xff0c;通过硬件与软件的深度协同&#xff0c;实现对工业现场多类型信号的精准测控&#xff0c;展现 LabVIEW 在…