编辑地图 实时地图优化
This commit is contained in:
parent
d8c5e5a3f9
commit
587f299f38
@ -738,7 +738,7 @@ onBeforeUnmount(() => {
|
||||
.map-box-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 370px);
|
||||
overflow-x: hidden;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin; /* Firefox */
|
||||
scrollbar-color: #888 #f1f1f1;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2073,22 +2073,27 @@ const startFromPoint = (index, event) => {
|
||||
if (toolbarSwitchType.value === 'clickDrawRoute') {
|
||||
let list = state.allMapPointInfo
|
||||
const point = list[index]
|
||||
if (point.id) {
|
||||
state.startDrawPoint = point //开始点
|
||||
if (point?.id) {
|
||||
state.startDrawPoint = { ...point } //创建点位数据副本
|
||||
state.startDrawPointIndex = index
|
||||
state.isDrawing = true
|
||||
event.preventDefault() // 防止默认行为
|
||||
event.preventDefault()
|
||||
} else {
|
||||
message.warning('选择的节点未保存')
|
||||
// 重置状态
|
||||
state.startDrawPointIndex = -1 // 起始点的索引
|
||||
state.startDrawPoint = null
|
||||
state.isDrawing = false
|
||||
state.currentDrawX = 0
|
||||
state.currentDrawY = 0
|
||||
resetDrawState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重置绘制状态
|
||||
const resetDrawState = () => {
|
||||
state.startDrawPointIndex = -1
|
||||
state.startDrawPoint = null
|
||||
state.isDrawing = false
|
||||
state.currentDrawX = 0
|
||||
state.currentDrawY = 0
|
||||
}
|
||||
|
||||
//开始框选绘制
|
||||
const startDrawSelection = (event) => {
|
||||
if (
|
||||
@ -2146,6 +2151,71 @@ const updateDrawSelection = (event) => {
|
||||
}
|
||||
//结束框选绘制
|
||||
const endDrawSelection = (event) => {
|
||||
if (toolbarSwitchType.value === 'clickDrawRoute') {
|
||||
if (state.isDrawing) {
|
||||
const endPointIndex = findClosestPoint(state.currentDrawX, state.currentDrawY)
|
||||
|
||||
if (endPointIndex !== null && endPointIndex !== state.startDrawPointIndex) {
|
||||
let list = state.allMapPointInfo
|
||||
const endPoint = list[endPointIndex]
|
||||
|
||||
if (!endPoint?.id) {
|
||||
message.warning('选择的节点未保存')
|
||||
resetDrawState()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查路线是否重复
|
||||
if (checkRouteDuplicate(state.startDrawPoint, endPoint, state.mapRouteList)) {
|
||||
message.warning('此路线已存在')
|
||||
} else {
|
||||
// 创建新路线对象
|
||||
const newRoute = {
|
||||
isSelected: false,
|
||||
startingPointId: state.startDrawPoint.id,
|
||||
endPointId: endPoint.id,
|
||||
startPointX: state.startDrawPoint.locationX,
|
||||
startPointY: state.startDrawPoint.locationY,
|
||||
endPointX: endPoint.locationX,
|
||||
endPointY: endPoint.locationY,
|
||||
actualStartPointX: state.startDrawPoint.actualLocationX,
|
||||
actualStartPointY: state.startDrawPoint.actualLocationY,
|
||||
actualEndPointX: endPoint.actualLocationX,
|
||||
actualEndPointY: endPoint.actualLocationY,
|
||||
actualBeginControlX: '',
|
||||
actualBeginControlY: '',
|
||||
actualEndControlX: '',
|
||||
actualEndControlY: '',
|
||||
beginControlX: 0,
|
||||
beginControlY: 0,
|
||||
endControlX: 0,
|
||||
endControlY: 0,
|
||||
expansionZoneFront: 0,
|
||||
expansionZoneAfter: 0,
|
||||
expansionZoneLeft: 0,
|
||||
expansionZoneRight: 0,
|
||||
method: 0,
|
||||
direction: 2,
|
||||
forwardSpeedLimit: 1.5,
|
||||
reverseSpeedLimit: 0.4,
|
||||
toward: 0,
|
||||
beginWidth: state.startDrawPoint.locationWidePx,
|
||||
beginHigh: state.startDrawPoint.locationDeepPx,
|
||||
endWidth: endPoint.locationWidePx,
|
||||
endHigh: endPoint.locationDeepPx,
|
||||
startingSortNum: state.startDrawPoint.sortNum,
|
||||
endPointSortNum: endPoint.sortNum
|
||||
}
|
||||
|
||||
state.mapRouteList.push(newRoute)
|
||||
addEditHistory()
|
||||
}
|
||||
}
|
||||
resetDrawState()
|
||||
}
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
if (
|
||||
toolbarSwitchType.value === 'createLineLibrary' ||
|
||||
toolbarSwitchType.value === 'createRegion' ||
|
||||
@ -2159,112 +2229,37 @@ const endDrawSelection = (event) => {
|
||||
event.preventDefault() // 阻止默认行为(避免选中图片或文本)
|
||||
return
|
||||
}
|
||||
if (toolbarSwitchType.value === 'clickDrawRoute') {
|
||||
if (state.isDrawing) {
|
||||
// 找到最近的终点
|
||||
const endPointIndex = findClosestPoint(state.currentDrawX, state.currentDrawY)
|
||||
|
||||
if (endPointIndex !== null && endPointIndex !== state.startDrawPointIndex) {
|
||||
let list = state.allMapPointInfo
|
||||
const endPoint = list[endPointIndex]
|
||||
|
||||
//先判断节点有没有保存
|
||||
if (!endPoint.id) {
|
||||
message.warning('选择的节点未保存')
|
||||
// 重置状态
|
||||
state.startDrawPointIndex = -1 // 起始点的索引
|
||||
state.startDrawPoint = null
|
||||
state.isDrawing = false
|
||||
state.currentDrawX = 0
|
||||
state.currentDrawY = 0
|
||||
return
|
||||
}
|
||||
|
||||
const newLine = {
|
||||
startPointX: state.startDrawPoint.locationX,
|
||||
startPointY: state.startDrawPoint.locationY,
|
||||
endPointX: endPoint.locationX,
|
||||
endPointY: endPoint.locationY
|
||||
}
|
||||
// 检查是否已存在相同的直线
|
||||
const isDuplicate = state.mapRouteList.some(
|
||||
(line) =>
|
||||
(line.startPointX === newLine.startPointX &&
|
||||
line.startPointY === newLine.startPointY &&
|
||||
line.endPointX === newLine.endPointX &&
|
||||
line.endPointY === newLine.endPointY) ||
|
||||
(line.startPointX === newLine.endPointX &&
|
||||
line.startPointY === newLine.endPointY &&
|
||||
line.endPointX === newLine.startPointX &&
|
||||
line.endPointY === newLine.startPointY)
|
||||
)
|
||||
if (isDuplicate) {
|
||||
message.warning('此路线已存在')
|
||||
} else {
|
||||
// 保存当前直线
|
||||
state.mapRouteList.push({
|
||||
isSelected: false, //是否选中
|
||||
startingPointId: state.startDrawPoint.id,
|
||||
endPointId: endPoint.id,
|
||||
startPointX: state.startDrawPoint.locationX, //开始点
|
||||
startPointY: state.startDrawPoint.locationY, //开始点
|
||||
endPointX: endPoint.locationX, //结束点
|
||||
endPointY: endPoint.locationY, //结束点
|
||||
actualStartPointX: state.startDrawPoint.actualLocationX, //实际开始点位x轴
|
||||
actualStartPointY: state.startDrawPoint.actualLocationY, //实际开始点位y轴
|
||||
actualEndPointX: endPoint.actualLocationX, //实际结束点位x轴
|
||||
actualEndPointY: endPoint.actualLocationY, //实际结束点位y轴
|
||||
actualBeginControlX: '', //实际开始控制点x轴
|
||||
actualBeginControlY: '', //实际开始控制点y轴
|
||||
actualEndControlX: '', //实际结束控制点x轴
|
||||
actualEndControlY: '', //实际结束控制点y轴
|
||||
beginControlX: 0, //开始控制点x轴
|
||||
beginControlY: 0, //开始控制点y轴
|
||||
endControlX: 0, //结束控制点x轴
|
||||
endControlY: 0, //结束控制点y轴
|
||||
expansionZoneFront: 0, //膨胀区域前
|
||||
expansionZoneAfter: 0, //膨胀区域后
|
||||
expansionZoneLeft: 0, // 膨胀区域左
|
||||
expansionZoneRight: 0, //膨胀区域右
|
||||
method: 0, //行走方法 0.直线 1.上左曲线2.上右曲线3.下左曲线 4.下右曲线
|
||||
direction: 2, //方向 1.单向 2.双向,
|
||||
forwardSpeedLimit: 1.5, //正向限速
|
||||
reverseSpeedLimit: 0.4, // 反向限速
|
||||
toward: 0, // 车头朝向( 0:正正 1:正反 2:反正 3:反反)
|
||||
beginWidth: state.startDrawPoint.locationWidePx, //起点宽
|
||||
beginHigh: state.startDrawPoint.locationDeepPx, // 起点高
|
||||
endWidth: endPoint.locationWidePx, // 终点宽
|
||||
endHigh: endPoint.locationDeepPx, // 终点高
|
||||
startingSortNum: state.startDrawPoint.sortNum,
|
||||
endPointSortNum: endPoint.sortNum
|
||||
})
|
||||
addEditHistory()
|
||||
}
|
||||
}
|
||||
// 重置状态
|
||||
state.startDrawPointIndex = -1 // 起始点的索引
|
||||
state.startDrawPoint = null
|
||||
state.isDrawing = false
|
||||
state.currentDrawX = 0
|
||||
state.currentDrawY = 0
|
||||
}
|
||||
event.preventDefault() // 阻止默认行为(避免选中图片或文本)
|
||||
return
|
||||
}
|
||||
}
|
||||
// 找到最近的点
|
||||
const findClosestPoint = (x, y) => {
|
||||
const list = state.allMapPointInfo
|
||||
if (!Array.isArray(list) || list.length === 0) return null
|
||||
|
||||
const searchRadius = 100
|
||||
let minDistance = Infinity
|
||||
let closestIndex = null
|
||||
let list = state.allMapPointInfo
|
||||
list.forEach((point, index) => {
|
||||
const distance = Math.sqrt((point.locationX - x) ** 2 + (point.locationY - y) ** 2)
|
||||
if (distance < minDistance && distance < point.locationWide) {
|
||||
// 10 是点的捕捉范围
|
||||
|
||||
// 使用空间分区优化:先进行粗略筛选
|
||||
const potentialPoints = list.filter((point, index) => {
|
||||
if (!point?.locationX || !point?.locationY) return false
|
||||
const dx = Math.abs(point.locationX - x)
|
||||
const dy = Math.abs(point.locationY - y)
|
||||
return dx <= searchRadius && dy <= searchRadius
|
||||
})
|
||||
|
||||
// 在筛选后的点中找最近的
|
||||
potentialPoints.forEach((point, i) => {
|
||||
const dx = point.locationX - x
|
||||
const dy = point.locationY - y
|
||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||
const captureRadius = point.locationWide || 10
|
||||
|
||||
if (distance < minDistance && distance < captureRadius) {
|
||||
minDistance = distance
|
||||
closestIndex = index
|
||||
closestIndex = list.findIndex((p) => p.id === point.id)
|
||||
}
|
||||
})
|
||||
|
||||
return closestIndex
|
||||
}
|
||||
//点击区域
|
||||
@ -3565,6 +3560,17 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
||||
// 检查路线是否重复的函数
|
||||
const checkRouteDuplicate = (startPoint, endPoint, routeList) => {
|
||||
if (!startPoint?.id || !endPoint?.id) return false
|
||||
|
||||
return routeList.some(
|
||||
(route) =>
|
||||
(route.startingPointId === startPoint.id && route.endPointId === endPoint.id) ||
|
||||
(route.startingPointId === endPoint.id && route.endPointId === startPoint.id)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -204,7 +204,10 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.main-content {
|
||||
box-sizing: border-box;
|
||||
margin-top: 62px;
|
||||
height: calc(100vh - 120px) !important;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,474 +1,105 @@
|
||||
<template>
|
||||
<div class="fence-container">
|
||||
<div class="svg-container" ref="svgContainer">
|
||||
<svg
|
||||
ref="fenceSvg"
|
||||
@click="handleSvgClick"
|
||||
@mousedown="startPan"
|
||||
@mousemove="handleMouseMove"
|
||||
@mouseup="endPan"
|
||||
@mouseleave="endPan"
|
||||
@wheel="handleWheel"
|
||||
:viewBox="viewBox"
|
||||
preserveAspectRatio="none"
|
||||
<div class="map-container">
|
||||
<div
|
||||
ref="fullscreenElement"
|
||||
class="fullscreen-content"
|
||||
:class="{ 'is-fullscreen': isFullscreen }"
|
||||
>
|
||||
<div
|
||||
v-for="car in cars"
|
||||
:key="car.id"
|
||||
class="car"
|
||||
:style="{ left: car.x + 'px', top: car.y + 'px' }"
|
||||
>
|
||||
<!-- 网格背景 -->
|
||||
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
|
||||
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#eee" stroke-width="0.5" />
|
||||
</pattern>
|
||||
<rect width="100%" height="100%" fill="url(#grid)" />
|
||||
|
||||
<!-- 围栏多边形 -->
|
||||
<polygon
|
||||
v-if="fencePoints.length > 2"
|
||||
:points="fencePointsString"
|
||||
fill="rgba(231, 76, 60, 0.2)"
|
||||
stroke="#e74c3c"
|
||||
stroke-width="2"
|
||||
/>
|
||||
|
||||
<!-- 绘制中的线段 -->
|
||||
<polyline
|
||||
v-if="isDrawing && currentPoints.length > 0"
|
||||
:points="currentPointsString"
|
||||
fill="none"
|
||||
stroke="#3498db"
|
||||
stroke-width="2"
|
||||
/>
|
||||
|
||||
<!-- 顶点标记 -->
|
||||
<circle
|
||||
v-for="(point, index) in allPoints"
|
||||
:key="index"
|
||||
:cx="point.x"
|
||||
:cy="point.y"
|
||||
r="3"
|
||||
fill="#3498db"
|
||||
@mousedown.stop="startDrag(index, $event)"
|
||||
/>
|
||||
|
||||
<!-- 测试点 -->
|
||||
<circle
|
||||
v-if="testPoint"
|
||||
:cx="testPoint.x"
|
||||
:cy="testPoint.y"
|
||||
r="5"
|
||||
:fill="isInside ? '#2ecc71' : '#e74c3c'"
|
||||
/>
|
||||
|
||||
<!-- 坐标显示 -->
|
||||
<text x="10" y="20" font-size="12" fill="#333" v-if="cursorPosition">
|
||||
X: {{ cursorPosition.x.toFixed(1) }}, Y: {{ cursorPosition.y.toFixed(1) }}
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<h3>绘制控制</h3>
|
||||
<button @click="startDrawing">开始绘制</button>
|
||||
<button @click="stopDrawing">结束绘制</button>
|
||||
<button @click="clearDrawing">清空</button>
|
||||
<button @click="checkRandomPoint">检测随机点</button>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<h3>视图控制</h3>
|
||||
<button @click="zoomIn">放大</button>
|
||||
<button @click="zoomOut">缩小</button>
|
||||
<button @click="resetView">重置视图</button>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<h3>数据管理</h3>
|
||||
<button @click="saveFence">保存围栏</button>
|
||||
<button @click="loadFence">加载围栏</button>
|
||||
<img src="@/assets/imgs/indexPage/chache-4备份 7@2x.png" alt="car" class="car-image" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const svgContainer = ref(null)
|
||||
const fenceSvg = ref(null)
|
||||
const isDrawing = ref(false)
|
||||
const fencePoints = ref([])
|
||||
const currentPoints = ref([])
|
||||
const testPoint = ref(null)
|
||||
const isInside = ref(false)
|
||||
const draggingIndex = ref(null)
|
||||
const initialPos = ref(null)
|
||||
const viewBox = ref('0 0 1000 600')
|
||||
const svgSize = ref({ width: 1000, height: 600 })
|
||||
const isPanning = ref(false)
|
||||
const panStart = ref({ x: 0, y: 0 })
|
||||
const cursorPosition = ref(null)
|
||||
const fullscreenElement = ref(null)
|
||||
const isFullscreen = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const fencePointsString = computed(() => {
|
||||
return fencePoints.value.map((p) => `${p.x},${p.y}`).join(' ')
|
||||
})
|
||||
// 创建20辆车的数据
|
||||
const cars = ref(
|
||||
Array.from({ length: 20 }, (_, index) => ({
|
||||
id: index + 1,
|
||||
x: Math.random() * 800, // 初始x坐标
|
||||
y: Math.random() * 600, // 初始y坐标
|
||||
dx: (Math.random() - 0.5) * 6, // x方向速度
|
||||
dy: (Math.random() - 0.5) * 6 // y方向速度
|
||||
}))
|
||||
)
|
||||
|
||||
const currentPointsString = computed(() => {
|
||||
return currentPoints.value.map((p) => `${p.x},${p.y}`).join(' ')
|
||||
})
|
||||
// 更新车辆位置
|
||||
let updateInterval
|
||||
const updateCarPositions = () => {
|
||||
cars.value = cars.value.map((car) => {
|
||||
// 更新位置
|
||||
let newX = car.x + car.dx
|
||||
let newY = car.y + car.dy
|
||||
|
||||
const allPoints = computed(() => {
|
||||
return isDrawing.value ? [...fencePoints.value, ...currentPoints.value] : fencePoints.value
|
||||
})
|
||||
// 边界检查
|
||||
if (newX <= 0 || newX >= 1200) car.dx *= -1
|
||||
if (newY <= 0 || newY >= 800) car.dy *= -1
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
updateSvgSize()
|
||||
window.addEventListener('resize', updateSvgSize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateSvgSize)
|
||||
})
|
||||
|
||||
function updateSvgSize() {
|
||||
const container = svgContainer.value
|
||||
svgSize.value = {
|
||||
width: container.clientWidth,
|
||||
height: container.clientHeight
|
||||
}
|
||||
fenceSvg.value.setAttribute('viewBox', viewBox.value)
|
||||
}
|
||||
|
||||
// 绘制控制
|
||||
const startDrawing = () => {
|
||||
fencePoints.value = []
|
||||
currentPoints.value = []
|
||||
isDrawing.value = true
|
||||
testPoint.value = null
|
||||
}
|
||||
|
||||
const stopDrawing = () => {
|
||||
if (currentPoints.value.length > 0) {
|
||||
fencePoints.value = [...fencePoints.value, ...currentPoints.value]
|
||||
currentPoints.value = []
|
||||
}
|
||||
|
||||
if (fencePoints.value.length > 2) {
|
||||
// 确保多边形闭合
|
||||
if (
|
||||
fencePoints.value[0].x !== fencePoints.value[fencePoints.value.length - 1].x ||
|
||||
fencePoints.value[0].y !== fencePoints.value[fencePoints.value.length - 1].y
|
||||
) {
|
||||
fencePoints.value.push({ ...fencePoints.value[0] })
|
||||
return {
|
||||
...car,
|
||||
x: newX,
|
||||
y: newY
|
||||
}
|
||||
}
|
||||
|
||||
isDrawing.value = false
|
||||
}
|
||||
|
||||
const clearDrawing = () => {
|
||||
fencePoints.value = []
|
||||
currentPoints.value = []
|
||||
isDrawing.value = false
|
||||
testPoint.value = null
|
||||
}
|
||||
|
||||
const handleSvgClick = (e) => {
|
||||
if (!isDrawing.value || isPanning.value) return
|
||||
|
||||
const svg = fenceSvg.value
|
||||
const pt = svg.createSVGPoint()
|
||||
pt.x = e.clientX
|
||||
pt.y = e.clientY
|
||||
const cursorPt = pt.matrixTransform(svg.getScreenCTM().inverse())
|
||||
|
||||
currentPoints.value.push({
|
||||
x: cursorPt.x,
|
||||
y: cursorPt.y
|
||||
})
|
||||
}
|
||||
|
||||
// 点检测
|
||||
const checkRandomPoint = () => {
|
||||
if (fencePoints.value.length < 3) {
|
||||
alert('请先绘制电子围栏')
|
||||
return
|
||||
onMounted(() => {
|
||||
// 每100毫秒更新一次,即每秒10次
|
||||
updateInterval = setInterval(updateCarPositions, 200)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清除定时器
|
||||
if (updateInterval) {
|
||||
clearInterval(updateInterval)
|
||||
}
|
||||
|
||||
const [vbX, vbY, vbWidth, vbHeight] = viewBox.value.split(' ').map(Number)
|
||||
|
||||
testPoint.value = {
|
||||
x: vbX + Math.random() * vbWidth,
|
||||
y: vbY + Math.random() * vbHeight
|
||||
}
|
||||
|
||||
isInside.value = pointInPolygon(testPoint.value, fencePoints.value)
|
||||
}
|
||||
|
||||
function pointInPolygon(point, polygon) {
|
||||
const x = point.x
|
||||
const y = point.y
|
||||
let inside = false
|
||||
|
||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||
const xi = polygon[i].x
|
||||
const yi = polygon[i].y
|
||||
const xj = polygon[j].x
|
||||
const yj = polygon[j].y
|
||||
|
||||
const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi
|
||||
if (intersect) inside = !inside
|
||||
}
|
||||
|
||||
return inside
|
||||
}
|
||||
|
||||
// 顶点拖拽
|
||||
const startDrag = (index, e) => {
|
||||
e.preventDefault()
|
||||
draggingIndex.value = index
|
||||
|
||||
const svg = fenceSvg.value
|
||||
const pt = svg.createSVGPoint()
|
||||
pt.x = e.clientX
|
||||
pt.y = e.clientY
|
||||
const cursorPt = pt.matrixTransform(svg.getScreenCTM().inverse())
|
||||
|
||||
initialPos.value = {
|
||||
x: cursorPt.x,
|
||||
y: cursorPt.y,
|
||||
index
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', handleDrag)
|
||||
document.addEventListener('mouseup', stopDrag)
|
||||
}
|
||||
|
||||
const handleDrag = (e) => {
|
||||
if (draggingIndex.value === null) return
|
||||
|
||||
const svg = fenceSvg.value
|
||||
const pt = svg.createSVGPoint()
|
||||
pt.x = e.clientX
|
||||
pt.y = e.clientY
|
||||
const cursorPt = pt.matrixTransform(svg.getScreenCTM().inverse())
|
||||
|
||||
const points =
|
||||
isDrawing.value && draggingIndex.value >= fencePoints.value.length
|
||||
? currentPoints.value
|
||||
: fencePoints.value
|
||||
|
||||
const actualIndex =
|
||||
isDrawing.value && draggingIndex.value >= fencePoints.value.length
|
||||
? draggingIndex.value - fencePoints.value.length
|
||||
: draggingIndex.value
|
||||
|
||||
points[actualIndex] = {
|
||||
x: cursorPt.x,
|
||||
y: cursorPt.y
|
||||
}
|
||||
|
||||
// 如果是闭合点,同步第一个点
|
||||
if (
|
||||
!isDrawing.value &&
|
||||
fencePoints.value.length > 0 &&
|
||||
draggingIndex.value === fencePoints.value.length - 1
|
||||
) {
|
||||
fencePoints.value[0] = { ...fencePoints.value[fencePoints.value.length - 1] }
|
||||
}
|
||||
}
|
||||
|
||||
const stopDrag = () => {
|
||||
draggingIndex.value = null
|
||||
initialPos.value = null
|
||||
document.removeEventListener('mousemove', handleDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
}
|
||||
|
||||
// 视图控制
|
||||
const handleWheel = (e) => {
|
||||
e.preventDefault()
|
||||
const delta = e.deltaY > 0 ? 0.9 : 1.1
|
||||
zoomAt(e.clientX, e.clientY, delta)
|
||||
}
|
||||
|
||||
function zoomAt(clientX, clientY, factor) {
|
||||
const container = svgContainer.value
|
||||
const rect = container.getBoundingClientRect()
|
||||
const x = ((clientX - rect.left) / rect.width) * svgSize.value.width
|
||||
const y = ((clientY - rect.top) / rect.height) * svgSize.value.height
|
||||
|
||||
const [vbX, vbY, vbWidth, vbHeight] = viewBox.value.split(' ').map(Number)
|
||||
|
||||
const newWidth = vbWidth * factor
|
||||
const newHeight = vbHeight * factor
|
||||
const newX = vbX + (x - vbX) * (1 - factor)
|
||||
const newY = vbY + (y - vbY) * (1 - factor)
|
||||
|
||||
// 限制缩放范围
|
||||
if (newWidth > svgSize.value.width * 10 || newWidth < svgSize.value.width / 10) return
|
||||
|
||||
viewBox.value = `${newX} ${newY} ${newWidth} ${newHeight}`
|
||||
fenceSvg.value.setAttribute('viewBox', viewBox.value)
|
||||
}
|
||||
|
||||
const zoomIn = () => {
|
||||
const container = svgContainer.value
|
||||
const rect = container.getBoundingClientRect()
|
||||
zoomAt(rect.left + rect.width / 2, rect.top + rect.height / 2, 1.2)
|
||||
}
|
||||
|
||||
const zoomOut = () => {
|
||||
const container = svgContainer.value
|
||||
const rect = container.getBoundingClientRect()
|
||||
zoomAt(rect.left + rect.width / 2, rect.top + rect.height / 2, 0.8)
|
||||
}
|
||||
|
||||
const resetView = () => {
|
||||
viewBox.value = `0 0 ${svgSize.value.width} ${svgSize.value.height}`
|
||||
fenceSvg.value.setAttribute('viewBox', viewBox.value)
|
||||
}
|
||||
|
||||
const startPan = (e) => {
|
||||
if (draggingIndex.value !== null) return
|
||||
isPanning.value = true
|
||||
panStart.value = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
}
|
||||
fenceSvg.value.style.cursor = 'grabbing'
|
||||
}
|
||||
|
||||
const handlePan = (e) => {
|
||||
if (!isPanning.value) return
|
||||
|
||||
const [vbX, vbY, vbWidth, vbHeight] = viewBox.value.split(' ').map(Number)
|
||||
const dx = ((e.clientX - panStart.value.x) / svgSize.value.width) * vbWidth
|
||||
const dy = ((e.clientY - panStart.value.y) / svgSize.value.height) * vbHeight
|
||||
|
||||
viewBox.value = `${vbX - dx} ${vbY - dy} ${vbWidth} ${vbHeight}`
|
||||
fenceSvg.value.setAttribute('viewBox', viewBox.value)
|
||||
|
||||
panStart.value = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
}
|
||||
}
|
||||
|
||||
const endPan = () => {
|
||||
isPanning.value = false
|
||||
fenceSvg.value.style.cursor = 'crosshair'
|
||||
}
|
||||
|
||||
// 坐标显示
|
||||
const handleMouseMove = (e) => {
|
||||
const svg = fenceSvg.value
|
||||
const pt = svg.createSVGPoint()
|
||||
pt.x = e.clientX
|
||||
pt.y = e.clientY
|
||||
const cursorPt = pt.matrixTransform(svg.getScreenCTM().inverse())
|
||||
|
||||
cursorPosition.value = {
|
||||
x: cursorPt.x,
|
||||
y: cursorPt.y
|
||||
}
|
||||
}
|
||||
|
||||
// 数据持久化
|
||||
const saveFence = () => {
|
||||
if (fencePoints.value.length < 3) {
|
||||
alert('请先绘制有效的电子围栏')
|
||||
return
|
||||
}
|
||||
|
||||
const data = {
|
||||
points: fencePoints.value,
|
||||
viewBox: viewBox.value,
|
||||
svgSize: svgSize.value
|
||||
}
|
||||
|
||||
localStorage.setItem('fenceData', JSON.stringify(data))
|
||||
alert('围栏已保存')
|
||||
}
|
||||
|
||||
const loadFence = () => {
|
||||
const data = localStorage.getItem('fenceData')
|
||||
if (data) {
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
fencePoints.value = parsed.points
|
||||
viewBox.value = parsed.viewBox || `0 0 ${svgSize.value.width} ${svgSize.value.height}`
|
||||
svgSize.value = parsed.svgSize || { width: 1000, height: 600 }
|
||||
|
||||
fenceSvg.value.setAttribute('viewBox', viewBox.value)
|
||||
alert('围栏已加载')
|
||||
} catch (e) {
|
||||
alert('加载围栏数据失败')
|
||||
}
|
||||
} else {
|
||||
alert('没有找到保存的围栏数据')
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fence-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
.map-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.svg-container {
|
||||
flex: 1;
|
||||
.fullscreen-content {
|
||||
width: 1200px;
|
||||
height: 800px;
|
||||
background-color: #f0f0f0;
|
||||
position: relative;
|
||||
border: 1px solid #ccc;
|
||||
background: #f9f9f9;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
svg {
|
||||
.car {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.car-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: crosshair;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
svg.panning {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.controls {
|
||||
padding: 10px;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.control-group h3 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 12px;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
circle {
|
||||
cursor: move;
|
||||
/* 全屏模式下的样式 */
|
||||
.fullscreen-content:fullscreen,
|
||||
.fullscreen-content:-webkit-full-screen,
|
||||
.fullscreen-content:-moz-full-screen,
|
||||
.fullscreen-content:-ms-fullscreen {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,115 +1,117 @@
|
||||
// websocket.js
|
||||
class WebSocketClient {
|
||||
constructor(url) {
|
||||
this.currentUrl = url;
|
||||
this.socket = null;
|
||||
this.heartbeatInterval = null;
|
||||
this.messageCallback = null;
|
||||
this.reconnectTimer = null;
|
||||
this.reconnectAttempts = 0;
|
||||
this.MAX_RECONNECT_ATTEMPTS = 5;
|
||||
this.RECONNECT_DELAY = 5000; // 5 秒
|
||||
this.HEARTBEAT_INTERVAL = 30000; // 30 秒
|
||||
this.URL_CHECK_INTERVAL = 5000; // 每 5 秒检查一次 URL
|
||||
this.init();
|
||||
this.startUrlCheck();
|
||||
}
|
||||
constructor(url) {
|
||||
this.currentUrl = url
|
||||
this.socket = null
|
||||
this.heartbeatInterval = null
|
||||
this.messageCallback = null
|
||||
this.reconnectTimer = null
|
||||
this.reconnectAttempts = 0
|
||||
this.MAX_RECONNECT_ATTEMPTS = 5
|
||||
this.RECONNECT_DELAY = 5000 // 5 秒
|
||||
this.HEARTBEAT_INTERVAL = 30000 // 30 秒
|
||||
this.URL_CHECK_INTERVAL = 5000 // 每 5 秒检查一次 URL
|
||||
this.init()
|
||||
this.startUrlCheck()
|
||||
}
|
||||
|
||||
init() {
|
||||
try {
|
||||
console.log('尝试创建 WebSocket 连接:', this.currentUrl);
|
||||
this.socket = new WebSocket(this.currentUrl);
|
||||
init() {
|
||||
try {
|
||||
console.log('尝试创建 WebSocket 连接:', this.currentUrl)
|
||||
this.socket = new WebSocket(this.currentUrl)
|
||||
|
||||
this.socket.onopen = () => {
|
||||
console.log('WebSocket 连接已建立:', this.currentUrl);
|
||||
this.startHeartbeat();
|
||||
this.reconnectAttempts = 0;
|
||||
};
|
||||
this.socket.onopen = () => {
|
||||
console.log('WebSocket 连接已建立:', this.currentUrl)
|
||||
this.startHeartbeat()
|
||||
this.reconnectAttempts = 0
|
||||
}
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
if (this.messageCallback) {
|
||||
this.messageCallback(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.onclose = () => {
|
||||
console.log('WebSocket 连接已关闭:', this.currentUrl);
|
||||
this.stopHeartbeat();
|
||||
this.reconnect();
|
||||
};
|
||||
|
||||
this.socket.onerror = (error) => {
|
||||
console.error('WebSocket 发生错误:', error);
|
||||
this.socket.close();
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('创建 WebSocket 连接时出错:', error);
|
||||
this.socket.onmessage = (event) => {
|
||||
if (this.messageCallback) {
|
||||
this.messageCallback(event.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startHeartbeat() {
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send('ping');
|
||||
}
|
||||
}, this.HEARTBEAT_INTERVAL);
|
||||
}
|
||||
this.socket.onclose = () => {
|
||||
console.log('WebSocket 连接已关闭:', this.currentUrl)
|
||||
this.stopHeartbeat()
|
||||
this.reconnect()
|
||||
}
|
||||
|
||||
stopHeartbeat() {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMessage(callback) {
|
||||
this.messageCallback = callback;
|
||||
}
|
||||
|
||||
send(message) {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(message);
|
||||
} else {
|
||||
console.error('WebSocket 连接未打开,无法发送消息');
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.socket.onerror = (error) => {
|
||||
console.log('WebSocket 发生错误:', error)
|
||||
if (this.socket) {
|
||||
this.stopHeartbeat();
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
this.socket.close()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('创建 WebSocket 连接时出错:', error)
|
||||
}
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
if (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS) {
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
console.log('尝试重新连接...', this.currentUrl);
|
||||
this.reconnectAttempts++;
|
||||
this.init();
|
||||
}, this.RECONNECT_DELAY);
|
||||
} else {
|
||||
console.error('达到最大重连次数,停止重连');
|
||||
}
|
||||
}
|
||||
startHeartbeat() {
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send('ping')
|
||||
}
|
||||
}, this.HEARTBEAT_INTERVAL)
|
||||
}
|
||||
|
||||
startUrlCheck() {
|
||||
setInterval(() => {
|
||||
const newUrl = this.getUpdatedUrl();
|
||||
if (newUrl && newUrl!== this.currentUrl) {
|
||||
this.disconnect();
|
||||
this.currentUrl = newUrl;
|
||||
this.init();
|
||||
}
|
||||
}, this.URL_CHECK_INTERVAL);
|
||||
stopHeartbeat() {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval)
|
||||
this.heartbeatInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
// 这个方法需要根据实际情况重写,用于获取最新的 URL
|
||||
getUpdatedUrl() {
|
||||
// 这里只是示例,返回 null 表示没有更新的 URL
|
||||
return null;
|
||||
onMessage(callback) {
|
||||
this.messageCallback = callback
|
||||
}
|
||||
|
||||
send(message) {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(message)
|
||||
} else {
|
||||
console.log('WebSocket 连接未打开,无法发送消息')
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.stopHeartbeat()
|
||||
this.socket.close()
|
||||
this.socket = null
|
||||
}
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
if (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS) {
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
console.log('尝试重新连接...', this.currentUrl)
|
||||
this.reconnectAttempts++
|
||||
this.init()
|
||||
}, this.RECONNECT_DELAY)
|
||||
} else {
|
||||
console.log('达到最大重连次数,停止重连')
|
||||
}
|
||||
}
|
||||
|
||||
startUrlCheck() {
|
||||
setInterval(() => {
|
||||
const newUrl = this.getUpdatedUrl()
|
||||
if (newUrl && newUrl !== this.currentUrl) {
|
||||
this.disconnect()
|
||||
this.currentUrl = newUrl
|
||||
this.init()
|
||||
}
|
||||
}, this.URL_CHECK_INTERVAL)
|
||||
}
|
||||
|
||||
// 这个方法需要根据实际情况重写,用于获取最新的 URL
|
||||
getUpdatedUrl() {
|
||||
// 这里只是示例,返回 null 表示没有更新的 URL
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default WebSocketClient;
|
||||
export default WebSocketClient
|
||||
|
Loading…
Reference in New Issue
Block a user