加功能

This commit is contained in:
2025-09-16 18:01:49 +08:00
parent 3ac1d5f49c
commit 1a1ddd256f
14 changed files with 682 additions and 2710 deletions

View File

@@ -13,6 +13,7 @@ export const stopMapping = () => post('teaching/stopMapping', {})
export const getLocalMaps = () => post('teaching/getLocalMaps', {})
export const oneClickDeployment = (map) => post('teaching/oneClickDeployment?mapName=' + map, {})
export const abandonMapping = () => post('teaching/abandonMapping', {})
export const sendAutoBack = () => post('teaching/sendAutoBack', {})
// 操作
export const queryStation = () => post('api/operate/queryStation', {})

View File

@@ -192,4 +192,11 @@ export const queryErrorDataByCode = (code) => {
]
}
return res
}
export const setStation = (sn, code) => {
let res = {
code: 200,
message: 'ok'
}
return res
}

View File

@@ -113,5 +113,11 @@ module.exports = {
Syncsuccessfully: 'Sync successfully',
EnablePointCloud: 'Enable Point Cloud',
DisablePointCloud: 'Disable Point Cloud',
CartPosition: 'Cart Position'
CartPosition: 'Cart Position',
Whetherdrivebackpoint: 'Whether to automatically drive back to the previous point?',
yes: 'Yes',
no: 'No',
Dispatchnotconnected: 'Dispatch is not connected',
Vehiclenotconnected: 'Vehicle body is not connected',
Dispatchvehiclenotconnected: 'Dispatch and the vehicle body are not connected.'
}

View File

@@ -113,5 +113,11 @@ module.exports = {
Syncsuccessfully: '同步成功',
EnablePointCloud: '开启点云',
DisablePointCloud: '关闭点云',
CartPosition: '小车位置'
CartPosition: '小车位置',
Whetherdrivebackpoint: '是否自动开回上一个点?',
yes: '是',
no: '否',
Dispatchnotconnected: '调度未连接',
Vehiclenotconnected: '车辆本体未连接',
Dispatchvehiclenotconnected: '调度和车辆本体未连接'
}

View File

