192.Vue3 + OpenLayers 实战:点击地图 Feature,列表自动滚动定位

一、最终效果

🎯 实现功能

  • 点击地图上的多边形(Feature)

  • 列表自动滚动到对应位置

  • 当前项高亮显示

  • 鼠标悬浮 Feature 变为 pointer

👉 核心体验:地图 → 列表联动


二、前言

在 WebGIS 项目中,经常会遇到这样一个需求:

👉 点击地图上的某个要素(Feature)

👉 右侧列表自动滚动并定位到对应数据项

这个交互在以下场景非常常见:

  • 无人机巡检系统 ✈️

  • 电子围栏管理 🗺️

  • 设备点位管理系统 📍

  • GIS 可视化大屏

本篇文章带你用 Vue3 + OpenLayers 实现一个完整、丝滑的联动效果。


三、核心实现思路

整个功能其实就三步:

1️⃣ Feature 和列表建立索引关系

复制代码
new Feature({
  geometry: item.area,
  listindex: i,
  name: item.descName,
});

👉 关键点:
给每个 Feature 绑定 listindex


2️⃣ 点击地图获取 Feature

复制代码
const feature = map.value.forEachFeatureAtPixel(
  e.pixel,
  (feature) => feature
);

👉 OpenLayers 核心 API


3️⃣ 滚动到对应 DOM

复制代码
const el = document.getElementById("pos" + i);
el && el.scrollIntoView({ behavior: "smooth", block: "center" });

🔥 重点:

window.location.hash 更优雅!


四、完整代码实现

javascript 复制代码
<!--
 * @Author: 彭麒
 * @Date: 2026/3/19
 * @Email: 1062470959@qq.com
 * @Description: 此源码版权归吉檀迦俐所有,可供学习和借鉴或商用。
 -->
<template>
  <div class="container">
    <div class="w-full flex justify-center flex-wrap">
      <div class="font-bold text-[24px]">
        Vue3+Openlayers:点击某feature,列表滑动,定位到相应的点的列表位置
      </div>
    </div>
    <div class="list">
      <div
          v-for="(item, index) in list"
          :key="index"
          :id="'pos' + index"
          style="margin: 10px 10px 0;"
      >
        <el-link :type="item.show ? 'primary' : 'info'">
          {{ item.descName }}
        </el-link>
      </div>
    </div>

    <div id="vue-openlayers"></div>
  </div>
</template>

<script setup>
import { ref, onMounted } from "vue";

import "ol/ol.css";
import { Map, View } from "ol";
import TileLayer from "ol/layer/Tile";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import XYZ from "ol/source/XYZ";
import Feature from "ol/Feature";
import { Style, Fill, Stroke } from "ol/style";
import { Select } from "ol/interaction";
import GeoJSON from "ol/format/GeoJSON";
import fData from "@/assets/map/china.json";
const map = ref(null);
const source = new VectorSource({ wrapX: false });
const select = ref(null);
const list = ref([]);
const drawfeatures = ref([]);

const initload = () => {
  drawfeatures.value = new GeoJSON().readFeatures(fData, {
    dataProjection: "EPSG:4326",
    featureProjection: "EPSG:4326",
  });

  updateList();
  showPolygons();
};

// 更新列表
const updateList = () => {
  list.value = [];

  drawfeatures.value.forEach((feature, index) => {
    const g = feature.getGeometry();

    list.value.push({
      descName: "围栏 " + index,
      show: true,
      area: g,
    });
  });
};

// 显示多边形
const showPolygons = () => {
  const features = list.value.map((item, i) => {
    return new Feature({
      geometry: item.area,
      listindex: i,
      name: item.descName,
    });
  });

  source.addFeatures(features);
};

// 点击 feature → 滚动列表
const clickFeature = () => {
  map.value.on("click", (e) => {
    const feature = map.value.forEachFeatureAtPixel(
        e.pixel,
        (feature) => feature
    );

    // 鼠标样式
    map.value.getTargetElement().style.cursor = feature
        ? "pointer"
        : "auto";

    if (feature) {
      const i = feature.get("listindex");

      // 滚动定位(推荐替代 hash)
      const el = document.getElementById("pos" + i);
      el && el.scrollIntoView({ behavior: "smooth", block: "center" });

      list.value.forEach((item, index) => {
        item.show = index !== i;
      });
    } else {
      list.value.forEach((item) => (item.show = true));
    }
  });
};

