@@ -1,838 +0,0 @@
< template >
< div class = "page_container" : class = "{'enClass': $i18n.locale === 'en-us'}" >
< div class = "canvas-container" ref = "mapContainer" >
< canvas
class = "mapCanvas"
ref = "mapCanvas"
@click ="handleCanvasClick"
@touchstart ="handleTouchStart"
@touchmove ="handleTouchMove"
@touchend ="handleTouchEnd"
@mousedown ="handleMouseDown"
@mousemove ="handleMouseMove"
@mouseup ="handleMouseUp"
@mouseleave ="handleMouseUp"
@wheel ="handleZoom"
> < / canvas >
< svg
class = "mapSvg"
ref = "mapSvg"
xmlns = "http://www.w3.org/2000/svg"
>
< g ref = "svgPoints" >
< circle
v-for = "(point, index) in greenPoints"
:key = "index"
:cx = "convertToSvgX(point.x)"
:cy = "convertToSvgY(point.y)"
r = "1"
fill = "red"
/ >
< / g >
< image
ref = "carImage"
:href = "require('../../images/new/agv.png')"
alt = "小车"
width = "30"
height = "30"
:x = "carX"
:y = "carY"
: transform = "`rotate(${rotateAngle} ${carX + 15} ${carY + 15})`"
@mousedown ="startCarDrag"
@touchstart ="startCarDrag"
/ >
< / svg >
< div class = "map_tools" >
< el-button class = "zoom_btn" type = "primary" : disabled = "zoomPercentage === 2" icon = "el-icon-minus" size = "mini" @click ="zoomOut" > < / el -button >
< div class = "zoom_data" > { { zoomPercentage } } % < / div >
< el-button class = "zoom_btn" type = "primary" : disabled = "zoomPercentage === 200" icon = "el-icon-plus" size = "mini" @click ="zoomIn" > < / el -button >
< el-button class = "zoom_btn" style = "margin-top: .2rem !important" type = "primary" icon = "el-icon-rank" size = "mini" > < / el-button >
< el-button class = "zoom_btn" type = "primary" icon = "el-icon-location" size = "mini" @click ="handleRelocate" > < / el -button >
< / div >
< / div >
< div
v-if = "showPopup"
class = "point-popup"
:style = "popupStyle"
@click.stop
>
< el-row type = "flex" justify = "space-between" class = "popup-content" style = "border-bottom: 1px solid #fff;" >
< el-col :span = "10" > < h3 > { { $t ( 'Number' ) } } < / h3 > < / el-col >
< el-col :span = "14" > < p > { { selectedPoint . station _code } } < / p > < / el-col >
< / el-row >
< el-row type = "flex" justify = "space-between" class = "popup-content" >
< el-col :span = "10" > < h3 > { { $t ( 'Alias' ) } } < / h3 > < / el-col >
< el-col :span = "14" > < p > { { selectedPoint . station _name } } < / p > < / el-col >
< / el-row >
< el-row type = "flex" justify = "space-between" class = "popup-content" >
< el-col :span = "10" > < h3 > { { $t ( 'XCoordinate' ) } } < / h3 > < / el-col >
< el-col :span = "14" > < p > { { selectedPoint . x } } < / p > < / el-col >
< / el-row >
< el-row type = "flex" justify = "space-between" class = "popup-content" >
< el-col :span = "10" > < h3 > { { $t ( 'YCoordinate' ) } } < / h3 > < / el-col >
< el-col :span = "14" > < p > { { selectedPoint . y } } < / p > < / el-col >
< / el-row >
< el-row type = "flex" justify = "space-between" class = "popup-content" >
< el-col :span = "10" > < h3 > { { $t ( 'AngleValue' ) } } < / h3 > < / el-col >
< el-col :span = "14" > < p > { { selectedPoint . angle } } < / p > < / el-col >
< / el-row >
< / div >
< div
v-if = "showPathPopup"
class = "point-popup"
:style = "pathPopupStyle"
@click.stop
>
< el-row type = "flex" justify = "space-between" class = "popup-content" style = "border-bottom: 1px solid #fff;" >
< el-col :span = "10" > < h3 > { { $t ( 'PathID' ) } } < / h3 > < / el-col >
< el-col :span = "14" > < p > { { selectedPath . route _id } } < / p > < / el-col >
< / el-row >
< el-row type = "flex" justify = "space-between" class = "popup-content" >
< el-col :span = "10" > < h3 > { { $t ( 'StartCode' ) } } < / h3 > < / el-col >
< el-col :span = "14" > < p > { { getStationNameById ( selectedPath . start _id ) } } < / p > < / el-col >
< / el-row >
< el-row type = "flex" justify = "space-between" class = "popup-content" >
< el-col :span = "10" > < h3 > { { $t ( 'EndCode' ) } } < / h3 > < / el-col >
< el-col :span = "14" > < p > { { getStationNameById ( selectedPath . end _id ) } } < / p > < / el-col >
< / el-row >
< / div >
< / div >
< / template >
< script >
import { throttle } from 'lodash'
import markerImage from '@/images/new/station.png'
import { getMapInfoByCode , getRouteInfo , queryMapAllStation } from '@/config/mork.js'
import { mapGetters } from 'vuex'
import { points } from '@/config/point.js'
/* eslint-disable */
export default {
name : 'ModuleMap' ,
data ( ) {
return {
canvas : null , // Canvas元素
ctx : null , // Canvas绘图上下文
canvasWidth : 0 , // Canvas/SVG 宽度(同步更新)
canvasHeight : 0 , // Canvas/SVG 高度(同步更新)
originX : 0 , // Canvas原点X
originY : 0 , // Canvas原点Y
cachedImages : {
map : null ,
marker : null
} ,
mapData : null , // 地图数据
pathData : null , // 路径数据
pointData : null , // 点位数据
mapScale : 1 , // 地图图片缩放比例
showPopup : false ,
selectedPoint : { } ,
popupStyle : { left : '0px' , top : '0px' } ,
selectedPointId : null ,
showPathPopup : false ,
selectedPath : { route _id : null } ,
pathPopupStyle : { left : '0px' , top : '0px' } ,
pathClickThreshold : 5 ,
touchStart : null , // 触摸起点
isDragging : false , // 是否正在拖动地图
lastX : 0 , // 上一次鼠标/触摸X位置
lastY : 0 , // 上一次鼠标/触摸Y位置
offsetX : 0 , // Canvas偏移X
offsetY : 0 , // Canvas偏移Y
scale : 1 , // 缩放比例
zoomPercentage : 100 , // 当前缩放百分比
greenPoints : [ ] , // 绿色点位数组
websocket : null , // WebSocket实例
isCarLocked : false , // 是否锁定小车位置( 点击relocate后为true)
isDraggingCar : false , // 是否正在拖拽小车
carX : 0 , // 小车自然坐标转页面坐标
carY : 0 , // 小车自然坐标转页面坐标
rotateAngle : 0 , // 旋转弧度
dragStartX : 0 , // 记录拖拽开始的偏移量
dragStartY : 0 // 记录拖拽开始的偏移量
}
} ,
computed : {
... mapGetters ( [ 'serverUrl' , 'carPosition' , 'userRole' ] )
} ,
watch : {
carPosition : {
handler ( newVal ) {
this . updateCarPosition ( newVal )
} ,
deep : true
} ,
mapData : {
handler ( newVal ) {
if ( newVal ) {
this . updateCarPosition ( this . carPosition )
}
}
}
} ,
mounted ( ) {
this . loadAllDataInParallel ( )
} ,
beforeDestroy ( ) {
this . closeWebSocket ( )
} ,
methods : {
async loadAllDataInParallel ( ) {
try {
this . loading = this . $loading ( {
lock : true ,
spinner : 'el-icon-loading' ,
background : 'rgba(0, 0, 0, 0.6)'
} )
const [ mapData , pathData , stations ] = await Promise . all ( [
this . _getMapInfoByCode ( ) ,
this . _getRouteInfo ( ) ,
this . _queryMapAllStation ( )
] )
await Promise . all ( [
this . preloadImage ( 'map' , mapData . mapImageAddress ) ,
this . preloadImage ( 'marker' , markerImage )
] )
this . mapData = mapData
this . pathData = [ ... pathData ]
this . pointData = this . filterPointData ( pathData , stations )
this . initCanvas ( )
this . loading . close ( )
return true
} catch ( e ) {
this . $message . error ( e . message )
this . loading . close ( )
return false
}
} ,
preloadImage ( key , url ) {
return new Promise ( ( resolve , reject ) => {
const img = new Image ( )
img . src = url
img . onload = ( ) => {
this . cachedImages [ key ] = img
resolve ( )
}
img . onerror = ( ) => reject ( new Error ( ` 图片加载失败: ${ url } ` ) )
} )
} ,
async _getMapInfoByCode ( ) {
try {
const res = await getMapInfoByCode ( )
if ( ! res ) throw new Error ( '地图信息为空' )
return res
} catch ( e ) {
console . error ( '获取地图信息失败:' , e )
throw new Error ( ` 获取地图信息失败: ${ e . message } ` )
}
} ,
async _getRouteInfo ( ) {
try {
const res = await getRouteInfo ( )
if ( ! res ) throw new Error ( '路径信息为空' )
return res
} catch ( e ) {
console . error ( '获取路径信息失败:' , e )
throw new Error ( ` 获取路径信息失败: ${ e . message } ` )
}
} ,
async _queryMapAllStation ( ) {
try {
const res = await queryMapAllStation ( )
if ( ! res ) throw new Error ( '站点信息为空' )
return res
} catch ( e ) {
console . error ( '获取站点信息失败:' , e )
throw new Error ( ` 获取站点信息失败: ${ e . message } ` )
}
} ,
filterPointData ( routes , stations ) {
const result = [ ]
const seenStationIds = new Set ( )
routes . forEach ( route => {
const startStation = stations . find ( s => s . station _id === route . start _id )
const endStation = stations . find ( s => s . station _id === route . end _id )
if ( startStation && ! seenStationIds . has ( startStation . station _id ) ) {
result . push ( startStation )
seenStationIds . add ( startStation . station _id )
}
if ( endStation && ! seenStationIds . has ( endStation . station _id ) ) {
result . push ( endStation )
seenStationIds . add ( endStation . station _id )
}
} )
return result
} ,
initCanvas ( ) {
this . canvas = this . $refs . mapCanvas
this . ctx = this . canvas . getContext ( '2d' )
const container = this . $refs . mapContainer
this . canvasWidth = container . clientWidth
this . canvasHeight = container . clientHeight
this . canvas . width = this . canvasWidth
this . canvas . height = this . canvasHeight
// 计算地图图片缩放比例
if ( this . mapData . width > this . canvas . width || this . mapData . height > this . canvas . height ) {
const scaleX = this . canvas . width / this . mapData . width
const scaleY = this . canvas . height / this . mapData . height
this . mapScale = Math . min ( scaleX , scaleY )
}
const scaledWidth = this . mapData . width * this . mapScale
const scaledHeight = this . mapData . height * this . mapScale
// 计算地图图片左上角位置(即要移动到的原点)
this . originX = ( this . canvas . width - scaledWidth ) / 2
this . originY = ( this . canvas . height - scaledHeight ) / 2
this . ctx . translate ( this . originX , this . originY )
this . ctx . save ( )
this . redrawCanvas ( )
// svg
const mapSvg = this . $refs . mapSvg
mapSvg . setAttribute ( 'width' , this . canvasWidth ) ;
mapSvg . setAttribute ( 'height' , this . canvasHeight ) ;
mapSvg . setAttribute ( 'viewBox' , ` - ${ this . originX } - ${ this . originY } ${ this . canvasWidth } ${ this . canvasHeight } ` )
} ,
updateCarPosition ( newVal ) {
if ( ! this . mapData ) {
return
}
if ( typeof newVal ? . x !== 'number' || typeof newVal ? . y !== 'number' ) {
return
}
if ( ! this . isCarLocked ) {
const carX = ( ( newVal . x - this . mapData . x ) / this . mapData . resolution ) * this . mapScale
const carY = ( this . mapData . height - ( newVal . y - this . mapData . y ) / this . mapData . resolution ) * this . mapScale
const rotateAngle = - newVal . angle * 57.295779513
this . carX = carX - 15
this . carY = carY - 15
this . rotateAngle = rotateAngle
}
} ,
redrawCanvas : throttle ( function ( ) {
if ( ! this . ctx || ! this . mapData ) return
const scaledWidth = this . mapData . width * this . mapScale
const scaledHeight = this . mapData . height * this . mapScale
this . ctx . save ( )
this . ctx . setTransform ( 1 , 0 , 0 , 1 , 0 , 0 )
this . ctx . clearRect ( 0 , 0 , this . canvas . width , this . canvas . height )
this . ctx . restore ( )
if ( ! this . cachedImages . map ) {
this . cachedImages . map = new Image ( )
this . cachedImages . map . src = this . mapData . mapImageAddress
this . cachedImages . map . onload = ( ) => {
this . drawMapAndOverlays ( scaledWidth , scaledHeight )
}
} else {
this . drawMapAndOverlays ( scaledWidth , scaledHeight )
}
} , 30 ) ,
drawMapAndOverlays ( scaledWidth , scaledHeight ) {
if ( ! this . ctx || ! this . cachedImages . map ) return
// 绘制地图背景
this . ctx . drawImage ( this . cachedImages . map , 0 , 0 , scaledWidth , scaledHeight )
// 绘制路径(叠加在地图上)
this . drawPath ( )
// 绘制点位(叠加在最上层)
this . drawMarkers ( )
} ,
drawPath ( ) {
if ( ! this . pathData . length ) return
this . pathData . forEach ( ( point , index ) => {
const startX = ( ( point . start _x - this . mapData . x ) / this . mapData . resolution ) * this . mapScale
const startY = ( this . mapData . height - ( point . start _y - this . mapData . y ) / this . mapData . resolution ) * this . mapScale
const endX = ( ( point . end _x - this . mapData . x ) / this . mapData . resolution ) * this . mapScale
const endY = ( this . mapData . height - ( point . end _y - this . mapData . y ) / this . mapData . resolution ) * this . mapScale
const nextPoint = this . pathData [ index + 1 ] ;
let controlX , controlY ;
if ( nextPoint ) {
// 有下一个点时,使用当前终点和下一个起点的中点作为控制点
const nextStartX = ( ( nextPoint . start _x - this . mapData . x ) / this . mapData . resolution ) * this . mapScale ;
const nextStartY = ( this . mapData . height - ( nextPoint . start _y - this . mapData . y ) / this . mapData . resolution ) * this . mapScale ;
controlX = ( endX + nextStartX ) / 2 ;
controlY = ( endY + nextStartY ) / 2 ;
} else {
// 最后一个点,使用当前终点作为控制点
controlX = endX ;
controlY = endY ;
}
this . ctx . beginPath ( )
this . ctx . moveTo ( startX , startY )
this . ctx . quadraticCurveTo ( controlX , controlY , endX , endY ) ;
if ( this . selectedPath . route _id === point . route _id ) {
this . ctx . strokeStyle = '#ff5722' // 橙色高亮
this . ctx . lineWidth = 4
} else {
this . ctx . strokeStyle = '#009de5' // 默认蓝色
this . ctx . lineWidth = 2
}
this . ctx . stroke ( )
} )
} ,
drawMarkers ( ) {
if ( ! this . pointData . length || ! this . ctx ) return
const markerSize = 16
if ( ! this . cachedImages . marker ) {
this . cachedImages . marker = new Image ( )
this . cachedImages . marker . src = markerImage
// 图标加载完成后重新绘制,确保点位显示
this . cachedImages . marker . onload = ( ) => {
this . redrawCanvas ( )
}
return // 等待图标加载完成后再绘制
}
this . pointData . forEach ( point => {
const x = ( ( point . x - this . mapData . x ) / this . mapData . resolution ) * this . mapScale
const y = ( this . mapData . height - ( point . y - this . mapData . y ) / this . mapData . resolution ) * this . mapScale
// 绘制选中状态
if ( point . station _id === this . selectedPointId ) {
this . ctx . beginPath ( )
this . ctx . arc ( x , y , 5 , 0 , Math . PI * 2 )
this . ctx . fillStyle = '#59ccd2'
this . ctx . fill ( )
} else {
this . ctx . drawImage ( this . cachedImages . marker , x - markerSize / 2 , y - markerSize / 2 , markerSize , markerSize )
}
// 绘制点位名称(文字大小随缩放变化)
if ( point . station _type !== 'Station' ) {
this . ctx . font = '12px Arial'
this . ctx . fillStyle = '#62fa0a'
this . ctx . textAlign = 'center'
this . ctx . fillText ( point . station _name , x , y + 22 )
this . ctx . fillText ( point . x , x , y + 34 )
this . ctx . fillText ( point . y , x , y + 50 )
}
} )
} ,
handleCanvasClick ( event ) {
const rect = this . canvas . getBoundingClientRect ( )
const mouseX = ( event . clientX - rect . left - this . originX ) / this . scale
const mouseY = ( event . clientY - rect . top - this . originY ) / this . scale
// 1. 优先检测点位点击
let pointClicked = false
for ( const point of this . pointData ) {
let x = ( ( point . x - this . mapData . x ) / this . mapData . resolution ) * this . mapScale
let y = ( this . mapData . height - ( point . y - this . mapData . y ) / this . mapData . resolution ) * this . mapScale
x = Math . abs ( x ) === 0 ? 0 : x
y = Math . abs ( y ) === 0 ? 0 : y
if ( mouseX >= x - 10 && mouseX <= x + 10 && mouseY >= y - 10 && mouseY <= y + 10 ) {
this . handlePointSelect ( point , event )
event . stopPropagation ( )
pointClicked = true
if ( this . showPathPopup ) {
this . selectedPath = { route _id : null }
this . showPathPopup = false
this . redrawCanvas ( )
}
break
}
}
// 2. 只有点位未被点击时,才检测路径点击
if ( ! pointClicked ) {
if ( this . showPopup ) {
this . selectedPointId = null
this . selectedPoint = null
this . showPopup = false
this . redrawCanvas ( )
}
let pathClicked = false
for ( const path of this . pathData ) {
// 计算路径线段的Canvas坐标
const startX = ( ( path . start _x - this . mapData . x ) / this . mapData . resolution ) * this . mapScale
const startY = ( this . mapData . height - ( path . start _y - this . mapData . y ) / this . mapData . resolution ) * this . mapScale
const endX = ( ( path . end _x - this . mapData . x ) / this . mapData . resolution ) * this . mapScale
const endY = ( this . mapData . height - ( path . end _y - this . mapData . y ) / this . mapData . resolution ) * this . mapScale
// 计算点击点到线段的距离
const distance = this . pointToLineDistance (
{ x : mouseX , y : mouseY } ,
{ x : startX , y : startY } ,
{ x : endX , y : endY }
)
// 距离小于阈值则认为点击了该路径
if ( distance <= this . pathClickThreshold ) {
this . handlePathSelect ( path , event )
event . stopPropagation ( )
pathClicked = true
break
}
}
if ( ! pathClicked && this . showPathPopup ) {
this . selectedPath = { route _id : null }
this . showPathPopup = false
this . redrawCanvas ( )
}
}
} ,
// 计算点到线段的最短距离
pointToLineDistance ( point , lineStart , lineEnd ) {
// 线段向量
const dx = lineEnd . x - lineStart . x
const dy = lineEnd . y - lineStart . y
// 线段长度的平方
const lengthSquared = dx * dx + dy * dy
// 线段长度为0( 起点终点重合) , 直接返回点到起点的距离
if ( lengthSquared === 0 ) {
return Math . hypot ( point . x - lineStart . x , point . y - lineStart . y )
}
// 计算点击点在投影到线段上的位置比例( 0~1之间为在线段上)
let t = ( ( point . x - lineStart . x ) * dx + ( point . y - lineStart . y ) * dy ) / lengthSquared
t = Math . max ( 0 , Math . min ( 1 , t ) ) // 限制在0~1范围内
// 投影点坐标
const projectionX = lineStart . x + t * dx
const projectionY = lineStart . y + t * dy
// 点到投影点的距离
return Math . hypot ( point . x - projectionX , point . y - projectionY )
} ,
// 处理点位选中
handlePointSelect ( point , event ) {
if ( this . selectedPointId === point . station _id ) {
this . selectedPointId = null
this . selectedPoint = null
this . showPopup = false
} else {
this . selectedPointId = point . station _id
this . selectedPoint = point
this . showPopup = true
this . calculatePopupPosition ( event )
}
this . redrawCanvas ( )
} ,
// 处理路径选中
handlePathSelect ( path , event ) {
if ( this . selectedPath . route _id === path . route _id ) {
this . selectedPath = { route _id : null }
this . showPathPopup = false
} else {
this . selectedPath = path
this . showPathPopup = true
this . calculatePathPopupPosition ( event )
}
this . redrawCanvas ( )
} ,
// 计算弹窗位置
calculatePopupPosition ( event ) {
const popupWidth = 200
const popupHeight = 180
let left = event . clientX - 40
let top = event . clientY - 150
// 限制弹窗在窗口可视范围内
left = Math . max ( 10 , Math . min ( left , window . innerWidth - popupWidth - 10 ) )
top = Math . max ( 10 , Math . min ( top , window . innerHeight - popupHeight - 10 ) )
this . popupStyle = { left : ` ${ left } px ` , top : ` ${ top } px ` }
} ,
// 计算路径弹窗位置
calculatePathPopupPosition ( event ) {
const popupWidth = 160
const popupHeight = 100
let left = event . clientX - 40
let top = event . clientY - 120
// 限制弹窗在窗口可视范围内
left = Math . max ( 10 , Math . min ( left , window . innerWidth - popupWidth - 10 ) )
top = Math . max ( 10 , Math . min ( top , window . innerHeight - popupHeight - 10 ) )
this . pathPopupStyle = { left : ` ${ left } px ` , top : ` ${ top } px ` }
} ,
getStationNameById ( stationId ) {
if ( ! stationId || ! this . pointData || ! this . pointData . length ) return null
const station = this . pointData . find ( point => point . station _id === stationId )
return station ? station . station _name : null
} ,
// 触摸事件:支持单指拖动和双指缩放
handleTouchStart ( event ) {
// 记录触摸点
const touches = Array . from ( event . touches )
if ( touches . length === 1 ) {
// 单指:准备拖动
this . isDragging = true
this . lastX = touches [ 0 ] . clientX
this . lastY = touches [ 0 ] . clientY
} else if ( touches . length === 2 ) {
// 双指:准备缩放(禁用拖动)
this . isDragging = false
this . touchStart = touches . map ( touch => ( {
x : touch . clientX ,
y : touch . clientY
} ) )
}
} ,
applyTransform ( ) {
const mapSvg = this . $refs . mapSvg
if ( this . canvas ) {
this . canvas . style . transform = ` translate( ${ this . offsetX } px, ${ this . offsetY } px) scale( ${ this . scale } ) `
mapSvg . style . transform = ` translate( ${ this . offsetX } px, ${ this . offsetY } px) scale( ${ this . scale } ) `
}
} ,
// 计算两点之间的距离
getDistance ( x1 , y1 , x2 , y2 ) {
return Math . sqrt ( ( x2 - x1 ) * * 2 + ( y2 - y1 ) * * 2 )
} ,
zoom ( step ) {
this . scale = Math . min ( 2 , Math . max ( 0.02 , this . scale + step / 100 ) )
this . zoomPercentage = Math . round ( this . scale * 100 )
this . applyTransform ( )
} ,
handleTouchMove ( event ) {
event . preventDefault ( ) // 阻止页面滚动
const touches = Array . from ( event . touches )
if ( this . isDragging && touches . length === 1 ) {
// 单指拖动
const currentX = touches [ 0 ] . clientX
const currentY = touches [ 0 ] . clientY
// 计算移动距离(考虑缩放影响,拖动速度与缩放比例成反比)
const moveX = ( currentX - this . lastX ) / this . scale
const moveY = ( currentY - this . lastY ) / this . scale
// 更新偏移量
this . offsetX += moveX
this . offsetY += moveY
// 更新上一次位置
this . lastX = currentX
this . lastY = currentY
// 应用变换
this . applyTransform ( )
} else if ( this . touchStart && touches . length === 2 ) {
// 双指缩放(原有逻辑)
const startDist = this . getDistance ( this . touchStart [ 0 ] . x , this . touchStart [ 0 ] . y , this . touchStart [ 1 ] . x , this . touchStart [ 1 ] . y )
const currentDist = this . getDistance ( touches [ 0 ] . clientX , touches [ 0 ] . clientY , touches [ 1 ] . clientX , touches [ 1 ] . clientY )
// 计算缩放步长(更平滑的缩放)
const scaleFactor = currentDist / startDist
const step = ( scaleFactor - 1 ) * 50 ; // 调整缩放灵敏度
this . zoom ( step )
// 更新触摸起点
this . touchStart = touches . map ( touch => ( {
x : touch . clientX ,
y : touch . clientY
} ) )
}
} ,
handleTouchEnd ( ) {
this . touchStart = null
this . isDragging = false
} ,
// 鼠标按下(开始拖动)
handleMouseDown ( event ) {
event . preventDefault ( )
this . isDragging = true
// 记录初始鼠标位置
this . lastX = event . clientX
this . lastY = event . clientY
// 鼠标样式变为抓手
this . canvas . style . cursor = 'grabbing'
} ,
// 鼠标移动(拖动中)
handleMouseMove ( event ) {
if ( ! this . isDragging ) return
event . preventDefault ( )
// 计算移动距离(考虑缩放影响)
const currentX = event . clientX
const currentY = event . clientY
const moveX = ( currentX - this . lastX ) / this . scale
const moveY = ( currentY - this . lastY ) / this . scale
// 更新偏移量
this . offsetX += moveX
this . offsetY += moveY
// 更新上一次位置
this . lastX = currentX
this . lastY = currentY
// 应用变换
this . applyTransform ( )
} ,
// 鼠标松开(结束拖动)
handleMouseUp ( ) {
this . isDragging = false
this . canvas . style . cursor = 'default'
} ,
// 鼠标滚动缩放
handleZoom ( event ) {
event . preventDefault ( )
const delta = event . deltaY
const step = delta > 0 ? - 1 : 1
this . zoom ( step )
} ,
zoomIn ( ) {
if ( this . scale < 2 ) {
this . zoom ( 8 ) // 每次放大8%
}
} ,
zoomOut ( ) {
if ( this . scale > 0.02 ) {
this . zoom ( - 8 ) // 每次缩小8%
}
} ,
initWebSocket ( ) {
this . loading = this . $loading ( {
lock : true ,
spinner : 'el-icon-loading' ,
background : 'rgba(0, 0, 0, 0.6)'
} )
const protocol = this . serverUrl . startsWith ( 'https' ) ? 'wss' : 'ws'
const wsHost = this . serverUrl . replace ( /^https?:\/\// , '' )
this . closeWebSocket ( )
this . websocket = new WebSocket ( ` ${ protocol } :// ${ wsHost } /webSocket/PointCloudData/ ${ this . userRole } ` )
this . websocket . onopen = ( ) => { }
this . websocket . onmessage = ( event ) => {
try {
const res = JSON . parse ( event . data )
this . greenPoints = res
this . loading . close ( )
} catch ( error ) {
console . error ( 'WebSocket消息解析失败:' , error )
}
}
this . websocket . onerror = ( error ) => {
this . $message . error ( this . $t ( 'WebSocketerror' ) + ':' , error )
}
this . websocket . onclose = ( ) => {
this . loading . close ( )
}
} ,
closeWebSocket ( ) {
if ( this . websocket ) {
this . websocket . close ( 1000 , '正常关闭' )
this . websocket = null
}
} ,
// 点击重定位:锁定小车位置,初始化拖拽起点
handleRelocate ( ) {
this . greenPoints = [ ]
this . greenPoints = points . data
// this.initWebSocket()
if ( typeof this . carPosition ? . x !== 'number' || typeof this . carPosition ? . y !== 'number' || ! this . mapData ) {
return
}
// 锁定小车( 不再响应carPosition变化)
this . isCarLocked = true
} ,
// 开始拖拽小车
startCarDrag ( e ) {
if ( ! this . isCarLocked ) return // 未锁定时不允许拖拽
this . isDraggingCar = true
// 初始化拖拽起点
const clientX = e . clientX || e . touches [ 0 ] . clientX
const clientY = e . clientY || e . touches [ 0 ] . clientY
// 将 clientX 转换为 SVG 坐标系中的 carX
const svgPoint = this . clientToSvg ( clientX , clientY )
// 记录拖拽开始的偏移量
this . dragStartX = svgPoint . x - this . carX
this . dragStartY = svgPoint . y - this . carY
const options = { passive : false }
document . addEventListener ( 'mousemove' , this . onCarDragMove , options )
document . addEventListener ( 'mouseup' , this . endCarDrag , options )
document . addEventListener ( 'touchmove' , this . onCarDragMove , options )
document . addEventListener ( 'touchend' , this . endCarDrag , options )
e . preventDefault ( )
} ,
// 拖拽小车移动
onCarDragMove : throttle ( function ( e ) {
if ( ! this . isDraggingCar || ! this . isCarLocked ) return
e . preventDefault ( )
const clientX = e . clientX || e . touches [ 0 ] . clientX
const clientY = e . clientY || e . touches [ 0 ] . clientY
// 转换为 SVG 坐标
const svgPoint = this . clientToSvg ( clientX , clientY )
// 计算新的车辆位置(减去偏移量)
this . carX = svgPoint . x - this . dragStartX
this . carY = svgPoint . y - this . dragStartY
} , 16 ) ,
// 结束拖拽小车
endCarDrag ( ) {
if ( this . isDraggingCar ) {
this . isDraggingCar = false
const options = { passive : false }
document . removeEventListener ( 'mousemove' , this . onCarDragMove , options )
document . removeEventListener ( 'mouseup' , this . endCarDrag , options )
document . removeEventListener ( 'touchmove' , this . onCarDragMove , options )
document . removeEventListener ( 'touchend' , this . endCarDrag , options )
const mapCoord = this . svgToMapCoordinate ( this . carX + 15 , this . carY + 15 )
console . log ( mapCoord )
}
} ,
// 将 clientX 转换为 SVG 坐标系
clientToSvg ( clientX , clientY ) {
const svg = this . $refs . mapSvg
const pt = svg . createSVGPoint ( )
pt . x = clientX
pt . y = clientY
// 使用 SVG 的变换矩阵将屏幕坐标转换为 SVG 内部坐标
return pt . matrixTransform ( svg . getScreenCTM ( ) . inverse ( ) )
} ,
// 将 SVG 坐标转换为 clientX
svgToClient ( svgX , svgY ) {
const svg = this . $refs . mapSvg
const pt = svg . createSVGPoint ( )
pt . x = svgX
pt . y = svgY
// 将 SVG 坐标转换为屏幕坐标
return pt . matrixTransform ( svg . getScreenCTM ( ) )
} ,
// 将 SVG 坐标 carX 转换回地图坐标系
svgToMapCoordinate ( svgX , svgY ) {
if ( ! this . mapData ) return null
// 反向计算您的原始公式
const mapX = ( svgX / this . mapScale ) * this . mapData . resolution + this . mapData . x
const mapY = this . mapData . y + ( this . mapData . height - ( svgY / this . mapScale ) ) * this . mapData . resolution
return { x : mapX , y : mapY }
} ,
convertToSvgX ( x ) {
if ( ! this . mapData || ! this . mapScale ) return 0
const svgX = ( ( x - this . mapData . x ) / this . mapData . resolution ) * this . mapScale
return svgX
} ,
convertToSvgY ( y ) {
if ( ! this . mapData || ! this . mapScale ) return 0
const svgY = ( this . mapData . height - ( y - this . mapData . y ) / this . mapData . resolution ) * this . mapScale
return svgY
}
}
}
< / script >
< style lang = "stylus" scoped >
. canvas - container
position relative
height 100 %
background - color rgba ( 4 , 33 , 58 , 70 % )
box - shadow inset 1 px 1 px 7 px 2 px # 4 d9bcd
overflow hidden
. mapCanvas , . mapSvg
position absolute
top 0
left 0
width 100 %
height 100 %
. mapSvg
pointer - events none
z - index 10
image
pointer - events auto
. point - popup
position fixed
background rgba ( 0 , 0 , 0 , 70 % )
border 1 px solid rgba ( 255 , 255 , 255 , .3 )
border - radius : 8 px ;
padding 10 px
box - shadow 0 0 px 4 px 2 px rgba ( 255 , 255 , 255 , 0.1 )
z - index 100
min - width 160 px
animation fadeIn 0.2 s ease - out
h3
color # fff
font - size 14 px
line - height 24 px
text - align center
p
color # fff
font - size 14 px
line - height 24 px
text - align center
@ keyframes fadeIn {
from { opacity : 0 ; transform : translateY ( 5 px ) ; }
to { opacity : 1 ; transform : translateY ( 0 ) ; }
}
. enClass
. point - popup
min - width 240 px
< / style >