@@ -14,7 +14,7 @@
<el-col :span="8">
<el-row type="flex" justify="end">
<button class="button_control" @click="$router.push('/index/home')"><p>{{$t('AbandonMapbuild')}}</p></button>
<button id="v-step-3" class="button_control" style="margin-left: 10px" :disabled="disabled" @click="stopMappingConfirm"><p>{{$t('FinishMapbuild')}}</p></button>
<button id="v-step-3" class="button_control" :class="{'button_control_gray': autoLoopEnable !== '1'}" style="margin-left: 10px" :disabled="disabled" @click="stopMappingConfirm"><p>{{$t('FinishMapbuild')}}</p></button>
</el-row>
</el-col>
</el-row>
@@ -56,11 +56,11 @@
</template>
<script>
import GlMap from './gl-map-4.vue'
import GlMap from './gl-map.vue'
import { driver } from 'driver.js'
import 'driver.js/dist/driver.css'
// import { startMapping } from '../../config/mork.js'
import { startMapping, stopMapping, getMappingStatus, setStation, oneClickDeployment, abandonMapping } from '../../config/getData.js'
import { startMapping, stopMapping, getMappingStatus, setStation, oneClickDeployment, abandonMapping, sendAutoBack } from '../../config/getData.js'
import { mapGetters } from 'vuex'
export default {
name: 'ModuleBuilding',
@@ -137,11 +137,38 @@ export default {
showProgress: false,
warnTip: false,
percentage: 0,
intervalId: null // 用于存储定时器ID
intervalId: null, // 用于存储定时器ID
backActive: false // 打点完成后新增一个弹框 提示用户是否自动开回上一个点
}
},
computed: {
...mapGetters(['isTop', 'carPosition'])
...mapGetters(['isTop', 'carPosition', 'autoLoopEnable', 'autoBackEnable'])
},
watch: {
autoBackEnable (val) {
if (this.backActive && val === '0') {
this.loading = this.$loading({
lock: true,
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.6)'
})
}
if (this.backActive && val === '1') {
this.loading.close()
}
if (this.backActive && val === '2') {
this.$confirm(this.$t('Whetherdrivebackpoint'), this.$t('Prompt'), {
confirmButtonText: this.$t('yes'),
cancelButtonText: this.$t('no'),
type: 'warning'
}).then(() => {
this._sendAutoBack()
this.backActive = false
}).catch(() => {
this.backActive = false
})
}
},
},
beforeDestroy () {
document.removeEventListener('keydown', this.handleKeydown);
@@ -297,8 +324,10 @@ export default {
message: res.message
})
this.keyPoints.push(this.dataForm.stationCode)
this.backActive = true
} else {
this.$message.error(res.message)
this.backActive = false
}
}
this.dialogVisible = false
@@ -307,9 +336,11 @@ export default {
this.$message.error(e)
this.dialogVisible = false
this.disabled = false
this.backActive = false
}
},
stopMappingConfirm () {
if (this.autoLoopEnable !== '1') return
if (this.driverActive) return
this.$confirm(this.$t('sureendbuilding'), this.$t('Prompt'), {
confirmButtonText: this.$t('Confirm'),
@@ -443,6 +474,26 @@ export default {
this.$refs.glMap.init()
})
}
},
// 打点功能点击反馈成功后
async _sendAutoBack () {
try {
this.loading = this.$loading({
lock: true,
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.6)'
})
let res = await sendAutoBack()
if (res && res.code === 200) {
this.$message({
type: 'success',
message: res.message
})
}
this.loading.close()
} catch (e) {
this.loading.close()
}
}
}
}
@@ -468,7 +519,7 @@ export default {
z-index 2003
display flex
align-items center
font-size .16rem
font-size .2rem
list-height 1
p
margin-left 10px
@@ -512,6 +563,9 @@ export default {
line-height: 0.36rem;
color: #fff
overflow: hidden
.button_control
p
font-size .26rem
.enClass
.button_control
p

View File

@@ -37,7 +37,7 @@
<el-col class="pp_item" v-for="(e, i) in newData" :key="'new' + i">
<el-row type="flex" align="middle">
<i class="el-icon-caret-right icon-caret-right" :style="i === 0 ? 'display: none' : ''"></i>
<div class="point_item point_checked">
<div class="relative point_item point_checked">
<el-row type="flex" align="middle" class="zbox">
<el-col :span="17">
<p class="point_checked_p1">{{ e.station_name }}</p>
@@ -46,6 +46,7 @@
<p class="point_checked_p2">{{ radioOption | findByValue(e.action_type) }}</p>
</el-col>
</el-row>
<el-button class="absolute icon-delt" type="danger" icon="el-icon-delete" circle @click="deletePoint(i)"></el-button>
</div>
</el-row>
</el-col>
@@ -352,7 +353,10 @@ export default {
this.loading.close()
this.disabled = false
}
}
},
deletePoint (index) {
this.newData.splice(index, 1)
}
}
}
</script>
@@ -476,6 +480,17 @@ export default {
.button_control_s
p
font-size .27rem
.icon-delt
padding 0
height: 0.3rem;
line-height: .3rem;
width: 0.3rem;
font-size: .16rem;
right: -0.1rem;
top: -0.1rem;
&:focus, &:hover
background-color: #c3390c;
border-color: #ed865c;
.enClass
.button_control p
line-height .22rem

View File

@@ -1,576 +0,0 @@
<template>
<div class="point-cloud-map">
<div ref="canvasContainer" class="canvas-container"></div>
<!-- 加载状态指示器 -->
<div v-if="isLoading" class="loading-indicator">
<i class="fa fa-circle-o-notch fa-spin"></i> 加载中...
</div>
</div>
</template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { mapGetters } from 'vuex'
// import { points } from '../../config/point.js'
// import { points1 } from '../../config/point1.js'
// import { points2 } from '../../config/point2.js'
export default {
/* eslint-disable */
data () {
return {
// Three.js核心对象
scene: null,
camera: null,
renderer: null,
controls: null,
// 点云相关
pointCloudGeometry: null,
pointCloudMaterial: null,
pointCloudMesh: null,
allPoints: [], // 存储所有累加的点云数据
pointCount: 0,
pointScale: 1.0,
// 小车相关
vehicleImage: require('../../images/new/agv.png'),
carMesh: null,
carTexture: null,
// WebSocket相关
socket: null,
isLoading: true,
reconnectTimer: null, // 重连计时器
// 动画与性能
viewSize: 20,
animationId: null,
lastUpdateTime: 0,
updateInterval: 800, // 限制更新频率,毫秒
// 点云边界:记录所有点的最大/最小x、y
pointBounds: {
minX: Infinity, // 初始设为无穷大,方便后续比较
maxX: -Infinity,
minY: Infinity,
maxY: -Infinity
},
boundMargin: 0.1, // 边界边距避免点贴屏幕边缘值为10%的范围)
defaultViewSize: 20, // 无点云时的默认可视范围
// 标记组件是否已销毁
isDestroyed: false,
minZoomRatio: 0.5,
maxZoomRatio: 3,
baseViewSize: 0
}
},
computed: {
...mapGetters(['serverUrl', , 'userRole', 'carPosition']),
},
watch: {
carPosition: {
handler(newVal) {
this.updateCarPosition(newVal)
},
deep: true
}
},
mounted () {
// 初始化Three.js场景
this.initThreeJs();
// 初始化控制器(拖动和缩放)
this.initControls();
// 初始化点云
this.initPointCloud();
// 初始化小车模型
this.initCar();
// 启动动画循环
this.startAnimationLoop();
// 监听窗口大小变化
window.addEventListener('resize', this.handleResize);
// 初始加载完成
setTimeout(() => {
this.isLoading = false;
}, 1000);
},
beforeDestroy () {
// 先标记组件已销毁,阻止新消息处理
this.isDestroyed = true;
// 1. 清理WebSocket先移除事件监听再关闭连接
this.closeWebSocket()
// 2. 停止动画循环并清理
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
// 3. 清理OrbitControls关键控制器有内部事件监听
if (this.controls) {
this.controls.dispose(); // 控制器自带的清理方法,移除事件监听
this.controls = null;
}
// 4. 清理Three.js场景对象先从场景移除再释放资源
if (this.scene) {
// 移除点云网格
if (this.pointCloudMesh) {
this.scene.remove(this.pointCloudMesh);
}
// 移除小车模型
if (this.carMesh) {
this.scene.remove(this.carMesh);
}
// 清空场景(彻底释放所有子对象引用)
this.scene.clear();
this.scene = null;
}
// 5. 释放Three.js渲染器及DOM
if (this.renderer) {
// 从DOM中移除canvas元素避免残留DOM节点
const container = this.$refs.canvasContainer;
if (container && this.renderer.domElement) {
container.removeChild(this.renderer.domElement);
}
// 释放渲染器资源
this.renderer.dispose();
this.renderer.forceContextLoss(); // 强制释放WebGL上下文关键
this.renderer = null;
}
// 6. 释放几何体和材质(确保先移除引用)
if (this.pointCloudGeometry) {
this.pointCloudGeometry.dispose();
this.pointCloudGeometry = null;
}
if (this.pointCloudMaterial) {
// 先清除材质的纹理引用(避免纹理无法释放)
this.pointCloudMaterial.map = null;
this.pointCloudMaterial.dispose();
this.pointCloudMaterial = null;
}
// 7. 释放小车纹理(确保材质不再引用)
if (this.carTexture) {
this.carTexture.dispose();
this.carTexture = null;
}
// 8. 移除窗口大小变化监听(避免重复监听)
window.removeEventListener('resize', this.handleResize);
// 9. 清空其他引用帮助GC回收
this.camera = null;
this.carMesh = null;
this.pointCloudMesh = null;
},
methods: {
/**
* 初始化Three.js场景、相机和渲染器
*/
initThreeJs() {
const container = this.$refs.canvasContainer;
const aspect = container.clientWidth / container.clientHeight
// 创建场景
this.scene = new THREE.Scene();
// 创建正交相机适合2D场景
this.camera = new THREE.OrthographicCamera(
-this.viewSize * aspect / 2, // left
this.viewSize * aspect / 2, // right
this.viewSize / 2, // top
-this.viewSize / 2, // bottom
0.1, // near
1000 // far
);
this.camera.position.z = 100; // 保持在Z轴上方不影响2D视图
// 创建渲染器 - 启用alpha通道实现透明背景
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true, // 关键启用alpha通道
transparent: true // 关键:允许透明
});
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
// 可选设置clearAlpha确保完全透明
this.renderer.setClearAlpha(0);
container.appendChild(this.renderer.domElement);
},
/**
* 初始化控制器,支持拖动和缩放
*/
initControls() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableRotate = false; // 禁用旋转保持2D视图
this.controls.enableZoom = true; // 启用缩放
this.controls.enablePan = true; // 启用平移(拖动)
this.controls.screenSpacePanning = true; // 2D平移模式
this.controls.touchZoomSpeed = 1;
this.controls.panSpeed = 1;
this.controls.touches = {
ONE: THREE.TOUCH.PAN, // 单指拖动为平移
TWO: THREE.TOUCH.DOLLY_PAN // 双指拖动为缩放和平移
};
// 添加错误处理逻辑
this.controls.addEventListener('error', (event) => {
console.error('OrbitControls error:', event);
});
},
/**
* 初始化点云
*/
initPointCloud() {
// 使用BufferGeometry提高大量点的渲染性能
this.pointCloudGeometry = new THREE.BufferGeometry();
// 点材质设置
this.pointCloudMaterial = new THREE.PointsMaterial({
color: 0xFFFFFF,
size: 1.5, // 初始值会被动态覆盖
sizeAttenuation: true, // 关键!
transparent: true,
opacity: 1
});
// 创建点云网格
this.pointCloudMesh = new THREE.Points(
this.pointCloudGeometry,
this.pointCloudMaterial
);
this.scene.add(this.pointCloudMesh);
// 初始化历史数据为空
this.allPoints = [];
},
/**
* 初始化小车模型
*/
initCar() {
// 加载小车图片作为精灵
const loader = new THREE.TextureLoader();
this.carTexture = loader.load(this.vehicleImage, (texture) => {
const material = new THREE.SpriteMaterial({
map: texture,
transparent: true, // 保留图片透明区域
opacity: 1,
depthWrite: false // 避免被点云遮挡(可选,根据层级需求调整)
});
this.carMesh = new THREE.Sprite(material);
this.scene.add(this.carMesh);
// 初始计算小车尺寸
this.calculateCarSize();
// 初始位置角度更新
if (this.carPosition) {
this.updateCarPosition(this.carPosition);
}
}, undefined, (error) => {
console.error('小车图片加载失败:', error);
});
},
/**
* 单独的小车尺寸计算方法,确保窗口缩放时可复用
*/
calculateCarSize() {
if (!this.carMesh) return;
const container = this.$refs.canvasContainer;
const canvasWidth = container.clientWidth;
const canvasHeight = container.clientHeight;
const aspect = canvasWidth / canvasHeight;
// 核心计算将50px转换为Three.js世界坐标
// 公式:(目标像素尺寸 / 画布尺寸) * 相机可视范围 * 校正系数
const spriteSizeX = (50 / canvasWidth) * this.viewSize * aspect;
const spriteSizeY = (50 / canvasHeight) * this.viewSize;
this.carMesh.scale.set(spriteSizeX, spriteSizeY, 1);
},
/**
* 更新小车位置和角度
*/
updateCarPosition(position) {
if (this.carMesh && position) {
this.carMesh.position.x = position.x * this.pointScale;
this.carMesh.position.y = position.y * this.pointScale;
this.carMesh.position.z = 1;
// 转换角度为弧度并调整方向Three.js使用弧度
this.carMesh.rotation.z = position.angle;
}
},
/**
* 初始化WebSocket连接
*/
initWebSocket() {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
const wsHost = this.serverUrl.replace(/^https?:\/\//, '')
this.socket = new WebSocket(`ws://${wsHost}/webSocket/PointCloudData/${this.userRole}`);
this.socket.onopen = () => {
console.log('WebSocket连接已建立');
this.isLoading = false;
};
this.socket.onmessage = (event) => {
// 组件已销毁则直接返回,不处理消息
if (this.isDestroyed) return
try {
// 限制更新频率,优化性能
const now = Date.now();
if (now - this.lastUpdateTime < this.updateInterval) {
return;
}
this.lastUpdateTime = now;
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
const pointData = JSON.parse(event.data);
this.updatePointCloud(pointData.data);
} catch (error) {
console.error('解析点云数据失败:', error);
}
};
this.socket.onclose = (event) => {
console.log('WebSocket连接已关闭代码:', event.code);
this.isLoading = true;
// 自动重连
this.reconnectTimer = setTimeout(() => this.initWebSocket(), 3000);
};
this.socket.onerror = (error) => {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
console.error('WebSocket错误:', error);
this.isLoading = true;
};
},
closeWebSocket () {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
if (this.socket) {
this.socket.onopen = null;
this.socket.onmessage = null;
this.socket.onclose = null;
this.socket.onerror = null;
this.socket.close(1000, '组件销毁');
this.socket = null;
}
this.allPoints = []
},
init () {
this.initWebSocket();
// const pointData = points.data
// this.updatePointCloud(pointData);
// setTimeout(() => {
// const arr = points1.data
// this.updatePointCloud(arr);
// }, 1000)
// setTimeout(() => {
// const arr = points2.data
// this.updatePointCloud(arr);
// }, 2000)
},
/**
* 更新点云数据,优化大量点的渲染性能
*/
updatePointCloud(points) {
if (!Array.isArray(points) || points.length === 0) return;
// 用于跟踪已存在的点,使用"x,y"作为唯一标识
const existingPoints = {};
// 先将已有点添加到跟踪对象
this.allPoints.forEach(point => {
const key = `${point.x},${point.y}`;
existingPoints[key] = true;
});
// 过滤新点中的重复点
const newUniquePoints = points.filter(point => {
const key = `${point.x},${point.y}`;
if (!existingPoints[key]) {
existingPoints[key] = true;
return true;
}
return false;
});
this.allPoints = [...this.allPoints, ...newUniquePoints];
this.pointCount = this.allPoints.length; // 更新总点数
// 重新计算所有点云的边界
this.pointBounds = { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity };
this.allPoints.forEach(point => {
const scaledX = (point.x || 0) * this.pointScale; // 缩放后的实际坐标
const scaledY = (point.y || 0) * this.pointScale;
// 更新最大/最小值
this.pointBounds.minX = Math.min(this.pointBounds.minX, scaledX);
this.pointBounds.maxX = Math.max(this.pointBounds.maxX, scaledX);
this.pointBounds.minY = Math.min(this.pointBounds.minY, scaledY);
this.pointBounds.maxY = Math.max(this.pointBounds.maxY, scaledY);
});
this.adjustCameraByBounds();
// 动态扩展缓冲区
const positionAttribute = this.pointCloudGeometry.getAttribute('position');
let positions;
if (!positionAttribute) {
// 首次初始化:创建缓冲区
positions = new Float32Array(this.pointCount * 3);
this.pointCloudGeometry.setAttribute(
'position',
new THREE.BufferAttribute(positions, 3)
);
} else {
// 后续更新:扩展现有缓冲区(避免重建)
positions = positionAttribute.array;
const newLength = this.pointCount * 3;
if (newLength > positions.length) {
// 扩展时多预留20%空间,减少频繁扩展
const newPositions = new Float32Array(Math.ceil(newLength * 1.2));
newPositions.set(positions); // 复制原有数据
positions = newPositions;
this.pointCloudGeometry.setAttribute(
'position',
new THREE.BufferAttribute(positions, 3)
);
}
}
// 填充新数据(仅更新新增部分,减少重复计算)
const startIndex = (this.pointCount - newUniquePoints.length) * 3; // 从新增点开始
for (let i = 0; i < newUniquePoints.length; i++) {
const point = newUniquePoints[i];
const index = startIndex + i * 3;
positions[index] = (point.x || 0) * this.pointScale;
positions[index + 1] = (point.y || 0) * this.pointScale;
positions[index + 2] = 0;
}
// 标记更新
this.pointCloudGeometry.getAttribute('position').needsUpdate = true;
},
/**
* 根据边界动态调整相机可视范围
*/
adjustCameraByBounds() {
const container = this.$refs.canvasContainer;
const aspect = container.clientWidth / container.clientHeight; // 画布宽高比
// 1. 计算点云的实际范围(宽和高)
const pointRangeX = this.pointBounds.maxX - this.pointBounds.minX;
const pointRangeY = this.pointBounds.maxY - this.pointBounds.minY;
// 2. 计算“基准可视范围”(刚好覆盖点云+边距的最小范围)
let baseViewSize = this.defaultViewSize; // 默认值(无点云时)
if (this.allPoints.length > 0 && !(pointRangeX === 0 && pointRangeY === 0)) {
const requiredViewSizeX = pointRangeX * (1 + this.boundMargin); // X方向需覆盖范围
const requiredViewSizeY = pointRangeY * (1 + this.boundMargin); // Y方向需覆盖范围
// 基准可视范围取X/Y方向中较大值确保适配宽高比后覆盖所有点
baseViewSize = Math.max(requiredViewSizeX / aspect, requiredViewSizeY);
}
this.baseViewSize = baseViewSize; // 缓存基准值,用于后续缩放限制
// 3. 更新相机可视范围(基于基准值)
this.viewSize = baseViewSize; // 初始可视范围=基准范围
this.camera.left = -this.viewSize * aspect / 2;
this.camera.right = this.viewSize * aspect / 2;
this.camera.top = this.viewSize / 2;
this.camera.bottom = -this.viewSize / 2;
this.camera.updateProjectionMatrix(); // 必须更新投影矩阵,否则不生效
// 4. 让相机焦点对准点云中心(可选,确保点云在屏幕中间)
const centerX = (this.pointBounds.minX + this.pointBounds.maxX) / 2;
const centerY = (this.pointBounds.minY + this.pointBounds.maxY) / 2;
this.controls.target.set(centerX, centerY, 0); // 控制器目标对准点云中心
this.camera.position.set(centerX, centerY, 100); // 相机位置也对准中心Z轴不变
// 5. 关键设置OrbitControls缩放限制正交相机专用
if (this.controls) {
// 5.1 正交相机缩放限制minZoom/maxZoom基于基准可视范围的倍数
// 逻辑viewSize = baseViewSize / zoomRatio → 缩放倍数越大viewSize越小画面放大
this.controls.minZoom = this.minZoomRatio; // 最小缩放倍数→最大viewSize缩小画面
this.controls.maxZoom = this.maxZoomRatio; // 最大缩放倍数→最小viewSize放大画面
// 5.2 兼容控制器逻辑设置minDistance/maxDistance虽正交相机不用但避免异常
this.controls.minDistance = 50; // 相机最小Z轴距离小于此值可能穿模
this.controls.maxDistance = 200; // 相机最大Z轴距离大于此值画面过小
// 5.3 锁定缩放中心(可选:确保缩放时以点云中心为基准)
this.controls.zoomToCursor = false; // 关闭“鼠标位置缩放”,避免缩放偏移
}
// 相机参数更新后,同步更新小车尺寸
this.calculateCarSize();
},
/**
* 处理窗口大小变化
*/
handleResize() {
const container = this.$refs.canvasContainer;
const width = container.clientWidth;
const height = container.clientHeight;
// 原有渲染器尺寸更新
this.renderer.setSize(width, height);
// 基于当前点云边界重新适配相机(关键)
this.adjustCameraByBounds();
// 窗口缩放时重新计算小车尺寸
this.calculateCarSize();
},
/**
* 启动动画循环
*/
startAnimationLoop() {
const animate = () => {
this.animationId = requestAnimationFrame(animate);
// 更新控制器(用于阻尼效果)
if (this.controls) {
this.controls.update();
}
// 渲染场景
this.renderer.render(this.scene, this.camera);
};
animate();
}
}
}
</script>
<style lang="stylus" scoped>
.point-cloud-map
position relative
width 100%
height 100%
background-color rgba(4, 33, 58, 70%)
box-shadow inset 1px 1px 7px 2px #4d9bcd
overflow hidden
.canvas-container {
width: 100%;
height: 100%;
position: relative;
z-index: 0;
}
.loading-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, 0.8);
padding: .01rem .02rem;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
font-size: .12rem;
color: #333;
z-index: 100;
display: flex;
align-items: center;
}
</style>

