Vue实现地图图片动态轨迹组件,支持放大缩小重置,兼容触摸等
支持支持放大缩小重置等,鼠标滚动,以及触摸等,,直接上代码组件
调用组件方式
<TrackMap style="z-index: 1;position: absolute" v-show="!showCenterMap" :height="674" :width="1284"/>
<template><divref="container"class="track-map-container"style="position: relative; display: flex; justify-content: center; align-items: center; overflow: hidden;"><divref="zoomWrapper"class="zoom-wrapper":style="zoomWrapperStyle"@wheel.prevent="onWheel"@mousedown="onMouseDown"@mousemove="onMouseMove"@mouseup="onMouseUp"@mouseleave="onMouseUp"@touchstart="onTouchStart"@touchmove="onTouchMove"@touchend="onTouchEnd"><img:src="imageSrc"ref="mapImage"class="track-map-image"@load="onImageLoad":style="{display: loaded ? 'block' : 'none',width: imgNaturalWidth + 'px',height: imgNaturalHeight + 'px',pointerEvents: 'none',userSelect: 'none'}"draggable="false"/><canvasref="canvas"class="track-map-canvas":width="imgNaturalWidth":height="imgNaturalHeight":style="{position: 'absolute',top: 0,left: 0,width: imgNaturalWidth + 'px',height: imgNaturalHeight + 'px',pointerEvents: 'none',zIndex: 2}"></canvas></div><div v-if="!loaded" class="track-map-loading">图片加载中...</div><div class="zoom-controls"><button class="zoom-btn" @click="zoomIn" title="放大"><svg width="20" height="20" fill="none" viewBox="0 0 20 20"><circle cx="10" cy="10" r="9" stroke="#333" stroke-width="2"/><rect x="6" y="9" width="8" height="2" rx="1" fill="#333"/><rect x="9" y="6" width="2" height="8" rx="1" fill="#333"/></svg></button><button class="zoom-btn" @click="zoomOut" title="缩小"><svg width="20" height="20" fill="none" viewBox="0 0 20 20"><circle cx="10" cy="10" r="9" stroke="#333" stroke-width="2"/><rect x="6" y="9" width="8" height="2" rx="1" fill="#333"/></svg></button><button class="zoom-btn" @click="resetZoom" title="重置"><svg t="1753323363353" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"width="24" height="24"><pathd="M502.714987 58.258904l-126.531056-54.617723a52.797131 52.797131 0 0 0-41.873587 96.855428A447.865322 447.865322 0 0 0 392.02307 946.707184a61.535967 61.535967 0 0 0 13.83649 1.820591 52.797131 52.797131 0 0 0 13.65443-103.773672 342.453118 342.453118 0 0 1-31.678278-651.771485l-8.374718 19.480321a52.615072 52.615072 0 0 0 27.855039 69.182448 51.522718 51.522718 0 0 0 20.572675 4.369418A52.797131 52.797131 0 0 0 476.498481 254.882703L530.205907 127.441352a52.979191 52.979191 0 0 0-27.49092-69.182448zM962.960326 509.765407A448.775617 448.775617 0 0 0 643.992829 68.090094a52.797131 52.797131 0 1 0-30.403866 101.042786A342.635177 342.635177 0 0 1 674.578753 801.059925a52.615072 52.615072 0 0 0-92.30395-50.612422l-71.913335 117.246043a52.433013 52.433013 0 0 0 17.295612 72.82363l117.063985 72.823629a52.797131 52.797131 0 1 0 54.617722-89.755123l-16.021198-10.013249A448.593558 448.593558 0 0 0 962.960326 509.765407z"fill="#333333"></path></svg></button></div></div>
</template><script>
/*** TrackMap 轨迹地图组件* 功能说明:* - 支持图片和轨迹的缩放与拖拽(平移)* - 支持鼠标滚轮缩放和按钮缩放* - 支持移动端双指缩放与拖拽* - 支持轨迹点和连线的动态刷新* - 保证拖拽和缩放时,轨迹与图片始终对齐,无错位*/
export default {name: "TrackMap",data() {return {imageSrc: require('@/assets/img/map.png'), // 地图图片interval: 1000, // 轨迹刷新频率(毫秒)pointColor: '#FF0000',lineColor: '#00FF00',pointRadius: 4,imgNaturalWidth: 0, // 图片原始宽度imgNaturalHeight: 0, // 图片原始高度loaded: false,points: [], // 轨迹点timer: null, // 绘制定时器moveTimer: null, // 轨迹移动定时器currentPoint: {x: 0, y: 0}, // 当前点// 缩放与拖拽相关zoom: 1, // 当前缩放倍数minZoom: 0.2,maxZoom: 5,offsetX: 0, // 拖拽偏移(px)offsetY: 0,dragging: false,dragStart: {x: 0, y: 0},lastOffset: {x: 0, y: 0},pinchStartDist: 0,pinchStartZoom: 1,pinchCenter: null, // 用于双指缩放中心点};},computed: {// 外层包裹div的样式,实现缩放和拖动zoomWrapperStyle() {return {width: this.imgNaturalWidth + 'px',height: this.imgNaturalHeight + 'px',transform: `translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.zoom})`,transformOrigin: '0 0',position: 'relative',cursor: this.dragging ? 'grabbing' : 'grab',transition: this.dragging ? 'none' : 'transform 0.06s cubic-bezier(.4,2,.6,1)',zIndex: 1,touchAction: 'none'};}},mounted() {// 若图片已缓存,则立即初始化if (this.$refs.mapImage && this.$refs.mapImage.complete) {this.initPoints();this.onImageLoad();}},beforeDestroy() {this.stopFetching();this.stopMoveTimer();window.removeEventListener('mousemove', this.onMouseMove);window.removeEventListener('mouseup', this.onMouseUp);},methods: {/** 初始化轨迹点 */initPoints() {// TODO: 这里需要接口获取初始点this.currentPoint = {x: 50, y: 50};},/** 图片加载后,设置尺寸并初始化轨迹 */onImageLoad() {const img = this.$refs.mapImage;this.imgNaturalWidth = img.naturalWidth;this.imgNaturalHeight = img.naturalHeight;this.$nextTick(() => {this.loaded = true;this.points = [Object.assign({}, this.currentPoint)];this.draw();this.startMoveTimer();this.startFetching();});},/** 每3秒生成新轨迹点 */startMoveTimer() {this.stopMoveTimer();this.moveTimer = setInterval(() => {// TODO: 这里通过接口获取新点this.currentPoint = {x: Math.min(this.imgNaturalWidth - 20, this.currentPoint.x + 25),y: Math.min(this.imgNaturalHeight - 20, this.currentPoint.y + 25)};this.points.push(Object.assign({}, this.currentPoint));}, 3000);},stopMoveTimer() {if (this.moveTimer) {clearInterval(this.moveTimer);this.moveTimer = null;}},/** 绘制轨迹点和线 */draw() {const canvas = this.$refs.canvas;if (!canvas) return;const ctx = canvas.getContext('2d');// 清除画布ctx.clearRect(0, 0, this.imgNaturalWidth, this.imgNaturalHeight);if (this.points.length === 0) return;// 画连线ctx.save();ctx.strokeStyle = this.lineColor;ctx.lineWidth = 2;ctx.beginPath();this.points.forEach((p, i) => {if (i === 0) {ctx.moveTo(p.x, p.y);} else {ctx.lineTo(p.x, p.y);}});ctx.stroke();// 画点ctx.fillStyle = this.pointColor;this.points.forEach((p) => {ctx.beginPath();ctx.arc(p.x, p.y, this.pointRadius, 0, 2 * Math.PI);ctx.fill();});ctx.restore();},/** 每1秒刷新轨迹 */startFetching() {this.stopFetching();this.draw();this.timer = setInterval(() => {this.draw();}, this.interval);},stopFetching() {if (this.timer) {clearInterval(this.timer);this.timer = null;}},/** 放大操作 */zoomIn() {this.setZoom(this.zoom * 1.2);},/** 缩小操作 */zoomOut() {this.setZoom(this.zoom / 1.2);},/** 重置缩放和平移 */resetZoom() {this.zoom = 1;this.offsetX = 0;this.offsetY = 0;},/*** 设置缩放,支持以指定中心点为缩放锚点* @param {number} newZoom 新缩放倍数* @param {object|null} center {x, y} 屏幕坐标,缩放中心点*/setZoom(newZoom, center = null) {const prevZoom = this.zoom;newZoom = Math.max(this.minZoom, Math.min(this.maxZoom, newZoom));if (center) {// 保持缩放中心点位置不变// 先算出中心点在图片上的坐标const wrapperRect = this.$refs.container.getBoundingClientRect();const cx = center.x - wrapperRect.left - this.offsetX;const cy = center.y - wrapperRect.top - this.offsetY;// 缩放后新的偏移量this.offsetX -= cx * (newZoom / prevZoom - 1);this.offsetY -= cy * (newZoom / prevZoom - 1);}this.zoom = newZoom;},/** 鼠标滚轮缩放 */onWheel(e) {if (!this.loaded) return;const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9;this.setZoom(this.zoom * zoomFactor, {x: e.clientX, y: e.clientY});},/** 鼠标按下,开始拖拽 */onMouseDown(e) {if (!this.loaded) return;if (e.button !== 0) return; // 仅左键this.dragging = true;this.dragStart = {x: e.clientX, y: e.clientY};this.lastOffset = {x: this.offsetX, y: this.offsetY};window.addEventListener('mousemove', this.onMouseMove);window.addEventListener('mouseup', this.onMouseUp);},/** 鼠标移动,拖拽平移 */onMouseMove(e) {if (!this.dragging) return;const dx = e.clientX - this.dragStart.x;const dy = e.clientY - this.dragStart.y;this.offsetX = this.lastOffset.x + dx;this.offsetY = this.lastOffset.y + dy;},/** 鼠标松开,结束拖拽 */onMouseUp() {if (!this.dragging) return;this.dragging = false;window.removeEventListener('mousemove', this.onMouseMove);window.removeEventListener('mouseup', this.onMouseUp);},/** 触摸开始,支持单指拖拽和双指缩放 */onTouchStart(e) {if (e.touches.length === 1) {this.dragging = true;this.dragStart = {x: e.touches[0].clientX, y: e.touches[0].clientY};this.lastOffset = {x: this.offsetX, y: this.offsetY};} else if (e.touches.length === 2) {this.dragging = false;this.pinchStartDist = this.getTouchDist(e);this.pinchStartZoom = this.zoom;this.pinchCenter = this.getTouchCenter(e);}},/** 触摸移动,拖拽或缩放 */onTouchMove(e) {if (e.touches.length === 1 && this.dragging) {const dx = e.touches[0].clientX - this.dragStart.x;const dy = e.touches[0].clientY - this.dragStart.y;this.offsetX = this.lastOffset.x + dx;this.offsetY = this.lastOffset.y + dy;} else if (e.touches.length === 2) {const newDist = this.getTouchDist(e);const scale = newDist / this.pinchStartDist;this.setZoom(this.pinchStartZoom * scale, this.pinchCenter);}},/** 触摸结束,重置拖拽状态 */onTouchEnd(e) {if (e.touches.length === 0) {this.dragging = false;}},/** 计算双指间距 */getTouchDist(e) {const dx = e.touches[0].clientX - e.touches[1].clientX;const dy = e.touches[0].clientY - e.touches[1].clientY;return Math.sqrt(dx * dx + dy * dy);},/** 计算双指中点 */getTouchCenter(e) {const x = (e.touches[0].clientX + e.touches[1].clientX) / 2;const y = (e.touches[0].clientY + e.touches[1].clientY) / 2;return {x, y};}}
};
</script><style scoped>
.track-map-container {user-select: none;width: 100%;max-width: 100vw;height: 675px;position: relative;overflow: hidden;background: #f5f5f7;
}.zoom-wrapper {position: relative;width: 100%;height: 100%;overflow: visible;touch-action: none;will-change: transform;
}.track-map-canvas {z-index: 2;display: block;pointer-events: none;
}.track-map-image {z-index: 1;display: block;pointer-events: none;user-select: none;
}.track-map-loading {position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);background: rgba(255, 255, 255, 0.88);padding: 16px 32px;border-radius: 10px;z-index: 10;box-shadow: 0 4px 24px 0 rgba(0, 0, 0, 0.06);font-size: 18px;color: #555;
}.zoom-controls {position: absolute;right: 24px;top: 24px;z-index: 20;display: flex;flex-direction: column;gap: 12px;background: rgba(255, 255, 255, 0.75);border-radius: 16px;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);padding: 12px 8px;
}.zoom-btn {width: 40px;height: 40px;background: linear-gradient(180deg, #fff 70%, #f2f2f7 100%);border: none;border-radius: 8px;cursor: pointer;margin: 0 auto;display: flex;align-items: center;justify-content: center;transition: box-shadow 0.15s, background 0.15s;box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.05);outline: none;
}.zoom-btn:hover, .zoom-btn:focus {background: #f2f2f7;box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.08);
}.zoom-btn svg {display: block;
}
</style>