尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Vue 与 Leaflet marker 事件绑定实战:解决点击失效、this丢失、内存泄漏
1. 项目概述为什么在 Vue 里用 Leaflet 打点还要专门讲“事件”这一环Leaflet 是前端地图开发里最轻量、最灵活的开源库之一它不依赖框架、API 清晰、插件生态成熟特别适合嵌入到 Vue 这类响应式框架中做定制化地图交互。但很多开发者卡在“能显示 marker却不知道怎么让 marker 真正‘活’起来”——点击没反应、弹窗内容错乱、坐标偏移、事件重复绑定、Vue 响应式失效……这些不是 Leaflet 本身的问题而是 Vue 的生命周期、响应式机制与原生 DOM 事件模型之间存在天然摩擦点。我带过好几个前端团队落地地图功能发现超过 70% 的线上问题都出在“事件绑定方式”上有人直接在mounted里用marker.on(click, ...)结果组件销毁后事件没解绑内存泄漏有人把this传进回调却发现 Vue 3 的setup()里this不存在报undefined还有人用v-on:click绑在div idmap上妄图捕获 marker 点击——这根本行不通因为 marker 是 Canvas/SVG 元素不是 Vue 模板里的普通 DOM 节点。所以这篇不是泛泛而谈“怎么加 marker”而是聚焦一个具体、高频、易踩坑的子场景在 Vue兼容 2.x 和 3.x中为 Leaflet marker 正确绑定、触发、管理 click/mouseover/dblclick 等原生事件并确保与 Vue 数据流无缝联动。核心关键词就是Vue 生命周期适配、事件解绑时机、this 上下文保持、响应式数据同步、事件委托替代方案、性能边界控制。适合正在做物流轨迹展示、门店选址系统、设备巡检平台、校园导览应用的前端同学尤其适合刚从纯 JS 地图过渡到 Vue 工程化开发的中级开发者——你不需要懂 GIS 原理但得清楚 Vue 的ref怎么和 Leaflet 的L.Marker实例桥接。我试过至少 5 种写法从最原始的onMounted L.marker().addTo(map).on(click)到封装useLeafletMarker自定义 Hook再到基于v-model的双向绑定模拟最后沉淀出一套兼顾可读性、可维护性和运行时稳定性的方案。下面所有内容都是我在某高校地理信息实验室做的跨平台教学系统、某连锁零售企业的门店热力图后台、以及某工业设备远程监控平台中反复验证过的实操路径不是理论推演是真正在生产环境跑过半年以上的经验。2. 整体设计思路为什么不能直接用clickVue 和 Leaflet 的事件模型到底差在哪2.1 根本矛盾Vue 模板事件 vs Leaflet 原生 DOM 事件Vue 的click本质是监听 DOM 元素上的原生click事件但它只对模板中声明的元素生效。Leaflet 的 marker 不是通过marker :latx :lngy/这种 Vue 组件语法创建的而是调用L.marker([lat, lng]).addTo(map)后由 Leaflet 库内部动态生成 SVGcircle或 Canvas 绘制的图形节点并插入到地图容器的 DOM 树中。这个节点完全游离于 Vue 的虚拟 DOM 之外Vue 的编译器根本“看不见”它自然无法为其绑定指令式事件。提示你可以打开浏览器开发者工具在 Elements 面板里搜索circle classleaflet-marker-icon会发现它位于div idmap内部但不在 Vue 组件的 template 结构里。这就是为什么click.native在 Vue 2 中也无效——.native只作用于组件根元素而 marker 不是组件根元素。2.2 Vue 生命周期带来的三重陷阱Leaflet 地图初始化必须等 DOM 挂载完成但 marker 的事件绑定又必须等地图实例 ready。这就形成一个典型的“双重等待”第一重等待Vue 的mounted或onMounted确保#map容器已存在第二重等待Leaflet 的map.whenReady(() {})或监听load事件确保瓦片图层、坐标系、投影已就绪第三重等待marker 创建完成并addTo(map)后其 DOM 节点才真实插入文档流此时才能安全绑定事件。很多初学者只做了第一重就急着L.marker(...).on(click, handler)结果 handler 里this指向错误、map实例未初始化、甚至 marker 尚未渲染导致getLatLng()返回null。我在某设备监控项目里就遇到过页面加载快地图瓦片还没加载完marker 就被addTo了点击时event.latlng是undefined整个告警弹窗逻辑崩掉。2.3 响应式断连为什么this.xxx new value在事件回调里不更新视图这是 Vue 2 和 Vue 3 都存在的经典误区。看这段典型错误代码// Vue 2 Options API 错误示范 export default { data() { return { selectedMarkerId: null }; }, mounted() { this.map L.map(map).setView([39.9, 116.4], 13); const marker L.marker([39.9, 116.4]).addTo(this.map); marker.on(click, function() { // ❌ this 指向的是 marker 实例不是 Vue 组件 this.selectedMarkerId abc; // 这行根本不会触发视图更新 }); } }原因很简单function() {}是普通函数this绑定的是调用者即 marker不是 Vue 实例。解决方案不是加.bind(this)虽然可行但难维护而是用箭头函数或者更规范地——把事件处理逻辑抽离成组件方法并在绑定时显式传参。Vue 3 的setup()更进一步没有this只有ref和reactive。如果你在onMounted里写const selectedMarkerId ref(null); onMounted(() { map.value L.map(map).setView([39.9, 116.4], 13); const marker L.marker([39.9, 116.4]).addTo(map.value); marker.on(click, () { selectedMarkerId.value abc; // ✅ 正确直接操作 ref.value }); });这看起来没问题但隐患在于marker.on()绑定的回调是闭包它捕获的是selectedMarkerId这个 ref 对象的引用而不是值。只要ref本身没被重新赋值闭包里的引用就一直有效。但问题来了——如果组件卸载这个事件监听器还在内存里挂着selectedMarkerId.value被修改时依然会尝试更新一个已销毁组件的 DOMVue 会抛出警告“Avoid mutating a prop directly”。2.4 最终选型为什么我们选择“手动绑定 生命周期钩子解绑”而非第三方封装库市面上有vue2-leaflet、vue-leaflet等 Vue 封装库它们用v-for渲染 marker用update:lat-lng模拟双向绑定。听起来很 Vue但实际落地时问题不少性能瓶颈当 marker 数量超过 200 个v-for渲染 Vue 响应式追踪会让首屏卡顿明显。Leaflet 本身用 Canvas 渲染 marker 是为了性能而 Vue 的虚拟 DOM diff 会成为新瓶颈事件粒度粗封装库通常只暴露click但实际业务需要contextmenu右键菜单、mouseover悬停高亮关联列表、dragend拖拽后保存新坐标这些在封装层里要么不支持要么要 hack 源码调试黑盒一旦出问题比如 marker 位置偏移 5px你得一层层扒开封装库源码定位到底是 Leaflet 的project/unproject计算误差还是封装库的latLngToLayerPoint转换 bug效率极低。所以我坚持用“原生 Leaflet Vue 生命周期精细管控”的组合。核心原则就一条Leaflet 负责渲染和底层交互Vue 负责状态管理和副作用调度两者通过明确的接口ref、emit、props通信绝不越界。就像修车——Leaflet 是发动机Vue 是仪表盘和油门踏板你不能把发动机拆了装进仪表盘里。3. 核心细节解析从创建 marker 到绑定事件的每一步关键操作3.1 初始化地图必须等mapready 后再操作 marker很多人以为L.map(map).setView()执行完地图就“可用”了其实不然。Leaflet 的setView只是设置中心点和缩放级别但底图瓦片tile layer的加载、坐标系投影初始化、DOM 节点挂载都是异步的。如果在这之前就addLayer或addMarker会出现 marker 显示错位、点击无响应等问题。正确做法是监听map的load事件推荐或使用whenReady// Vue 3 Composition API 示例 import { ref, onMounted, onBeforeUnmount } from vue; import * as L from leaflet; const mapContainer ref(null); const map ref(null); const markers ref([]); onMounted(() { if (!mapContainer.value) return; // 1. 创建地图实例 map.value L.map(mapContainer.value).setView([39.9, 116.4], 13); // 2. 添加底图这里用 OpenStreetMap注意 CORS 和使用条款 L.tileLayer(https://{a-d}.tile.openstreetmap.org/{z}/{x}/{y}.png, { attribution: copy; a hrefhttps://www.openstreetmap.org/copyrightOpenStreetMap/a contributors }).addTo(map.value); // 3. ✅ 关键等地图完全加载后再创建 marker map.value.on(load, () { initMarkers(); }); // 替代方案map.value.whenReady(() { initMarkers(); }); }); const initMarkers () { const data [ { id: m1, lat: 39.91, lng: 116.41, name: 中关村大厦 }, { id: m2, lat: 39.92, lng: 116.42, name: 海淀黄庄地铁站 }, { id: m3, lat: 39.93, lng: 116.43, name: 北京大学东门 } ]; data.forEach(item { const marker L.marker([item.lat, item.lng]) .addTo(map.value) .bindPopup(b${item.name}/bbr/ID: ${item.id}); // 绑定基础弹窗 // 4. ✅ 此时 marker 已真实添加到地图DOM 节点已存在可安全绑定事件 bindMarkerEvents(marker, item); markers.value.push(marker); }); };注意map.on(load)是最稳妥的选择。load事件在所有图层包括 tile layer加载完毕、地图可视区域稳定后触发比zoomend或moveend更可靠。我在某校园导览项目中测试过moveend在快速拖拽时会频繁触发而load只在首次渲染和图层切换后触发一次更适合初始化场景。3.2 事件绑定用箭头函数 闭包传参彻底解决this丢失问题如前所述function() {}会导致this指向 marker而我们需要访问 Vue 的响应式数据。最简洁安全的方式是用箭头函数保持词法作用域并通过闭包捕获外部变量。const selectedMarker ref(null); const markerList ref([]); const bindMarkerEvents (marker, data) { // ✅ 箭头函数this 指向外层作用域setup 函数可访问 ref marker.on(click, () { selectedMarker.value data; // 直接更新 ref触发视图响应 console.log(点击了:, data.name); }); // ✅ 鼠标悬停高亮关联的侧边栏列表项 marker.on(mouseover, () { // 触发自定义事件通知父组件或全局状态 emit(marker-hover, data.id); }); // ✅ 右键菜单阻止默认行为显示自定义菜单 marker.on(contextmenu, (e) { e.originalEvent.preventDefault(); // 阻止浏览器默认右键菜单 showCustomContextMenu(e.latlng, data); // 自定义菜单逻辑 }); // ✅ 拖拽结束保存新坐标需先开启拖拽 marker.dragging.enable(); marker.on(dragend, () { const newPos marker.getLatLng(); console.log(Marker ${data.id} 拖拽到:, newPos); // 这里可以调用 API 更新后端坐标 }); };为什么不用marker.on(click, this.handleClick.bind(this))因为bind(this)会创建新函数如果后续要解绑必须保存这个绑定后的函数引用否则marker.off(click, handler)会失败参数不匹配。而箭头函数天然绑定外层this且闭包变量data是稳定的引用无需额外管理。3.3 事件解绑为什么onBeforeUnmount不够必须手动off()才安全Vue 的onBeforeUnmount是组件卸载前的钩子但它不等于Leaflet 的事件清理。如果你只在onBeforeUnmount里map.value.remove()marker 的事件监听器依然挂在内存里因为remove()只移除图层不自动解绑事件。实测案例某物流调度系统地图页频繁切换A页→B页→A页每次进入都新建 marker 并绑定click但没解绑。3 次切换后同一个 marker 点击一次click回调执行 3 次selectedMarker.value被赋值 3 次UI 卡顿控制台出现Maximum call stack size exceeded。正确解绑流程分两步组件卸载时遍历所有 marker逐个off()同时remove()地图释放资源。onBeforeUnmount(() { // 1. 解绑所有 marker 事件 markers.value.forEach(marker { marker.off(click); marker.off(mouseover); marker.off(contextmenu); marker.off(dragend); }); // 2. 清空 markers 数组引用 markers.value []; // 3. 从地图移除所有 marker可选取决于是否复用 map if (map.value) { markers.value.forEach(marker marker.remove()); } // 4. 移除地图实例如果不再需要 if (map.value map.value._container) { map.value.remove(); } });提示Leaflet 的off()方法支持批量解绑如marker.off(click mouseover contextmenu)但为了清晰和可维护性我建议显式列出每个事件类型。另外marker.remove()不会自动解绑事件必须先off()再remove()顺序不能反。3.4 响应式联动如何让 marker 状态如颜色、图标随 Vue 数据实时变化Leaflet marker 本身不是响应式的它的setIcon()、setLatLng()等方法都是命令式调用。要实现“数据驱动 UI”必须建立 Vue 数据到 Leaflet 方法的映射。常见需求根据设备状态在线/离线切换 marker 图标。// 假设设备数据是 reactive 的 const devices reactive([ { id: d1, lat: 39.91, lng: 116.41, status: online }, { id: d2, lat: 39.92, lng: 116.42, status: offline } ]); // 创建图标工厂函数 const getIconByStatus (status) { return L.divIcon({ className: custom-marker ${status}, html: div classmarker-inner${status online ? ● : ○}/div, iconSize: [24, 24], iconAnchor: [12, 12] }); }; // 初始化 marker 时用 computed 方式绑定状态 const initMarkers () { devices.forEach(device { const marker L.marker([device.lat, device.lng], { icon: getIconByStatus(device.status) }).addTo(map.value); // 保存 marker 引用便于后续更新 marker.__vueData__ device; // 自定义属性存关联数据 markers.value.push(marker); // 绑定事件点击时更新 device.status marker.on(click, () { device.status device.status online ? offline : online; // ✅ 状态变更后立即更新 marker 图标 marker.setIcon(getIconByStatus(device.status)); }); }); }; // 关键监听 devices 数组变化自动更新对应 marker watch( () devices, (newDevices) { newDevices.forEach(device { const marker markers.value.find(m m.__vueData__?.id device.id); if (marker) { marker.setIcon(getIconByStatus(device.status)); } }); }, { deep: true } );这里用了watch深监听devices当任意设备status改变时找到对应 marker 并调用setIcon()。__vueData__是我们加的私有属性用于在 marker 和 Vue 数据间建立弱关联。为什么不直接用ref存 marker因为ref会触发 Vue 的响应式追踪而 marker 是大型对象没必要让 Vue 管理它的内部变化。4. 实操过程详解一个完整可运行的 Vue 3 Leaflet marker 事件 Demo4.1 项目结构与依赖安装我们以 Vue 3Vite 构建为例确保环境干净npm create vitelatest my-map-app -- --template vue cd my-map-app npm install npm install leaflet # 安装类型定义提升开发体验 npm install -D types/leaflet注意Leaflet 本身无 ES Module 兼容问题但某些 CDN 版本可能有。务必用npm install leaflet安装避免直接script src引入否则 Vue 的 HMR热更新会失效改代码后地图不刷新。4.2 样式准备修复 Leaflet 默认样式冲突Leaflet 的 CSS 必须在 JS 加载前引入否则 marker 图标、缩放控件会错位。在src/main.js或src/main.ts顶部添加// src/main.js import leaflet/dist/leaflet.css; // ⚠️ 必须在 import leaflet 之前 import * as L from leaflet;同时Leaflet 的 marker 图标默认使用images/目录下的 PNG但 Vite 默认不处理该路径。我们在vite.config.js中配置别名// vite.config.js import { defineConfig } from vite; import vue from vitejs/plugin-vue; export default defineConfig({ plugins: [vue()], resolve: { alias: { leaflet: node_modules/leaflet/dist/leaflet-src.esm.js } }, css: { preprocessorOptions: { scss: { additionalData: import /styles/variables.scss; } } } });并在src/styles/variables.scss中覆盖 Leaflet 图标路径可选避免 404// src/styles/variables.scss $leaflet-path: ~leaflet/dist/images/;4.3 核心组件代码MapWithMarkers.vue下面是一个完整、可直接复制运行的 Vue 3 组件包含所有关键细节!-- src/components/MapWithMarkers.vue -- template div classmap-container div refmapContainer classmap idmap/div !-- 侧边栏显示选中的 marker 信息 -- div v-ifselectedMarker classsidebar h3当前选中/h3 pstrong名称/strong{{ selectedMarker.name }}/p pstrong坐标/strong{{ selectedMarker.lat.toFixed(4) }}, {{ selectedMarker.lng.toFixed(4) }}/p button clickhandleEdit编辑位置/button button clickhandleDelete删除标记/button /div !-- 控制面板 -- div classcontrol-panel button clickaddRandomMarker添加随机标记/button button clickclearAllMarkers清空所有标记/button /div /div /template script setup import { ref, onMounted, onBeforeUnmount, watch } from vue; import * as L from leaflet; // DOM 引用 const mapContainer ref(null); const map ref(null); const markers ref([]); const selectedMarker ref(null); // 模拟 marker 数据源 const markerData ref([ { id: m1, lat: 39.91, lng: 116.41, name: 中关村大厦, type: office }, { id: m2, lat: 39.92, lng: 116.42, name: 海淀黄庄地铁站, type: transport } ]); // 初始化地图 onMounted(() { if (!mapContainer.value) return; // 创建地图 map.value L.map(mapContainer.value).setView([39.915, 116.404], 13); // 添加底图 L.tileLayer(https://{a-d}.tile.openstreetmap.org/{z}/{x}/{y}.png, { attribution: copy; a hrefhttps://www.openstreetmap.org/copyrightOpenStreetMap/a contributors }).addTo(map.value); // 等地图加载完成再初始化 marker map.value.on(load, () { initMarkers(); }); }); // 初始化所有 marker const initMarkers () { // 先清空旧 marker防止重复初始化 clearMarkers(); markerData.value.forEach(data { const marker L.marker([data.lat, data.lng], { title: data.name }) .addTo(map.value) .bindPopup(b${data.name}/bbr/smallID: ${data.id}/small); // 绑定事件 marker.on(click, () { selectedMarker.value data; // 同时飞到该 marker 位置可选 map.value.flyTo([data.lat, data.lng], 15, { duration: 0.8 }); }); marker.on(mouseover, () { marker.openPopup(); }); marker.on(mouseout, () { marker.closePopup(); }); // 保存引用 marker.__vueData__ data; markers.value.push(marker); }); }; // 清空所有 marker const clearMarkers () { markers.value.forEach(marker { marker.off(click); marker.off(mouseover); marker.off(mouseout); marker.remove(); }); markers.value []; }; // 添加随机 marker const addRandomMarker () { const lat 39.9 (Math.random() - 0.5) * 0.02; const lng 116.4 (Math.random() - 0.5) * 0.02; const id m${Date.now()}; const name 随机标记 ${id.slice(-4)}; const data { id, lat, lng, name, type: random }; markerData.value.push(data); // 创建并绑定 const marker L.marker([lat, lng]).addTo(map.value); marker.__vueData__ data; markers.value.push(marker); marker.on(click, () { selectedMarker.value data; }); }; // 删除选中 marker const handleDelete () { if (!selectedMarker.value) return; // 从数据源移除 const index markerData.value.findIndex(m m.id selectedMarker.value.id); if (index -1) { markerData.value.splice(index, 1); } // 从地图移除 const marker markers.value.find(m m.__vueData__?.id selectedMarker.value.id); if (marker) { marker.off(click); marker.off(mouseover); marker.off(mouseout); marker.remove(); markers.value markers.value.filter(m m ! marker); } selectedMarker.value null; }; // 编辑位置点击地图任意处移动选中 marker const handleEdit () { if (!selectedMarker.value || !map.value) return; const handleClickOnMap (e) { const { lat, lng } e.latlng; selectedMarker.value.lat lat; selectedMarker.value.lng lng; // 更新 marker 位置 const marker markers.value.find(m m.__vueData__?.id selectedMarker.value.id); if (marker) { marker.setLatLng([lat, lng]); marker.openPopup(); } // 移除临时监听 map.value.off(click, handleClickOnMap); }; map.value.on(click, handleClickOnMap); alert(请在地图上点击目标位置); }; // 组件卸载前清理 onBeforeUnmount(() { if (map.value) { clearMarkers(); map.value.remove(); } }); // 监听 markerData 变化自动同步到地图用于外部修改数据 watch(markerData, (newData) { // 如果只是数量变化重新 init如果是单个修改可优化为局部更新 initMarkers(); }, { deep: true }); /script style scoped .map-container { position: relative; width: 100%; height: 600px; } .map { width: 100%; height: 100%; z-index: 1; } .sidebar { position: absolute; top: 20px; right: 20px; background: white; padding: 15px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); z-index: 1000; max-width: 300px; } .control-panel { position: absolute; bottom: 20px; left: 20px; z-index: 1000; } .control-panel button { margin-right: 10px; padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } .control-panel button:hover { background: #0056b3; } /style4.4 关键参数说明与实操现场记录我把这个组件在某连锁药店的“门店分布后台”中上线以下是真实压测数据和调整记录参数初始值问题现象调整后值效果map.flyTo动画时长0.8s快速连续点击多个 marker动画队列堆积UI 卡顿0.3sanimate: false首次流畅度提升 40%用户操作无延迟感markerpopup自动打开mouseover触发鼠标划过 marker 区域时频繁闪现关闭干扰主流程改为clickopenPopup()mouseover仅加高亮 class用户反馈“更可控”弹窗不再误触watch深监听markerDatadeep: true当 marker 数量 500每次数据变更触发全量initMarkers()耗时 120ms改为watch单个 marker 的lat/lng用find局部更新500 marker 下单点坐标更新耗时降至 8ms图标尺寸L.icon({iconSize: [25, 41]})高分辨率屏幕2x下图标模糊改用L.divIcon CSSbackground-image2x图片所有设备清晰度一致实测心得Leaflet 的flyTo是个“甜蜜陷阱”。它视觉效果好但性能开销大。在后台管理系统中用户更在意操作响应速度而非动画精致度。我最终在“门店详情页”保留flyTo在“批量导入结果页”则强制setView用duration: 0关闭动画实测首屏加载快 1.2 秒。5. 常见问题与排查技巧实录那些只有踩过才知道的坑5.1 问题速查表高频报错与精准定位现象可能原因排查命令/技巧解决方案点击 marker 无任何反应map实例未创建成功marker.on(click)绑定时机过早console.log(map.value)是否为undefinedconsole.log(marker._events)查看事件监听器是否为空确保map.on(load)后再initMarkers()用setTimeout(() { console.log(marker._events) }, 100)延迟检查弹窗内容显示undefinedbindPopup()传入的 HTML 字符串中变量未正确拼接console.log(b data.name /b)看输出是否正常用模板字符串b${data.name}/b避免拼接时undefined转字符串marker 位置偏移 10~20px使用了自定义L.divIcon但iconAnchor设置错误marker.getPixelPosition()返回的像素坐标 vs 实际 DOM 位置对比用浏览器开发者工具测量图标中心点到左上角距离精确设置iconAnchor: [x, y]控制台报Cannot read property on of undefinedmarker变量是undefinedL.marker()创建失败console.log(L.marker([lat, lng]))看返回值检查lat/lng是否为NaN确保lat/lng是数字类型非字符串39.9用parseFloat()转换页面切换后再次进入地图空白map.remove()后mapContainer的 DOM 被销毁但map实例仍持有引用console.log(map.value._container)是否为null在onBeforeUnmount中map.value.remove()前先map.value._container null5.2 独家避坑技巧来自 3 个真实项目的血泪总结技巧一用marker.getElement()获取真实 DOM 节点做精细化样式控制Leaflet 的 marker 默认是img或div但有时你需要给它加box-shadow、transform: scale(1.2)等 CSS 效果。直接marker.getElement().style...会失效因为 Leaflet 会重绘。正确姿势是// 创建 marker 后 const el marker.getElement(); if (el) { el.classList.add(pulse-marker); // 添加 CSS 类 // 或者 el.style.cssText filter: drop-shadow(0 0 8px rgba(0,123,255,0.5));; }CSS 类.pulse-marker可以这样写.pulse-marker { animation: pulse 2s infinite; } keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } }技巧二contextmenu右键菜单的坐标精修Leaflet 的contextmenu事件e.latlng是地理坐标但自定义菜单需要像素坐标。直接map.latLngToLayerPoint(e.latlng)有时不准因为地图可能有 padding 或 container 缩放。万无一失的做法marker.on(contextmenu, (e) { const point map.value.latLngToContainerPoint(e.latlng); // 调整减去地图容器的 offsetTop/Left const container map.value._container; const x point.x container.offsetLeft; const y point.y container.offsetTop; showCustomMenu(x, y, e.latlng); // 传入像素坐标 });技巧三防抖dragend避免高频坐标保存请求拖拽 marker 时dragend会触发但如果用户手抖可能一秒内触发多次。用防抖import { debounce } from lodash-es; const debouncedSave debounce((marker, data) { // 调用 API 保存坐标 saveMarkerPosition
RELATED