View File

@@ -1,610 +0,0 @@
<template>
<div class="point-cloud-map">
<div ref="canvasContainer" class="canvas-container"></div>
<!-- 加载状态指示器 -->
<div v-if="isLoading" class="loading-indicator">
<i class="fa fa-circle-o-notch fa-spin"></i> {{ $t('Loading') }}
</div>
</div>
</template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { mapGetters } from 'vuex'
// import { points } from '../../config/point.js'
// import { points1 } from '../../config/point1.js'
// import { points2 } from '../../config/point2.js'
// 常量定义
const UPDATE_INTERVAL = 800 // 限制更新频率,毫秒
const DEFAULT_VIEW_SIZE = 20 // 无点云时的默认可视范围
const BOUND_MARGIN = 0.1 // 边界边距避免点贴屏幕边缘值为10%的范围)
const MIN_ZOOM_RATIO = 0.5
const MAX_ZOOM_RATIO = 3
export default {
name: 'PointCloudMap',
data() {
return {
// Three.js核心对象
scene: null,
camera: null,
renderer: null,
controls: null,
// 点云相关
pointCloudGeometry: null,
pointCloudMaterial: null,
pointCloudMesh: null,
allPoints: [], // 存储所有累加的点云数据
pointCount: 0,
pointScale: 1.0,
// 小车相关
vehicleImage: require('../../images/new/agv.png'),
carMesh: null,
carTexture: null,
// WebSocket相关
socket: null,
isLoading: true,
reconnectTimer: null, // 重连计时器
// 动画与性能
viewSize: DEFAULT_VIEW_SIZE,
animationId: null,
lastUpdateTime: 0,
updateInterval: UPDATE_INTERVAL,
// 点云边界:记录所有点的最大/最小x、y
pointBounds: {
minX: Infinity, // 初始设为无穷大,方便后续比较
maxX: -Infinity,
minY: Infinity,
maxY: -Infinity
},
// 标记组件是否已销毁
isDestroyed: false,
baseViewSize: 0
}
},
computed: {
...mapGetters(['serverUrl', 'userRole', 'carPosition']),
},
watch: {
carPosition: {
handler(newVal) {
this.updateCarPosition(newVal)
},
deep: true
}
},
mounted() {
this.initializeComponent()
},
beforeDestroy() {
this.cleanup()
},
methods: {
/**
* 初始化组件
*/
initializeComponent() {
this.initThreeJs()
this.initControls()
this.initPointCloud()
this.initCar()
this.startAnimationLoop()
window.addEventListener('resize', this.handleResize)
// 初始加载完成
setTimeout(() => {
this.isLoading = false
}, 1000)
},
/**
* 初始化Three.js场景、相机和渲染器
*/
initThreeJs() {
const container = this.$refs.canvasContainer
const aspect = container.clientWidth / container.clientHeight
// 创建场景
this.scene = new THREE.Scene()
// 创建正交相机适合2D场景
this.camera = new THREE.OrthographicCamera(
-this.viewSize * aspect / 2, // left
this.viewSize * aspect / 2, // right
this.viewSize / 2, // top
-this.viewSize / 2, // bottom
0.1, // near
1000 // far
)
this.camera.position.z = 100 // 保持在Z轴上方不影响2D视图
// 创建渲染器 - 启用alpha通道实现透明背景
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true, // 关键启用alpha通道
transparent: true // 关键:允许透明
})
this.renderer.setSize(container.clientWidth, container.clientHeight)
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) // 限制最大像素比
// 可选设置clearAlpha确保完全透明
this.renderer.setClearAlpha(0)
container.appendChild(this.renderer.domElement)
},
/**
* 初始化控制器,支持拖动和缩放
*/
initControls() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.enableRotate = false // 禁用旋转保持2D视图
this.controls.enableZoom = true // 启用缩放
this.controls.enablePan = true // 启用平移(拖动)
this.controls.screenSpacePanning = true // 2D平移模式
this.controls.touchZoomSpeed = 1
this.controls.panSpeed = 1
this.controls.touches = {
ONE: THREE.TOUCH.PAN, // 单指拖动为平移
TWO: THREE.TOUCH.DOLLY_PAN // 双指拖动为缩放和平移
}
// 添加错误处理逻辑
this.controls.addEventListener('error', (event) => {
console.error('OrbitControls error:', event)
})
},
/**
* 初始化点云
*/
initPointCloud() {
// 使用BufferGeometry提高大量点的渲染性能
this.pointCloudGeometry = new THREE.BufferGeometry()
// 点材质设置
this.pointCloudMaterial = new THREE.PointsMaterial({
color: 0xFFFFFF,
size: 1, // 初始值会被动态覆盖
sizeAttenuation: true, // 关键!
transparent: true,
opacity: 1
})
// 创建点云网格
this.pointCloudMesh = new THREE.Points(
this.pointCloudGeometry,
this.pointCloudMaterial
)
this.scene.add(this.pointCloudMesh)
// 初始化历史数据为空
this.allPoints = []
},
/**
* 初始化小车模型
*/
initCar() {
// 加载小车图片作为精灵
const loader = new THREE.TextureLoader()
this.carTexture = loader.load(this.vehicleImage, (texture) => {
const material = new THREE.SpriteMaterial({
map: texture,
transparent: true, // 保留图片透明区域
opacity: 1,
depthWrite: false // 避免被点云遮挡(可选,根据层级需求调整)
})
this.carMesh = new THREE.Sprite(material)
this.scene.add(this.carMesh)
// 初始计算小车尺寸
this.calculateCarSize()
// 初始位置角度更新
if (this.carPosition) {
this.updateCarPosition(this.carPosition)
}
}, undefined, (error) => {
console.error('小车图片加载失败:', error)
})
},
/**
* 单独的小车尺寸计算方法,确保窗口缩放时可复用
*/
calculateCarSize() {
if (!this.carMesh) return
const container = this.$refs.canvasContainer
const canvasWidth = container.clientWidth
const canvasHeight = container.clientHeight
const aspect = canvasWidth / canvasHeight
// 核心计算将50px转换为Three.js世界坐标
// 公式:(目标像素尺寸 / 画布尺寸) * 相机可视范围 * 校正系数
const spriteSizeX = (50 / canvasWidth) * this.viewSize * aspect
const spriteSizeY = (50 / canvasHeight) * this.viewSize
this.carMesh.scale.set(spriteSizeX, spriteSizeY, 1)
},
/**
* 更新小车位置和角度
*/
updateCarPosition(position) {
if (this.carMesh && position) {
this.carMesh.position.x = position.x * this.pointScale
this.carMesh.position.y = position.y * this.pointScale
this.carMesh.position.z = 1
// 转换角度为弧度并调整方向Three.js使用弧度
this.carMesh.rotation.z = position.angle
}
},
/**
* 初始化WebSocket连接
*/
initWebSocket() {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
const wsHost = this.serverUrl.replace(/^https?:\/\//, '')
this.socket = new WebSocket(`ws://${wsHost}/webSocket/PointCloudData/${this.userRole}`)
this.socket.onopen = () => {
console.log('WebSocket连接已建立')
this.isLoading = false
}
this.socket.onmessage = (event) => {
// 组件已销毁则直接返回,不处理消息
if (this.isDestroyed) return
try {
// 限制更新频率,优化性能
const now = Date.now()
if (now - this.lastUpdateTime < this.updateInterval) {
return
}
this.lastUpdateTime = now
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
const pointData = JSON.parse(event.data)
this.updatePointCloud(pointData.data)
} catch (error) {
console.error('解析点云数据失败:', error)
}
}
this.socket.onclose = (event) => {
console.log('WebSocket连接已关闭代码:', event.code)
this.isLoading = true
// 自动重连
this.reconnectTimer = setTimeout(() => this.initWebSocket(), 3000)
}
this.socket.onerror = (error) => {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
console.error('WebSocket错误:', error)
this.isLoading = true
}
},
closeWebSocket() {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
if (this.socket) {
this.socket.onopen = null
this.socket.onmessage = null
this.socket.onclose = null
this.socket.onerror = null
this.socket.close(1000, '组件销毁')
this.socket = null
}
this.allPoints = []
},
init() {
this.initWebSocket()
// const pointData = points.data
// this.updatePointCloud(pointData);
// setTimeout(() => {
// const arr = points1.data
// this.updatePointCloud(arr);
// }, 1000)
// setTimeout(() => {
// const arr = points2.data
// this.updatePointCloud(arr);
// }, 2000)
},
/**
* 更新点云数据,优化大量点的渲染性能
*/
updatePointCloud(points) {
if (!Array.isArray(points) || points.length === 0) return
// 使用Set数据结构提高查找性能
const existingPoints = new Set(this.allPoints.map(p => `${p.x},${p.y}`))
// 过滤新点中的重复点
const newUniquePoints = points.filter(point => {
const key = `${point.x},${point.y}`
return !existingPoints.has(key)
})
// 如果没有新点,直接返回
if (newUniquePoints.length === 0) return
this.allPoints = [...this.allPoints, ...newUniquePoints]
this.pointCount = this.allPoints.length // 更新总点数
// 批量更新边界计算
this.updatePointBounds(newUniquePoints)
// 更新点云几何数据
this.updatePointCloudGeometry(newUniquePoints)
},
/**
* 批量更新点云边界
*/
updatePointBounds(newPoints) {
// 批量处理新点,减少循环次数
for (const point of newPoints) {
const scaledX = (point.x || 0) * this.pointScale
const scaledY = (point.y || 0) * this.pointScale
// 更新最大/最小值
if (scaledX < this.pointBounds.minX) this.pointBounds.minX = scaledX
if (scaledX > this.pointBounds.maxX) this.pointBounds.maxX = scaledX
if (scaledY < this.pointBounds.minY) this.pointBounds.minY = scaledY
if (scaledY > this.pointBounds.maxY) this.pointBounds.maxY = scaledY
}
this.adjustCameraByBounds()
},
/**
* 更新点云几何数据
*/
updatePointCloudGeometry(newUniquePoints) {
const positionAttribute = this.pointCloudGeometry.getAttribute('position')
let positions
const newPointsCount = newUniquePoints.length
if (!positionAttribute) {
// 首次初始化:创建缓冲区
positions = new Float32Array(this.pointCount * 3)
this.pointCloudGeometry.setAttribute(
'position',
new THREE.BufferAttribute(positions, 3)
)
} else {
// 后续更新:扩展现有缓冲区(避免重建)
positions = positionAttribute.array
const newLength = this.pointCount * 3
if (newLength > positions.length) {
// 扩展时多预留20%空间,减少频繁扩展
const newPositions = new Float32Array(Math.ceil(newLength * 1.2))
newPositions.set(positions) // 复制原有数据
positions = newPositions
this.pointCloudGeometry.setAttribute(
'position',
new THREE.BufferAttribute(positions, 3)
)
}
}
// 填充新数据(仅更新新增部分,减少重复计算)
const startIndex = (this.pointCount - newPointsCount) * 3
// 使用for循环而不是forEach性能更好
for (let i = 0; i < newPointsCount; i++) {
const point = newUniquePoints[i]
const index = startIndex + i * 3
positions[index] = (point.x || 0) * this.pointScale
positions[index + 1] = (point.y || 0) * this.pointScale
positions[index + 2] = 0
}
// 标记更新
this.pointCloudGeometry.getAttribute('position').needsUpdate = true
},
/**
* 根据边界动态调整相机可视范围
*/
adjustCameraByBounds() {
const container = this.$refs.canvasContainer
const aspect = container.clientWidth / container.clientHeight // 画布宽高比
// 1. 计算点云的实际范围(宽和高)
const pointRangeX = this.pointBounds.maxX - this.pointBounds.minX
const pointRangeY = this.pointBounds.maxY - this.pointBounds.minY
// 2. 计算"基准可视范围"
let baseViewSize = DEFAULT_VIEW_SIZE // 默认值(无点云时)
if (this.allPoints.length > 0 && !(pointRangeX === 0 && pointRangeY === 0)) {
const requiredViewSizeX = pointRangeX * (1 + BOUND_MARGIN) // X方向需覆盖范围
const requiredViewSizeY = pointRangeY * (1 + BOUND_MARGIN) // Y方向需覆盖范围
// 基准可视范围取X/Y方向中较大值确保适配宽高比后覆盖所有点
baseViewSize = Math.max(requiredViewSizeX / aspect, requiredViewSizeY)
}
this.baseViewSize = baseViewSize // 缓存基准值,用于后续缩放限制
// 3. 更新相机可视范围(基于基准值)
this.viewSize = baseViewSize // 初始可视范围=基准范围
this.camera.left = -this.viewSize * aspect / 2
this.camera.right = this.viewSize * aspect / 2
this.camera.top = this.viewSize / 2
this.camera.bottom = -this.viewSize / 2
this.camera.updateProjectionMatrix() // 必须更新投影矩阵,否则不生效
// 4. 让相机焦点对准点云中心
const centerX = (this.pointBounds.minX + this.pointBounds.maxX) / 2
const centerY = (this.pointBounds.minY + this.pointBounds.maxY) / 2
this.controls.target.set(centerX, centerY, 0) // 控制器目标对准点云中心
this.camera.position.set(centerX, centerY, 100) // 相机位置也对准中心Z轴不变
// 5. 关键设置OrbitControls缩放限制正交相机专用
if (this.controls) {
this.controls.minZoom = MIN_ZOOM_RATIO
this.controls.maxZoom = MAX_ZOOM_RATIO
this.controls.minDistance = 50
this.controls.maxDistance = 200
this.controls.zoomToCursor = false // 关闭"鼠标位置缩放",避免缩放偏移
}
// 相机参数更新后,同步更新小车尺寸
this.calculateCarSize()
},
/**
* 处理窗口大小变化
*/
handleResize() {
const container = this.$refs.canvasContainer
const width = container.clientWidth
const height = container.clientHeight
// 原有渲染器尺寸更新
this.renderer.setSize(width, height)
// 基于当前点云边界重新适配相机(关键)
this.adjustCameraByBounds()
// 窗口缩放时重新计算小车尺寸
this.calculateCarSize()
},
/**
* 启动动画循环
*/
startAnimationLoop() {
const animate = () => {
this.animationId = requestAnimationFrame(animate)
// 更新控制器(用于阻尼效果)
if (this.controls) {
this.controls.update()
}
// 渲染场景
this.renderer.render(this.scene, this.camera)
}
animate()
},
/**
* 清理资源
*/
cleanup() {
// 先标记组件已销毁,阻止新消息处理
this.isDestroyed = true
// 1. 清理WebSocket
this.closeWebSocket()
// 2. 停止动画循环
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
// 3. 清理OrbitControls
if (this.controls) {
this.controls.dispose()
this.controls = null
}
// 4. 清理Three.js场景对象
if (this.scene) {
// 移除点云网格
if (this.pointCloudMesh) {
this.scene.remove(this.pointCloudMesh)
}
// 移除小车模型
if (this.carMesh) {
this.scene.remove(this.carMesh)
}
// 清空场景
this.scene.clear()
this.scene = null
}
// 5. 释放Three.js渲染器及DOM
if (this.renderer) {
const container = this.$refs.canvasContainer
if (container && this.renderer.domElement) {
container.removeChild(this.renderer.domElement)
}
this.renderer.dispose()
this.renderer.forceContextLoss()
this.renderer = null
}
// 6. 释放几何体和材质
if (this.pointCloudGeometry) {
this.pointCloudGeometry.dispose()
this.pointCloudGeometry = null
}
if (this.pointCloudMaterial) {
this.pointCloudMaterial.map = null
this.pointCloudMaterial.dispose()
this.pointCloudMaterial = null
}
// 7. 释放小车纹理
if (this.carTexture) {
this.carTexture.dispose()
this.carTexture = null
}
// 8. 移除窗口大小变化监听
window.removeEventListener('resize', this.handleResize)
// 9. 清空其他引用
this.camera = null
this.carMesh = null
this.pointCloudMesh = null
}
}
}
</script>
<style lang="stylus" scoped>
.point-cloud-map
position relative
width 100%
height 100%
background-color rgba(4, 33, 58, 70%)
box-shadow inset 1px 1px 7px 2px #4d9bcd
overflow hidden
.canvas-container
width: 100%
height: 100%
position: relative
z-index: 0
.loading-indicator
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
background-color: rgba(255, 255, 255, 0.8)
padding: .01rem .02rem
border-radius: 4px
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1)
font-size: .12rem
color: #333
z-index: 100
display: flex
align-items: center
</style>

