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

Vue3 + OpenLayers 7 实战:手把手教你实现一个带撤销功能的WebGIS测距工具

Vue3 + OpenLayers 7 实战:构建企业级WebGIS测距组件

在数字化地图应用中,测量功能是最基础却最考验工程化能力的模块之一。本文将带您从零构建一个支持撤销重做、多单位切换的测量组件,采用Vue3组合式API与Pinia状态管理,实现比原生OpenLayers更符合现代前端工程规范的解决方案。

1. 工程化环境搭建

首先创建支持TypeScript的Vue3项目环境:

npm create vue@latest webgis-measure --template vue-ts cd webgis-measure npm install ol @types/ol pinia

配置基础地图容器组件MapContainer.vue

<template> <div ref="mapContainer" class="h-full w-full"></div> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue' import Map from 'ol/Map' import View from 'ol/View' import OSM from 'ol/source/OSM' import TileLayer from 'ol/layer/Tile' const mapContainer = ref<HTMLElement>() const map = ref<Map>() onMounted(() => { map.value = new Map({ layers: [new TileLayer({ source: new OSM() })], view: new View({ center: [0, 0], zoom: 2 }), target: mapContainer.value }) }) defineExpose({ map }) </script>

2. 核心测量功能实现

创建useMeasure.ts组合式函数封装测量逻辑:

import { ref } from 'vue' import Draw from 'ol/interaction/Draw' import { LineString } from 'ol/geom' import { getLength } from 'ol/sphere' import type Map from 'ol/Map' type MeasureUnit = 'm' | 'km' | 'ft' export function useMeasure(map: Map) { const active = ref(false) const results = ref<Array<{ distance: number; unit: MeasureUnit }>>([]) const currentUnit = ref<MeasureUnit>('m') const toggleMeasure = () => { active.value = !active.value if (active.value) initDrawInteraction() } const initDrawInteraction = () => { const draw = new Draw({ type: 'LineString', source: new VectorSource(), style: getDrawStyle() }) draw.on('drawend', (e) => { const line = e.feature.getGeometry() as LineString const length = convertLength(getLength(line)) results.value.push({ distance: length, unit: currentUnit.value }) }) map.addInteraction(draw) } const convertLength = (meters: number): number => { const conversions = { m: meters, km: meters / 1000, ft: meters * 3.28084 } return parseFloat(conversions[currentUnit.value].toFixed(2)) } return { active, results, currentUnit, toggleMeasure } }

3. 撤销重做功能设计

采用命令模式实现操作历史管理:

// historyManager.ts interface Command { execute(): void undo(): void } class MeasureCommand implements Command { private snapshot: any private context: any constructor(context: any) { this.context = context this.snapshot = JSON.parse(JSON.stringify(context.results.value)) } execute() { // 执行时不做操作,因为测量结果已自动记录 } undo() { this.context.results.value = this.snapshot } } export class HistoryManager { private undoStack: Command[] = [] private redoStack: Command[] = [] execute(command: Command) { command.execute() this.undoStack.push(command) this.redoStack = [] } undo() { const command = this.undoStack.pop() if (command) { command.undo() this.redoStack.push(command) } } redo() { const command = this.redoStack.pop() if (command) { command.execute() this.undoStack.push(command) } } }

集成到测量组件中:

<script setup lang="ts"> import { HistoryManager, MeasureCommand } from './historyManager' const history = new HistoryManager() const { map } = useMap() const measure = useMeasure(map) const handleMeasureEnd = () => { history.execute(new MeasureCommand(measure)) } </script>

4. 单位切换与显示优化

实现动态单位转换和格式化显示:

const formatDistance = (item: { distance: number; unit: MeasureUnit }) => { const formatter = new Intl.NumberFormat(undefined, { style: 'unit', unit: item.unit === 'ft' ? 'foot' : item.unit }) return formatter.format(item.distance) } const switchUnit = (unit: MeasureUnit) => { currentUnit.value = unit results.value = results.value.map(item => ({ distance: convertLength(item.distance * { m: 1, km: 1000, ft: 0.3048 }[item.unit]), unit })) }

对应的模板部分:

<template> <div class="unit-switcher"> <button v-for="unit in ['m', 'km', 'ft']" @click="switchUnit(unit)" :class="{ active: currentUnit === unit }" > {{ unit }} </button> </div> <ul class="results-list"> <li v-for="(result, index) in results" :key="index"> {{ formatDistance(result) }} <button @click="removeResult(index)">×</button> </li> </ul> </template>

5. 异常处理与用户体验优化

增强测量过程的健壮性:

const initDrawInteraction = () => { try { // 清除现有绘制交互 map.getInteractions() .getArray() .filter(i => i instanceof Draw) .forEach(i => map.removeInteraction(i)) const draw = new Draw({ /* 配置 */ }) draw.on('drawstart', () => { helpTooltip.value?.show() }) draw.on('drawabort', () => { helpTooltip.value?.hide() }) map.on('pointermove', (e) => { if (draw.getActive()) { updateHelpTooltip(e.coordinate) } }) map.addInteraction(draw) } catch (error) { console.error('测量初始化失败:', error) active.value = false } }

添加可视化反馈:

.measure-line { animation: pulse 1.5s infinite; } @keyframes pulse { 0% { stroke-width: 2px; } 50% { stroke-width: 4px; } 100% { stroke-width: 2px; } } .measure-point { animation: bounce 0.5s alternate infinite; } @keyframes bounce { from { transform: scale(1); } to { transform: scale(1.2); } }

6. 性能优化策略

针对大数据量测量的优化方案:

// 使用Web Worker处理复杂计算 const worker = new Worker('./measureWorker.js') worker.onmessage = (e) => { if (e.data.type === 'length') { results.value.push({ distance: convertLength(e.data.value), unit: currentUnit.value }) } } // 在drawend事件中改为: draw.on('drawend', (e) => { const coords = (e.feature.getGeometry() as LineString).getCoordinates() worker.postMessage({ type: 'calculate', coordinates: coords }) })

测量Worker脚本measureWorker.js:

importScripts('https://cdn.jsdelivr.net/npm/ol@7.3.0/dist/ol.js') self.onmessage = (e) => { if (e.data.type === 'calculate') { const line = new ol.geom.LineString(e.data.coordinates) const length = ol.sphere.getLength(line) self.postMessage({ type: 'length', value: length }) } }

7. 组件封装与API设计

最终导出可复用的测量组件:

<template> <div class="measure-control"> <button @click="toggle"> {{ active ? '退出测量' : '开始测量' }} </button> <select v-model="currentUnit"> <option value="m">米</option> <option value="km">千米</option> <option value="ft">英尺</option> </select> <button @click="undo" :disabled="!canUndo">撤销</button> <button @click="redo" :disabled="!canRedo">重做</button> <div v-if="active" class="measure-hint"> 点击开始测量,双击结束 </div> </div> </template> <script setup lang="ts"> defineProps<{ map: Map }>() const emit = defineEmits(['measure-start', 'measure-end']) // ...组合式函数逻辑 </script>

使用示例:

<template> <MapContainer ref="map" /> <MeasureControl :map="map" @measure-end="handleNewMeasurement" /> </template>
http://www.gsyq.cn/news/1497272.html

相关文章:

  • AI驱动的临床评价数据筛选框架:构建可追溯、可验证、合规的数据证据链
  • LPC2930汽车MCU开发实战:ARM9架构、CAN/LIN通信与电机控制详解
  • 智能车竞赛新手必看:用GPS+IMU让越野车模跑起来(从PID调参到实战避坑)
  • 深圳名表回收高奢首选,收的顶精收雅克德罗、伯爵 - 奢侈品回收测评
  • 2026快手视频怎么去掉水印?快手自带去水印功能与合法方法详解 - 科技热点发布
  • 合肥6月黄金回收口碑榜单:多次匿名探店,家门口对标大盘价靠谱门店盘点 - 禹竞
  • 告别卡顿!用STM32的DMA2D图形加速器让你的嵌入式UI丝滑流畅(附RT-Thread实战代码)
  • 云推互动平台怎么样?2026高收录、稳效果优质软文发稿平台 - 品牌速递
  • 别再死记硬背了!用‘数字金字塔’彻底搞懂C语言for循环的嵌套逻辑
  • 别再只跑KE30了!盘点SAP CO-PA那些被低估的报表工具:从KE31到KE3Z
  • 2025主流LLM注意力机制实战指南:从FlashAttention到StreamingLLM
  • AGV/AMR项目现场实施避坑大全:从PLC通讯对接到多车调度,一位老实施工程师的血泪经验分享
  • 多核共享缓存下的实时系统因果链延迟优化
  • 别再裸奔了!手把手教你给MongoDB 5.0/6.0加上账号密码(Windows版保姆级教程)
  • 从新手到老手:TMS320F28335系统时钟配置避坑指南(含PLLCR/DIVSEL寄存器详解)
  • S32K3疑难排查指南:如何利用MC_RGM复位原因记录和PMC状态寄存器快速定位系统死机问题
  • 海外商标哪个平台靠谱?2026跨境卖家买标避坑指南 - 速递信息
  • 用经典uA741运放DIY一个PWM信号发生器(附Multisim仿真文件)
  • 2026南京黄金回收实测盘点!本地6大正规平台实力横向对比 - 薛定谔的梨花猫
  • 忻州市2026年5月最新黄金回收白银回收铂金回收权威排行榜TOP5:纯金+金条+银条+钯金门店地址联系方式推荐 - 马刺总冠军
  • 保姆级教程:用NVIDIA SDK Manager给Jetson Xavier NX刷机,从硬件短接到软件源配置全流程
  • ADNI数据库下载实战:从注册到筛选,避开MRI数据处理的那些坑(含NII格式问题解决)
  • 从手机摄影到安防监控:一文讲透‘景深’背后的物理原理与实战选型指南
  • Sqribble:面向专业文档的可执行模板操作系统
  • 从‘通道’里‘挤’出高分辨率:手把手拆解PyTorch中PixelShuffle的底层逻辑与实现
  • 别再为2D视觉机器人抓不准发愁了!手把手教你用OpenCV搞定‘眼在手上’标定(附完整代码)
  • 告别GIS软件依赖:用Python手撸兰勃特投影正反算(附WGS-84参数)
  • 新手必看:手把手教你配置Python抢单脚本SecKill,避免Chrome版本不匹配的坑
  • Ardupilot避障方案深度对比:北醒TFmini-i-CAN、光流与超声波,谁才是你的菜?
  • 霍夫圆检测调参避坑指南:为什么你的cv2.HoughCircles总检测不到圆或误检太多?