// 初始化地图
const initMap = () => {
  const iconStyle = new Style({
    stroke: new Stroke({
      color: "red",
      width: 2,
    }),
    fill: new Fill({
      color: "rgba(255,0,0,0)",
    }),
  });

  const googleLayer = new TileLayer({
    source: new XYZ({
      url: "https://www.google.com/maps/vt?lyrs=m&gl=en&x={x}&y={y}&z={z}",
      crossOrigin: "anonymous",
    }),
  });

  const drawLayer = new VectorLayer({
    source,
    style: iconStyle,
  });

  map.value = new Map({
    target: "vue-openlayers",
    layers: [googleLayer, drawLayer],
    view: new View({
      projection: "EPSG:4326",
      center: [110.4116821, 41.7966156],
      zoom: 3,
    }),
  });

  clickFeature();

  select.value = new Select();
  map.value.addInteraction(select.value);
};

onMounted(() => {
  initMap();
  initload();
});
</script>

<style scoped>
.container {
  width: 840px;
  height: 570px;
  margin: 50px auto;
  border: 1px solid #42B983;
}

#vue-openlayers {
  width: 620px;
  height: 460px;
  margin: 0 auto;
  border: 1px solid #42B983;
  float: left;
}

.list {
  width: 200px;
  height: 180px;
  margin: 0 auto;
  float: left;
  overflow-y: auto;
}
</style>

五、关键优化点(面试加分项🔥)

✅ 1. scrollIntoView 优于 hash

❌ 不推荐:

复制代码
window.location.hash = "#pos" + i;

✅ 推荐:

复制代码
el.scrollIntoView({ behavior: "smooth" });

✅ 2. 数据驱动 UI

复制代码
item.show = index !== i;

👉 Vue 响应式自动更新 UI


✅ 3. Feature 绑定业务数据

复制代码
feature.get("listindex")

👉 地图 → 业务数据的桥梁


✅ 4. 解耦设计(高级)

可以进一步抽成:

  • useMap()

  • useFeatureClick()

  • useListSync()

👉 适合大型项目


六、可扩展功能(建议你写进简历)

🔥 你可以继续升级:

  • 点击列表 → 地图定位(反向联动)

  • Feature 高亮选中(改变样式)

  • hover 提示

  • 海量数据优化(WebGL / 分层加载)

  • 地图聚合 / 分组展示


七、总结

本文实现了一个非常实用的 GIS 交互功能:

✅ 地图点击

✅ 列表联动

✅ 平滑滚动定位

✅ Vue3 响应式驱动

👉 核心一句话总结:

"通过 Feature 绑定索引,实现地图与列表的双向映射"

相关推荐
还得是你大哥16 小时前
Java互联网医院管理系统源码SpringBoot
java·spring boot·vue
会周易的程序员1 天前
aiDgeScanner:工业设备扫描与管理的一体化利器——深度解析上位机与扫描端的无缝协作
c++·物联网·typescript·electron·vue·iot·aiot
阿部多瑞 ABU1 天前
运动会智能编排系统 - 完整详细需求规格说明书
python·贪心算法·vue·html
AIGC包拥它2 天前
RAG 项目实战进阶:基于 FastAPI + Vue3 前后端架构全面重构 LangChain 0.3 集成 Milvus 2.5 构建大模型智能应用
人工智能·python·重构·vue·fastapi·milvus·ai-native
:mnong2 天前
PlayCanvas 开源 WebGL/WebGPU 3D 创作平台分析
3d·开源·webgl
次次皮4 天前
代理启动前端dist包
java·前端·vue
展示猪肝4 天前
Vue2 + FastAPI + Dify 实现 AI 医疗预检分诊助手:从问诊追问到医生审核闭环
人工智能·vue·fastapi·dify
何忆清风4 天前
Easy Agent Pilot - Rust实现的开源桌面Agent软件
ai·rust·vue·agent·tauri·开发工具
码界筑梦坊4 天前
361-基于Python的空气质量气候数据分析预测系统
python·信息可视化·数据分析·flask·vue·毕业设计
我叫张小白。4 天前
劳动力招聘管理系统:全栈实战(Vue3+FastAPI+WebSocket+Dify)
websocket·vue·毕业设计·状态模式·fastapi·dify·智能体