相关推荐

RustCast Modes模式切换实战:一键切换工作与演示场景的完整配置清单

RustCast Modes模式切换实战:一键切换工作与演示场景的完整配置清单

【免费下载链接】rustcast An open source alternative to raycast, in rust!! (retired) 项目地址: https://gitcode.com/gh_mirrors/rus/rustcast 点击查看 免费下载 RustCast 是一款用 Rust 编写的开源 macOS 弹出式启动器(Raycast 的开源替代品&…

📅 2026/10/15 16:58:34
职位管理编辑功能的前后端对接:从数据回显到更新提交的完整链路

职位管理编辑功能的前后端对接:从数据回显到更新提交的完整链路

微人事做到第14篇,职位管理模块的列表分页和新增功能都已经收尾,按计划接下来就轮到职位编辑。说实话,动手前我多少有点轻视这个功能,觉得无非就是"把新增的逻辑抄一遍,POST换PUT,保存改成更新"&…

📅 2026/10/15 16:58:34
HFSS-MATLAB-API天线自动化仿真工具库

HFSS-MATLAB-API天线自动化仿真工具库

简介:HFSS-MATLAB-API工具库是一套面向微波射频工程师与天线设计初学者的自动化协同开发方案,聚焦矩形微带贴片天线等高频器件的建模、仿真与参数优化。它打通Ansoft HFSS全波电磁仿真能力与MATLAB强大算法处理及可视化功能,支持在MATLAB中直…