View File

@@ -1,576 +0,0 @@
<template>
<div class="point-cloud-map">
<div ref="canvasContainer" class="canvas-container"></div>
<!-- 定位按钮 -->
<el-button icon="el-icon-location" size="mini" class="locate-button" @click="locateCarToCenter">
</el-button>
<div v-if="isLoading" class="loading-indicator">
<i class="fa fa-circle-o-notch fa-spin"></i> {{ $t('Loading') }}
</div>
</div>
</template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { mapGetters } from 'vuex'
// import { points } from '../../config/point.js'
// import { points1 } from '../../config/point1.js'
// import { points2 } from '../../config/point2.js'
// 常量定义
const UPDATE_INTERVAL = 800
const DEFAULT_VIEW_SIZE = 20
const BOUND_MARGIN = 0.1
const MIN_ZOOM_RATIO = 0.5
const MAX_ZOOM_RATIO = 3
const AUTO_ADJUST_THRESHOLD = 100 // 每新增100个点检查是否需要调整视口
export default {
name: 'PointCloudMap',
data() {
return {
// Three.js核心对象
scene: null,
camera: null,
renderer: null,
controls: null,
// 点云相关
pointCloudGeometry: null,
pointCloudMaterial: null,
pointCloudMesh: null,
allPoints: [],
pointCount: 0,
pointScale: 1.0,
newPointsSinceLastAdjust: 0, // 新增点计数器
// 小车相关
vehicleImage: require('../../images/new/agv.png'),
carMesh: null,
carTexture: null,
// WebSocket相关
socket: null,
isLoading: true,
reconnectTimer: null,
// 动画与性能
viewSize: DEFAULT_VIEW_SIZE,
animationId: null,
lastUpdateTime: 0,
updateInterval: UPDATE_INTERVAL,
// 点云边界
pointBounds: {
minX: Infinity,
maxX: -Infinity,
minY: Infinity,
maxY: -Infinity
},
isDestroyed: false,
baseViewSize: 0,
// 画布尺寸(正方形)
canvasSize: {
width: 0,
height: 0
}
}
},
computed: {
...mapGetters(['serverUrl', 'userRole', 'carPosition']),
},
watch: {
carPosition: {
handler(newVal) {
this.updateCarPosition(newVal)
},
deep: true
}
},
mounted() {
this.initializeComponent()
},
beforeDestroy() {
this.cleanup()
},
methods: {
initializeComponent() {
// 设置画布为正方形(仅初始化时设置一次)
this.setCanvasSizeAsSquare()
this.initThreeJs()
this.initControls()
this.initPointCloud()
this.initCar()
this.startAnimationLoop()
},
/**
* 设置画布为正方形(宽高等于容器初始宽度)
*/
setCanvasSizeAsSquare() {
const container = this.$refs.canvasContainer
// 获取容器初始宽度
const containerWidth = container.clientWidth
// 设置宽高相等(正方形),仅初始化时计算一次
this.canvasSize.width = containerWidth
this.canvasSize.height = containerWidth
},
initThreeJs() {
const container = this.$refs.canvasContainer
const aspect = this.canvasSize.width / this.canvasSize.height // 正方形画布aspect=1
this.scene = new THREE.Scene()
this.camera = new THREE.OrthographicCamera(
-this.viewSize * aspect / 2,
this.viewSize * aspect / 2,
this.viewSize / 2,
-this.viewSize / 2,
0.1,
1000
)
this.camera.position.z = 100
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
transparent: true
})
// 使用初始化时计算的正方形尺寸
this.renderer.setSize(this.canvasSize.width, this.canvasSize.height)
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
this.renderer.setClearAlpha(0)
container.appendChild(this.renderer.domElement)
},
initControls() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.enableRotate = false
this.controls.enableZoom = true
this.controls.enablePan = true
this.controls.screenSpacePanning = true
this.controls.touchZoomSpeed = 1
this.controls.panSpeed = 1
this.controls.touches = {
ONE: THREE.TOUCH.PAN,
TWO: THREE.TOUCH.DOLLY_PAN
}
this.controls.addEventListener('error', (event) => {
console.error('OrbitControls error:', event)
})
},
initPointCloud() {
this.pointCloudGeometry = new THREE.BufferGeometry()
this.pointCloudMaterial = new THREE.PointsMaterial({
color: 0xFFFFFF,
size: 1,
sizeAttenuation: true,
transparent: true,
opacity: 1
})
this.pointCloudMesh = new THREE.Points(
this.pointCloudGeometry,
this.pointCloudMaterial
)
this.scene.add(this.pointCloudMesh)
this.allPoints = []
},
initCar() {
const loader = new THREE.TextureLoader()
this.carTexture = loader.load(this.vehicleImage, (texture) => {
const material = new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity: 1,
depthWrite: false
})
this.carMesh = new THREE.Sprite(material)
this.scene.add(this.carMesh)
this.calculateCarSize()
if (this.carPosition) {
this.updateCarPosition(this.carPosition)
// 初始时将小车定位到中心
this.locateCarToCenter()
}
}, undefined, (error) => {
console.error('小车图片加载失败:', error)
})
},
calculateCarSize() {
if (!this.carMesh) return
// 基于初始化的正方形画布计算小车尺寸(不再更新)
const spriteSizeX = (50 / this.canvasSize.width) * this.viewSize * (this.canvasSize.width / this.canvasSize.height)
const spriteSizeY = (50 / this.canvasSize.height) * this.viewSize
this.carMesh.scale.set(spriteSizeX, spriteSizeY, 1)
},
updateCarPosition(position) {
if (this.carMesh && position) {
this.carMesh.position.x = position.x * this.pointScale
this.carMesh.position.y = position.y * this.pointScale
this.carMesh.position.z = 1
this.carMesh.rotation.z = position.angle
}
},
/**
* 定位按钮点击事件 - 将小车置于视窗中心
*/
locateCarToCenter() {
if (!this.carMesh || !this.carPosition || !this.controls) return
// 计算小车在世界坐标系中的位置
const carWorldX = this.carPosition.x * this.pointScale
const carWorldY = this.carPosition.y * this.pointScale
// 将相机和控制器目标设置为小车位置,实现居中
this.controls.target.set(carWorldX, carWorldY, 0)
this.camera.position.set(carWorldX, carWorldY, 100)
this.controls.update() // 立即更新控制器状态
},
initWebSocket() {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
const protocol = this.serverUrl.startsWith('https') ? 'wss' : 'ws'
const wsHost = this.serverUrl.replace(/^https?:\/\//, '')
this.socket = new WebSocket(`${protocol}://${wsHost}/webSocket/PointCloudData/${this.userRole}`)
this.socket.onopen = () => {
console.log('WebSocket连接已建立')
this.isLoading = false
}
this.socket.onmessage = (event) => {
if (this.isDestroyed) return
try {
const now = Date.now()
if (now - this.lastUpdateTime < this.updateInterval) {
return
}
this.lastUpdateTime = now
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
const pointData = JSON.parse(event.data)
this.updatePointCloud(pointData.data)
} catch (error) {
console.error('解析点云数据失败:', error)
}
}
this.socket.onclose = (event) => {
console.log('WebSocket连接已关闭代码:', event.code)
this.isLoading = true
this.reconnectTimer = setTimeout(() => this.initWebSocket(), 3000)
}
this.socket.onerror = (error) => {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
console.error('WebSocket错误:', error)
this.isLoading = true
}
},
closeWebSocket() {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
if (this.socket) {
this.socket.onopen = null
this.socket.onmessage = null
this.socket.onclose = null
this.socket.onerror = null
this.socket.close(1000, '组件销毁')
this.socket = null
}
this.allPoints = []
},
init() {
this.initWebSocket()
// this.isLoading = false;
// const pointData = points.data
// this.updatePointCloud(pointData);
// setTimeout(() => {
// const arr = points1.data
// this.updatePointCloud(arr);
// }, 1000)
// setTimeout(() => {
// const arr = points2.data
// this.updatePointCloud(arr);
// }, 2000)
},
updatePointCloud(points) {
if (!Array.isArray(points) || points.length === 0) return
const existingPoints = new Set(this.allPoints.map(p => `${p.x},${p.y}`))
const newUniquePoints = points.filter(point => {
const key = `${point.x},${point.y}`
return !existingPoints.has(key)
})
if (newUniquePoints.length === 0) return
this.allPoints = [...this.allPoints, ...newUniquePoints]
this.pointCount = this.allPoints.length
this.newPointsSinceLastAdjust += newUniquePoints.length
this.updatePointBounds(newUniquePoints)
this.updatePointCloudGeometry(newUniquePoints)
// 定期检查是否需要调整视口
if (this.newPointsSinceLastAdjust >= AUTO_ADJUST_THRESHOLD) {
this.checkAndAdjustViewport()
this.newPointsSinceLastAdjust = 0
}
},
updatePointBounds(newPoints) {
for (const point of newPoints) {
const scaledX = (point.x || 0) * this.pointScale
const scaledY = (point.y || 0) * this.pointScale
if (scaledX < this.pointBounds.minX) this.pointBounds.minX = scaledX
if (scaledX > this.pointBounds.maxX) this.pointBounds.maxX = scaledX
if (scaledY < this.pointBounds.minY) this.pointBounds.minY = scaledY
if (scaledY > this.pointBounds.maxY) this.pointBounds.maxY = scaledY
}
// 仅在首次加载点云时自动适配
if (this.pointCount === newPoints.length) {
this.adjustCameraByBounds()
}
},
/**
* 检查并调整视口
*/
checkAndAdjustViewport() {
const aspect = this.canvasSize.width / this.canvasSize.height
const pointRangeX = this.pointBounds.maxX - this.pointBounds.minX
const pointRangeY = this.pointBounds.maxY - this.pointBounds.minY
// 计算当前视口能显示的范围
const currentViewWidth = this.viewSize * aspect
const currentViewHeight = this.viewSize
// 如果点云范围超过当前视口的80%,则调整视口
if (pointRangeX > currentViewWidth * 0.8 || pointRangeY > currentViewHeight * 0.8) {
this.adjustCameraByBounds()
}
},
updatePointCloudGeometry(newUniquePoints) {
const positionAttribute = this.pointCloudGeometry.getAttribute('position')
let positions
const newPointsCount = newUniquePoints.length
if (!positionAttribute) {
positions = new Float32Array(this.pointCount * 3)
this.pointCloudGeometry.setAttribute(
'position',
new THREE.BufferAttribute(positions, 3)
)
} else {
positions = positionAttribute.array
const newLength = this.pointCount * 3
if (newLength > positions.length) {
const newPositions = new Float32Array(Math.ceil(newLength * 1.2))
newPositions.set(positions)
positions = newPositions
this.pointCloudGeometry.setAttribute(
'position',
new THREE.BufferAttribute(positions, 3)
)
}
}
const startIndex = (this.pointCount - newPointsCount) * 3
for (let i = 0; i < newPointsCount; i++) {
const point = newUniquePoints[i]
const index = startIndex + i * 3
positions[index] = (point.x || 0) * this.pointScale
positions[index + 1] = (point.y || 0) * this.pointScale
positions[index + 2] = 0
}
this.pointCloudGeometry.getAttribute('position').needsUpdate = true
},
adjustCameraByBounds() {
const aspect = this.canvasSize.width / this.canvasSize.height
const pointRangeX = this.pointBounds.maxX - this.pointBounds.minX
const pointRangeY = this.pointBounds.maxY - this.pointBounds.minY
let baseViewSize = DEFAULT_VIEW_SIZE
if (this.allPoints.length > 0 && !(pointRangeX === 0 && pointRangeY === 0)) {
const requiredViewSizeX = pointRangeX * (1 + BOUND_MARGIN)
const requiredViewSizeY = pointRangeY * (1 + BOUND_MARGIN)
baseViewSize = Math.max(requiredViewSizeX / aspect, requiredViewSizeY)
// 确保视口不会太小
baseViewSize = Math.max(baseViewSize, DEFAULT_VIEW_SIZE)
}
this.baseViewSize = baseViewSize
this.viewSize = baseViewSize
this.camera.left = -this.viewSize * aspect / 2
this.camera.right = this.viewSize * aspect / 2
this.camera.top = this.viewSize / 2
this.camera.bottom = -this.viewSize / 2
this.camera.updateProjectionMatrix()
// 初始时将小车定位到中心
this.locateCarToCenter()
if (this.controls) {
this.controls.minZoom = MIN_ZOOM_RATIO
this.controls.maxZoom = MAX_ZOOM_RATIO
this.controls.minDistance = 50
this.controls.maxDistance = 200
this.controls.zoomToCursor = false
}
this.calculateCarSize()
},
startAnimationLoop() {
const animate = () => {
this.animationId = requestAnimationFrame(animate)
if (this.controls) {
this.controls.update()
}
this.renderer.render(this.scene, this.camera)
}
animate()
},
cleanup() {
this.isDestroyed = true
this.closeWebSocket()
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
if (this.controls) {
this.controls.dispose()
this.controls = null
}
if (this.scene) {
if (this.pointCloudMesh) {
this.scene.remove(this.pointCloudMesh)
}
if (this.carMesh) {
this.scene.remove(this.carMesh)
}
this.scene.clear()
this.scene = null
}
if (this.renderer) {
const container = this.$refs.canvasContainer
if (container && this.renderer.domElement) {
container.removeChild(this.renderer.domElement)
}
this.renderer.dispose()
this.renderer.forceContextLoss()
this.renderer = null
}
if (this.pointCloudGeometry) {
this.pointCloudGeometry.dispose()
this.pointCloudGeometry = null
}
if (this.pointCloudMaterial) {
this.pointCloudMaterial.map = null
this.pointCloudMaterial.dispose()
this.pointCloudMaterial = null
}
if (this.carTexture) {
this.carTexture.dispose()
this.carTexture = null
}
this.camera = null
this.carMesh = null
this.pointCloudMesh = null
}
}
}
</script>
<style lang="stylus" scoped>
.point-cloud-map
position relative
width 100%
height 100%
background-color rgba(4, 33, 58, 70%)
box-shadow inset 1px 1px 7px 2px #4d9bcd
overflow hidden
.canvas-container
display: flex;
align-items: center;
justify-content: center;
width: 100%
height: 100%
position: relative
z-index: 0
// 定位按钮样式
.locate-button
position: absolute
bottom: 20px
right: 20px
width: 40px
height: 40px
border-radius: 50%
background-color: rgba(255, 255, 255, 0.8)
border: none
color: #333
font-size: 18px
cursor: pointer
display: flex
align-items: center
justify-content: center
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2)
z-index: 100
transition: all 0.2s ease
&:hover
background-color: white
transform: scale(1.05)
&:active
transform: scale(0.95)
.loading-indicator
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
background-color: rgba(255, 255, 255, 0.8)
padding: .01rem .02rem
border-radius: 4px
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1)
font-size: .12rem
color: #333
z-index: 100
display: flex
align-items: center
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,542 +0,0 @@
<template>
<div class="page_container">
<div class="canvas-container">
<canvas
id="mapCanvas"
ref="mapCanvas"
@wheel="handleZoom"
@click="handleCanvasClick"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseUp"
></canvas>
<!-- <el-row type="flex" justify="end" class="map_tools">
<el-button type="primary" :disabled="zoomPercentage === 2" icon="el-icon-minus" size="mini" style="border: 0;border-radius: 0;" @click="zoomOut"></el-button>
<div class="zoom_data">{{ zoomPercentage }}%</div>
<el-button type="primary" :disabled="zoomPercentage === 200" icon="el-icon-plus" size="mini" style="border: 0;border-radius: 0;" @click="zoomIn"></el-button>
</el-row> -->
</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>编号</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>别名</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>X坐标</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>Y坐标</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>角度值</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>路径ID</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>起点编码</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>终点编码</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 carImage from '../../images/new/agv.png'
import { getMapInfoByCode, getRouteInfo, queryMapAllStation } from '../../config/getData.js'
import canvasZoomDrag from '../../config/canvasZoomDrag'
import { mapGetters } from 'vuex'
export default {
name: 'ModuleMap',
data () {
return {
canvas: null, // Canvas 元素
ctx: null, // Canvas 绘图上下文
mapData: null, // 地图数据
pathData: null, // 路径数据
pointData: null, // 点位数据
showPopup: false,
selectedPoint: {},
popupStyle: {left: '0px', top: '0px'},
selectedPointId: null,
cachedImages: {
map: null,
marker: null,
car: null
},
imageLoadStatus: {
map: false,
marker: false,
car: false
},
showPathPopup: false,
selectedPath: {route_id: null},
pathPopupStyle: { left: '0px', top: '0px' },
pathClickThreshold: 5
}
},
computed: {
...mapGetters(['serverUrl', 'carPosition'])
},
watch: {
carPosition: {
handler() {
this.redrawCanvas()
},
deep: true
}
},
mixins: [canvasZoomDrag],
mounted () {
this.preloadMarkerImage()
this.preloadCarImage()
this.loadAllDataInParallel()
document.addEventListener('click', this.handleDocumentClick)
},
beforeDestroy () {
document.removeEventListener('click', this.handleDocumentClick)
},
methods: {
preloadCarImage () {
if (this.cachedImages.car) {
this.imageLoadStatus.car = true
return
}
const img = new Image()
img.src = carImage
img.onload = () => {
this.cachedImages.car = img
this.imageLoadStatus.car = true
this.checkImagesLoadedAndRedraw()
}
img.onerror = () => {
console.error('小车图片加载失败')
this.imageLoadStatus.car = true
this.checkImagesLoadedAndRedraw()
}
},
preloadMarkerImage () {
if (this.cachedImages.marker) {
this.imageLoadStatus.marker = true
return
}
const img = new Image()
img.src = markerImage
img.onload = () => {
this.cachedImages.marker = img
this.imageLoadStatus.marker = true
this.checkImagesLoadedAndRedraw()
}
img.onerror = () => {
console.error('标记图片加载失败')
this.imageLoadStatus.marker = true
this.checkImagesLoadedAndRedraw()
}
},
loadMapImage () {
if (!this.mapData.mapImageAddress) {
return Promise.reject(new Error('地图数据缺失'))
}
if (this.cachedImages.map && this.imageLoadStatus.map) {
return Promise.resolve(this.cachedImages.map)
}
return new Promise((resolve, reject) => {
const img = new Image()
img.src = `${this.serverUrl}${this.mapData.mapImageAddress}`
// img.src = this.mapData.mapImageAddress
img.onload = () => {
this.cachedImages.map = img
this.imageLoadStatus.map = true
this.checkImagesLoadedAndRedraw()
resolve(img)
}
img.onerror = (error) => {
console.error('地图图片加载失败:', error)
this.imageLoadStatus.map = true
reject(error)
}
})
},
checkImagesLoadedAndRedraw () {
if (this.imageLoadStatus.map && this.imageLoadStatus.marker && this.imageLoadStatus.car) {
this.redrawCanvas()
}
},
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()
])
this.mapData = mapData
this.pathData = [...pathData]
this.pointData = this.filterPointData(pathData, stations)
this.initCanvas()
await this.loadMapImage()
this.loading.close()
} catch (e) {
this.$message.error(`数据加载失败: ${e.message || '未知错误'}`)
this.loading.close()
}
},
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')
this.canvas.width = this.mapData.width
this.canvas.height = this.mapData.height
this.redrawCanvas()
},
redrawCanvas: throttle(function () {
if (!this.ctx || !this.mapData) return
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.ctx.save()
// 绘制地图背景
if (this.cachedImages.map) {
this.ctx.drawImage(
this.cachedImages.map,
0, 0,
this.canvas.width,
this.canvas.height
)
} else {
this.ctx.fillStyle = '#ccc'
this.ctx.fillText('地图加载中...', 50, 50)
}
this.drawPath()
this.drawMarkers()
this.drawCar()
this.ctx.restore()
}, 30),
drawPath () {
if (!this.pathData.length) return
this.pathData.forEach(point => {
const startX = (point.start_x - this.mapData.x) / this.mapData.resolution
const startY = this.mapData.height - (point.start_y - this.mapData.y) / this.mapData.resolution
const endX = (point.end_x - this.mapData.x) / this.mapData.resolution
const endY = this.mapData.height - (point.end_y - this.mapData.y) / this.mapData.resolution
this.ctx.beginPath()
this.ctx.moveTo(startX, startY)
this.ctx.lineTo(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) return
const markerSize = 10 // 标记大小随缩放变化
this.pointData.forEach(point => {
const x = (point.x - this.mapData.x) / this.mapData.resolution
const y = this.mapData.height - (point.y - this.mapData.y) / this.mapData.resolution
// 绘制选中状态
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 if (this.cachedImages.marker) {
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 = 'white'
this.ctx.textAlign = 'center'
this.ctx.fillText(point.station_name, x, y + 18)
}
})
},
drawCar () {
if (!this.cachedImages.car || !this.mapData) return
const carX = (this.carPosition.x - this.mapData.x) / this.mapData.resolution
const carY = this.mapData.height - (this.carPosition.y - this.mapData.y) / this.mapData.resolution
this.ctx.save()
this.ctx.translate(carX, carY)
this.ctx.rotate(-this.carPosition.angle)
this.ctx.drawImage(
this.cachedImages.car,
-10,
-10,
20,
20
)
// this.ctx.restore()
},
handleCanvasClick (event) {
const rect = this.canvas.getBoundingClientRect()
const mouseX = (event.clientX - rect.left) / this.scale
const mouseY = (event.clientY - rect.top) / this.scale
// 1. 优先检测点位点击
let pointClicked = false
this.pointData.forEach(point => {
let x = (point.x - this.mapData.x) / this.mapData.resolution
let y = this.mapData.height - (point.y - this.mapData.y) / this.mapData.resolution
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
}
})
// 2. 只有点位未被点击时,才检测路径点击
if (!pointClicked) {
const clickedPath = this.detectPathClick(mouseX, mouseY)
if (clickedPath) {
this.handlePathSelect(clickedPath, event)
event.stopPropagation()
}
}
},
// 检测路径点击
detectPathClick (mouseX, mouseY) {
if (!this.pathData.length) return null
for (const path of this.pathData) {
// 计算路径线段的Canvas坐标
const startX = (path.start_x - this.mapData.x) / this.mapData.resolution
const startY = this.mapData.height - (path.start_y - this.mapData.y) / this.mapData.resolution
const endX = (path.end_x - this.mapData.x) / this.mapData.resolution
const endY = this.mapData.height - (path.end_y - this.mapData.y) / this.mapData.resolution
// 计算点击点到线段的距离
const distance = this.pointToLineDistance(
{ x: mouseX, y: mouseY },
{ x: startX, y: startY },
{ x: endX, y: endY }
)
// 距离小于阈值则认为点击了该路径
if (distance <= this.pathClickThreshold) {
return path
}
}
return null
},
// 计算点到线段的最短距离
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) {
this.resetPathSelection()
if (this.selectedPointId === point.station_id) {
this.resetSelection()
} else {
this.selectedPointId = point.station_id
this.selectedPoint = point
this.showPopup = true
this.calculatePopupPosition(event)
this.redrawCanvas()
}
},
// 处理路径选中
handlePathSelect (path, event) {
// 关闭点位弹窗(避免冲突)
this.resetPointSelection()
if (this.selectedPath.route_id === path.route_id) {
// 再次点击同一路径关闭弹窗
this.resetPathSelection()
} 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` }
},
handleDocumentClick () {
this.resetSelection()
},
resetPointSelection () {
if (this.selectedPointId) {
this.selectedPointId = null
this.selectedPoint = null
this.showPopup = false
}
},
// 重置路径选中状态
resetPathSelection () {
if (this.selectedPath) {
this.selectedPath = {route_id: null}
this.showPathPopup = false
}
},
resetSelection () {
this.resetPointSelection()
this.resetPathSelection()
this.redrawCanvas()
},
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
}
}
}
</script>
<style lang="stylus" scoped>
.canvas-container
position relative
display flex
justify-content: center;
align-items: center;
height 100%
background-color rgba(4, 33, 58, 70%)
box-shadow inset 1px 1px 7px 2px #4d9bcd
overflow hidden
.point-popup
position fixed
background rgba(0, 0, 0, 70%)
border 1px solid rgba(255, 255, 255, .3)
border-radius: 8px;
padding 10px
box-shadow 0 0px 4px 2px rgba(255,255,255,0.1)
z-index 100
min-width 160px
animation fadeIn 0.2s ease-out
h3
color #fff
font-size 14px
line-height 24px
text-align center
p
color #fff
font-size 14px
line-height 24px
text-align center
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
</style>

View File

@@ -30,7 +30,7 @@
<div class="error-tips-t">{{$t('ErrorPrompt')}}{{ exception }}</div>
</div>
</div>
<el-row type="flex" class="relative main-conatiner" :style="{'paddingTop': exception !== '' ? '.3rem' : '0', 'paddingBottom': taskSeq.length > 0 ? '.6rem': '0'}">
<el-row type="flex" class="relative main-conatiner" :style="{'paddingTop': exception !== '' ? '.4rem' : '0', 'paddingBottom': taskSeq.length > 0 ? '.6rem': '0'}">
<div class="absolute hud_left"></div>
<div class="absolute hud_left hud_right"></div>
<router-view></router-view>
@@ -107,7 +107,9 @@ export default {
// "en_error_description": null,
// "error_type": 1}
// ],
// forkTipObstacles: '1'
// forkTipObstacles: '1',
// auto_loop_enable: '0',
// auto_back_enable: ''
// },
taskSeq: [],
currentStep: null,
@@ -184,6 +186,11 @@ export default {
},
// 初始化WebSocket连接
initWebSocket () {
this.loading = this.$loading({
lock: true,
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.6)'
})
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
const wsHost = this.serverUrl.replace(/^https?:\/\//, '')
const lang = this.$i18n.locale.slice(0, 2)
@@ -194,6 +201,16 @@ export default {
this.websocket.onmessage = (event) => {
try {
const res = JSON.parse(event.data)
if (!res.data.rcsConnected && !res.data.vehicleConnected) {
this.$message.error(this.$t('Dispatchvehiclenotconnected'))
} else if (!res.data.rcsConnected && res.data.vehicleConnected) {
this.$message.error(this.$t('Dispatchnotconnected'))
} else if (!res.data.vehicleConnected && res.data.rcsConnected) {
this.$message.error(this.$t('Vehiclenotconnected'))
}
if (res.data.rcsConnected && res.data.vehicleConnected) {
this.loading.close()
}
this.handleWebSocketMessage(res)
} catch (error) {
console.error('WebSocket消息解析失败:', error)
@@ -203,6 +220,7 @@ export default {
this.$message.error(this.$t('WebSocketerror') + ':', error)
}
this.websocket.onclose = () => {
this.loading.close()
this.topInfo = {}
this.taskSeq = []
this.currentStep = null
@@ -233,7 +251,7 @@ export default {
this.reconnectTimer = setTimeout(() => {
this.$message.error(this.$t('AttreconnectWebSocket'))
this.initWebSocket()
}, 5000) // 5秒后重连
}, 3000) // 3秒后重连
}
}
}
@@ -295,8 +313,8 @@ export default {
top .48rem
left 0
width 100%
height .3rem
line-height .3rem
height .4rem
line-height .4rem
overflow hidden
white-space nowrap
background-color rgba(253, 246, 236, .4)
@@ -310,7 +328,7 @@ export default {
margin-right .05rem
.error-tips-t
display inline-block
font-size .16rem
font-size .24rem
color #e6bb3c
.task_wraper
position fixed

View File

@@ -78,7 +78,7 @@
// element css
.el-loading-spinner i
font-size .24rem
font-size .5rem
color #fff
.el-button
font-size .2rem
@@ -255,7 +255,7 @@
.driver-popover.driverjs-theme .driver-popover-description,
.driver-popover.driverjs-theme .driver-popover-progress-text {
color: #000;
font-size: 18px;
font-size: 22px;
}
.driver-popover.driverjs-theme button {
@@ -265,7 +265,7 @@
color: #ffffff;
border: 2px solid #000;
text-shadow: none;
font-size: 14px;
font-size: 20px;
padding: 5px 8px;
border-radius: 6px;
}
@@ -346,7 +346,7 @@
.main-conatiner
width 100%
height calc(100% - .48rem)
padding .3rem 0 .6rem 0
padding .4rem 0 .6rem 0
.page_container
_wh(96%, 100%)
padding .09rem

View File

@@ -4,14 +4,18 @@ const state = {
carPosition: {x: '', y: '', angle: ''},
isTop: false,
errorData: [],
backIoStatus: '0'
backIoStatus: '0',
autoLoopEnable: '0',
autoBackEnable: ''
}
const getters = {
carPosition: state => state.carPosition,
isTop: state => state.isTop,
errorData: state => state.errorData,
backIoStatus: state => state.backIoStatus
backIoStatus: state => state.backIoStatus,
autoLoopEnable: state => state.autoLoopEnable,
autoBackEnable: state => state.autoBackEnable
}
const actions = {
@@ -37,6 +41,12 @@ const mutations = {
if (Object.prototype.hasOwnProperty.call(data, 'forkTipObstacles')) {
state.backIoStatus = data.forkTipObstacles
}
if (Object.prototype.hasOwnProperty.call(data, 'auto_loop_enable')) {
state.autoLoopEnable = data.auto_loop_enable
}
if (Object.prototype.hasOwnProperty.call(data, 'auto_back_enable')) {
state.autoBackEnable = data.auto_back_enable
}
}
}