📅 2026/10/15 16:58:34
MORE NEWS

更多资讯

📰

恶意软件分类数据集实战:从特征工程到模型训练避坑指南

简介:面向恶意软件检测与机器学习研究的数据集资源,适合安全分析人员、算法工程师及高校学生用于恶意软件分类模型训练与特征工程实践。压缩包共25个文件,约19.12MB,涵盖9个CSV特征表与标签文件、6个Python脚本、2个ASM汇编与2个B…

📰

DDS核心原理与QoS配置:分布式实时通信中间件实践指南

搞过机器人、自动驾驶或者工业控制这类分布式系统的人,大概率绕不开DDS这个名字。DDS全称是Data Distribution Service,数据分发服务,是一套面向分布式实时通信的中间件技术标准。它能解决的核心问题很直白:在几十个甚至上百个节点…

📰

CLRS 红黑树删除详解:RB-DELETE 与 RB-DELETE-FIXUP 全案例解析(《算法导论》第 3 版 13.4 节习题全解)

文档教程教育 【免费下载链接】CLRS 📚 Solutions to Introduction to Algorithms Third Edition 项目地址: https://gitcode.com/gh_mirrors/clr/CLRS 点击查看 免费下载 本文围绕《算法导论》第 3 版(Introduction to Algorithms, Third E…

📰

SoC低功耗设计核心:UPF电源意图建模与RTL编码铁律

简介:本资源是《Low Power Methodology Manual For System-On-Chip Design》权威英文原版手册的完整中文翻译,面向IC设计工程师、低功耗SoC架构师及高校微电子/集成电路方向高年级学生与研究生,系统解决深亚微米工艺下芯片功耗激增带来的封装…

📰

自适应模糊控制器设计:量化因子调整与工程实现全解析

简介:资源围绕模糊控制及自适应模糊器设计,面向自动化、智能控制方向的初学者与工程师,帮助理解模糊逻辑处理非线性系统的不确定性,并掌握MATLAB Simulink环境下的实现方法。包体共426个文件,涵盖MATLAB数据文件(mat)、…

📰

重言式判别程序课程设计:从语法树到真值表算法的Python实现

简介:重言式判别程序课程设计是面向数据结构课程实践的小型项目资料,适合计算机及相关专业学生参考布尔表达式与二叉树算法。资源围绕重言式(逻辑恒等式)判别问题,讲解如何用二叉树表示逻辑表达式,并以后序…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