平台概览车辆移动

This commit is contained in:
renna 2025-07-08 10:50:41 +08:00
parent 5d1852939e
commit a9c66a376a
32 changed files with 1044 additions and 411 deletions

View File

@ -122,3 +122,6 @@ function getUserOptions() {
});
}
使用SockJS + STOMP协议
滑出蓝色 滑入黄色

View File

@ -15,11 +15,10 @@
},
"dependencies": {
"@element-plus/icons-vue": "2.0.10",
"@stomp/stompjs": "^7.1.1",
"@supermap/iclient-ol": "^11.1.1",
"@vueuse/core": "9.5.0",
"axios": "0.27.2",
"ol": "6.15.1",
"proj4": "^2.17.0",
"echarts": "5.4.0",
"element-plus": "2.2.21",
"file-saver": "2.0.5",
@ -28,7 +27,10 @@
"jsencrypt": "3.3.1",
"leaflet-minimap": "^3.6.1",
"nprogress": "0.2.0",
"ol": "6.15.1",
"pinia": "2.0.22",
"proj4": "^2.17.0",
"sockjs-client": "^1.6.1",
"vue": "3.2.45",
"vue-cropper": "1.0.3",
"vue-router": "4.1.4"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/images/znzBg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 590 B

View File

@ -344,7 +344,8 @@ defineExpose({
zoomOut,
compass,
resetView,
getCurrentMapState //
getCurrentMapState, //
map //
});
</script>

View File

@ -161,9 +161,12 @@ import Overlay from 'ol/Overlay';
import labelBg from '../../../assets/images/label_bg.png';
//
import car1Icon from '../../../assets/images/Aircraft.png';
import car1Icon from '../../../assets/images/Aircraft.png'; //
import car1Icon1 from '../../../assets/images/Aircraft1.png'; //
import car2Icon from '../../../assets/images/noPeopleCar.png';
import noPeopleCarIcon from '../../../assets/images/noPeopleCar.png';
import airportBg from '../../../assets/images/airport_bg.png'; //
import airportOutBg from '../../../assets/images/airport_out.png'; //
// props
const props = defineProps({
@ -191,7 +194,7 @@ const exampleVehicleLayers = ref([
name: '滑入航空器',
visible: true,
showLabel: true,
icon: car1Icon,
icon: car1Icon1,
type: 'aircraft-in',
layer: null,
features: [
@ -356,7 +359,18 @@ function initExampleLayers() {
//
const labelDiv = document.createElement('div');
labelDiv.className = `custom-label label-${layerConfig.type}`;
labelDiv.style.backgroundImage = `url(${labelBg})`;
// - 使(airport_out.png)使(airport_bg.png)
let backgroundImage;
if (layerConfig.type === 'aircraft-in') {
backgroundImage = airportOutBg; // -
} else if (layerConfig.type === 'aircraft-out') {
backgroundImage = airportBg; // -
} else {
backgroundImage = labelBg; //
}
labelDiv.style.backgroundImage = `url(${backgroundImage})`;
labelDiv.style.backgroundRepeat = 'no-repeat';
labelDiv.style.backgroundSize = '100% 100%';
labelDiv.style.padding = '3px 10px';
@ -436,7 +450,7 @@ function selectTextStyle(style) {
const allLabels = document.querySelectorAll('.custom-label');
allLabels.forEach(label => {
//
label.style.backgroundImage = "url('../../../assets/images/label_bg.png')";
label.style.backgroundImage = labelBg;
label.style.backgroundRepeat = "no-repeat";
label.style.backgroundSize = "100% 100%";
@ -457,13 +471,13 @@ function selectTextStyle(style) {
default:
//
if (label.classList.contains('label-aircraft-in')) {
// label.style.backgroundColor = 'rgba(52, 122, 226, 0.7)';
// label.style.borderColor = '#347AE2';
label.style.color = '#fff';
} else if (label.classList.contains('label-aircraft-out')) {
// label.style.backgroundColor = 'rgba(245, 231, 79, 0.7)';
// label.style.borderColor = '#E4CB0D';
label.style.color = '#333';
label.style.color = '#333'; // -
} else if (label.classList.contains('label-aircraft-out')) {
// label.style.backgroundColor = 'rgba(52, 122, 226, 0.7)';
// label.style.borderColor = '#347AE2';
label.style.color = '#fff'; // -
} else if (label.classList.contains('label-car')) {
// label.style.backgroundColor = 'rgba(37, 37, 37, 0.7)';
// label.style.borderColor = '#484848';
@ -513,13 +527,13 @@ function updateAllLabelsStyle(styleName) {
default:
//
if (isAircraftIn) {
// label.style.backgroundColor = 'rgba(52, 122, 226, 0.7)';
// label.style.borderColor = '#347AE2';
label.style.color = '#fff';
} else if (isAircraftOut) {
// label.style.backgroundColor = 'rgba(245, 231, 79, 0.7)';
// label.style.borderColor = '#E4CB0D';
label.style.color = '#333';
label.style.color = '#333'; // -
} else if (isAircraftOut) {
// label.style.backgroundColor = 'rgba(52, 122, 226, 0.7)';
// label.style.borderColor = '#347AE2';
label.style.color = '#fff'; // -
} else if (isCar) {
// label.style.backgroundColor = 'rgba(37, 37, 37, 0.7)';
// label.style.borderColor = '#484848';
@ -1126,20 +1140,20 @@ function toggleExampleLabel(layer) {
transform: translateX(-50%);
}
/* 滑入航空器 */
/* 滑入航空器 - 黄色背景,黑色文字 */
.label-aircraft-in {
background-color: rgba(52, 122, 226, 0.7);
border-color: #347AE2;
color: #fff;
}
/* 滑出航空器 */
.label-aircraft-out {
background-color: rgba(245, 231, 79, 0.7);
border-color: #E4CB0D;
color: #333;
}
/* 滑出航空器 - 蓝色背景,白色文字 */
.label-aircraft-out {
background-color: rgba(52, 122, 226, 0.7);
border-color: #347AE2;
color: #fff;
}
/* 无人车 */
.label-car {
background-color: rgba(37, 37, 37, 0.7);

View File

@ -1,7 +1,7 @@
<template>
<div class="map-info">
<!-- <input type="text" readonly :value="xyText" />
<input type="text" readonly :value="wgs84Text" /> -->
<input type="text" readonly :value="xyText" />
<input type="text" readonly :value="wgs84Text" />
<!-- <input type="text" readonly :value="pixelText" /> -->
</div>
</template>

View File

@ -1,35 +1,108 @@
<template>
<div class="zoom-control">
<LayerSwitcher class="layer-switcher" :map="map" @layerChange="onLayerChange" ref="layerSwitcherRef" />
<LayerSwitcher
class="layer-switcher"
:map="map"
@layerChange="onLayerChange"
ref="layerSwitcherRef"
/>
<div class="compass" @click="onCompass"></div>
<div
class="compass-container"
:style="{ transform: `rotate(${-rotation}deg)` }"
>
<div class="rotate-left" @click="rotateLeft" title="向右旋转90°"></div>
<div class="rotate-right" @click="rotateRight" title="向左旋转90°"></div>
</div>
<div class="zoom-in" @click="onZoomIn"></div>
<div class="zoom-out" @click="onZoomOut"></div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import LayerSwitcher from './LayerSwitcher.vue';
import { ref, onMounted, onUnmounted, watch } from "vue";
import LayerSwitcher from "./LayerSwitcher.vue";
//
import znzBgImg from "../../../assets/images/znzBg.png";
import needleImg from "../../../assets/images/znz.png";
import leftArrowImg from "../../../assets/images/left_arrow.png";
import rightArrowImg from "../../../assets/images/right_arrow.png";
// props
const props = defineProps({
map: Object, //
resetView: {
type: Function,
default: null
}
default: null,
},
});
//
const layerSwitcherRef = ref(null);
// ()
const rotation = ref(0);
//
const emit = defineEmits(['compass', 'zoomIn', 'zoomOut', 'layerChange']);
const emit = defineEmits([
"compass",
"zoomIn",
"zoomOut",
"layerChange",
"rotate",
]);
//
function updateRotation() {
if (props.map && props.map.getView()) {
// OpenLayers
rotation.value = (props.map.getView().getRotation() * 180) / Math.PI;
}
}
//
function resetRotation() {
if (props.map) {
// 0
props.map.getView().animate({
rotation: 0,
duration: 300,
});
emit("rotate", 0);
}
}
//
function rotateLeft() {
if (props.map) {
const currentRotation = props.map.getView().getRotation();
const newRotation = currentRotation - Math.PI / 2; // 90
props.map.getView().animate({
rotation: newRotation,
duration: 300,
});
emit("rotate", (newRotation * 180) / Math.PI);
}
}
//
function rotateRight() {
if (props.map) {
const currentRotation = props.map.getView().getRotation();
const newRotation = currentRotation + Math.PI / 2; // 90
props.map.getView().animate({
rotation: newRotation,
duration: 300,
});
emit("rotate", (newRotation * 180) / Math.PI);
}
}
// /
function onCompass() {
emit('compass');
emit("compass");
if (props.resetView) {
props.resetView();
} else if (props.map) {
@ -42,21 +115,76 @@ function onCompass() {
//
function onZoomIn() {
emit('zoomIn');
emit("zoomIn");
if (props.map) {
const currentZoom = props.map.getView().getZoom();
props.map.getView().animate({
zoom: currentZoom + 1,
duration: 250,
});
}
}
//
function onZoomOut() {
emit('zoomOut');
emit("zoomOut");
if (props.map) {
const currentZoom = props.map.getView().getZoom();
props.map.getView().animate({
zoom: currentZoom - 1,
duration: 250,
});
}
}
//
function onLayerChange(layerInfo) {
console.log('ZoomControl收到图层变化:', layerInfo);
console.log("ZoomControl收到图层变化:", layerInfo);
//
emit('layerChange', layerInfo);
emit("layerChange", layerInfo);
}
//
let rotationListener = null;
onMounted(() => {
if (props.map) {
//
updateRotation();
//
rotationListener = props.map
.getView()
.on("change:rotation", updateRotation);
}
});
onUnmounted(() => {
//
if (rotationListener) {
rotationListener.dispose();
}
});
//
watch(
() => props.map,
(newMap) => {
if (newMap) {
//
updateRotation();
//
if (rotationListener) {
rotationListener.dispose();
}
//
rotationListener = newMap.getView().on("change:rotation", updateRotation);
}
}
);
//
defineExpose({
//
@ -65,7 +193,13 @@ defineExpose({
return layerSwitcherRef.value.setLayerVisibility(layerName, visible);
}
return false;
}
},
//
resetRotation,
//
getRotation() {
return rotation.value;
},
});
</script>
@ -83,15 +217,64 @@ defineExpose({
gap: 25px;
}
.compass {
.compass-container {
position: relative;
width: 40px;
height: 40px;
background: url("../../../assets/images/znz.png") no-repeat;
background-size: 100% 100%;
cursor: pointer;
z-index: 3000;
background: url("../../../assets/images/znzBg.png") no-repeat;
background-size: 100% 100%;
}
.layer-switcher{
.rotation-controls {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 3002;
}
.rotate-left,
.rotate-right {
width: 24px;
height: 24px;
position: absolute;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
}
.rotate-left {
left: -3px;
background: url("../../../assets/images/left_arrow.png") no-repeat;
background-size: 100% 100%;
z-index: 5000;
}
.rotate-right {
right: -3px;
background: url("../../../assets/images/right_arrow.png") no-repeat;
background-size: 100% 100%;
}
.rotate-left:hover,
.rotate-left:active {
left: -3px;
background: url("../../../assets/images/left_arrow_active.png") no-repeat;
background-size: 100% 100%;
}
.rotate-right:hover,
.rotate-right:active {
right: -3px;
background: url("../../../assets/images/right_arrow_active.png") no-repeat;
background-size: 100% 100%;
}
.layer-switcher {
z-index: 3000;
width: 40px;
height: 40px;
@ -99,6 +282,11 @@ defineExpose({
background-size: 100% 100%;
cursor: pointer;
}
.layer-switcher:hover,
.layer-switcher:active {
background: url("../../../assets/images/layerActive.png") no-repeat;
background-size: 100% 100%;
}
.zoom-in {
background: url("../../../assets/images/zoomOut.png") no-repeat;
@ -108,6 +296,11 @@ defineExpose({
height: 40px;
cursor: pointer;
}
.zoom-in:hover,
.zoom-in:active {
background: url("../../../assets/images/zoomOutActive.png") no-repeat;
background-size: 100% 100%;
}
.zoom-out {
background: url("../../../assets/images/zoomIn.png") no-repeat;
@ -117,15 +310,43 @@ defineExpose({
height: 40px;
cursor: pointer;
}
.zoom-out:hover,
.zoom-out:active {
background: url("../../../assets/images/zoomInActive.png") no-repeat;
background-size: 100% 100%;
}
/* 响应式调整 */
@media (max-width: 768px) {
.zoom-in, .zoom-out, .compass {
.zoom-in,
.zoom-out,
.compass-container,
.compass-bg,
.compass-needle {
width: 28px;
height: 28px;
}
.zoom-in,
.zoom-out {
background-size: 80%;
background-position: center center;
background-repeat: no-repeat;
}
.rotate-left,
.rotate-right {
width: 18px;
height: 18px;
}
.rotate-left {
left: -18px;
}
.rotate-right {
right: -18px;
}
}
</style>

View File

@ -0,0 +1,454 @@
<template>
<div class="vehicle-movement-control">
<!-- 连接状态指示器 -->
<div class="ws-status" :class="{ connected: wsConnected }">
<span v-if="wsConnected">已连接</span>
<span v-else>未连接</span>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { Vector as VectorSource } from 'ol/source';
import { Vector as VectorLayer } from 'ol/layer';
import { Style, Icon } from 'ol/style';
import Feature from 'ol/Feature';
import Point from 'ol/geom/Point';
import Overlay from 'ol/Overlay';
import { transform } from 'ol/proj';
// SockJSpolyfill
if (typeof window !== 'undefined' && !window.global) {
window.global = window;
}
let SockJS;
let StompJS;
//
import carIcon from '../../../assets/images/noPeopleCar.png';
import aircraftIcon from '../../../assets/images/Aircraft.png';
import aircraft1Icon from '../../../assets/images/Aircraft1.png';
import labelBg from '../../../assets/images/label_bg.png';
import airportBg from '../../../assets/images/airport_bg.png';
import airportOutBg from '../../../assets/images/airport_out.png';
// props
const props = defineProps({
map: Object
});
// WebSocket
const wsConnected = ref(false);
let stompClient = null;
//
const vehicles = ref({});
let vehicleLayer = null;
let vehicleSource = null;
//
function createVehicleLayer() {
if (!props.map) return;
//
vehicleSource = new VectorSource();
//
vehicleLayer = new VectorLayer({
source: vehicleSource,
zIndex: 20, //
});
//
props.map.addLayer(vehicleLayer);
console.log('车辆移动图层已创建');
//
setupMapListeners();
}
//
function updateVehiclePosition(vehicleData) {
if (!vehicleSource || !props.map) return;
const { object_id, object_type, position, heading, speed } = vehicleData;
console.log(`接收到位置数据: ${object_id}`, position);
// ()
// WGS84使
let coordinates;
try {
coordinates = transform(
[position.longitude, position.latitude],
'EPSG:4326',
props.map.getView().getProjection()
);
} catch (e) {
console.error('坐标转换失败:', e);
coordinates = [position.x || 0, position.y || 0]; // 使
}
//
let feature = vehicleSource.getFeatureById(object_id);
//
const isAircraftIn = object_type.toLowerCase().includes('aircraft') && !object_id.toLowerCase().includes('ac001');
const isAircraftOut = object_id.toLowerCase().includes('ac001');
const isAircraft = isAircraftIn || isAircraftOut;
// -
let iconSrc;
if (isAircraftIn) {
iconSrc = aircraftIcon; //
} else if (isAircraftOut) {
iconSrc = aircraft1Icon; //
} else {
iconSrc = carIcon; //
}
//
const style = new Style({
image: new Icon({
src: iconSrc,
scale: 1.5,
anchor: [0.5, 0.5],
rotation: heading ? (heading * Math.PI / 180) : 0, //
})
});
if (!feature) {
// Feature
feature = new Feature({
geometry: new Point(coordinates),
name: `${object_type} ${object_id}`,
type: object_type,
speed: speed,
isAircraftIn: isAircraftIn,
isAircraftOut: isAircraftOut,
isAircraft: isAircraft
});
feature.setId(object_id);
feature.setStyle(style);
vehicleSource.addFeature(feature);
//
vehicles.value[object_id] = {
id: object_id,
type: object_type,
position: coordinates,
heading: heading,
speed: speed,
feature: feature,
isAircraftIn: isAircraftIn,
isAircraftOut: isAircraftOut,
isAircraft: isAircraft
};
// - 使updateVehicleLabel
updateVehicleLabel(object_id, coordinates, speed);
} else {
// Feature
feature.getGeometry().setCoordinates(coordinates);
feature.setStyle(style);
//
vehicles.value[object_id] = {
...vehicles.value[object_id],
position: coordinates,
heading: heading,
speed: speed,
isAircraftIn: isAircraftIn,
isAircraftOut: isAircraftOut,
isAircraft: isAircraft
};
//
updateVehicleLabel(object_id, coordinates, speed);
}
}
//
function updateVehicleLabel(id, position, speed) {
if (!props.map || !vehicles.value[id]) return;
console.log(`更新标签位置: ${id}, 位置: [${position[0]}, ${position[1]}]`);
// overlay
if (vehicles.value[id].overlay) {
props.map.removeOverlay(vehicles.value[id].overlay);
}
//
const vehicle = vehicles.value[id];
const isAircraftIn = vehicle.isAircraftIn;
const isAircraftOut = vehicle.isAircraftOut;
// - 使(airport_out.png)使(airport_bg.png)
let backgroundImage;
if (isAircraftOut) {
backgroundImage = airportOutBg; // -
} else if (isAircraftIn) {
backgroundImage = airportBg; // -
} else {
backgroundImage = labelBg; //
}
//
const labelDiv = document.createElement('div');
labelDiv.className = `vehicle-label ${isAircraftIn ? 'vehicle-aircraft-in' : ''} ${isAircraftOut ? 'vehicle-aircraft-out' : ''} ${(!isAircraftIn && !isAircraftOut) ? 'vehicle-car' : ''}`;
labelDiv.innerHTML = `${id} ${speed} km/h`;
labelDiv.style.backgroundImage = `url(${backgroundImage})`;
labelDiv.style.backgroundSize = '100% 100%';
labelDiv.style.color = '#fff';
labelDiv.style.padding = '5px 10px';
// Overlay
const overlay = new Overlay({
element: labelDiv,
position: position,
positioning: 'bottom-center', //
offset: [0, -30], //
stopEvent: false,
insertFirst: true, // DOM
autoPan: false, //
});
//
vehicles.value[id].overlay = overlay;
vehicles.value[id].labelDiv = labelDiv;
//
props.map.addOverlay(overlay);
}
//
function setupMapListeners() {
if (!props.map) return;
//
props.map.on('moveend', () => {
//
Object.keys(vehicles.value).forEach(id => {
const vehicle = vehicles.value[id];
if (vehicle.feature) {
const coordinates = vehicle.feature.getGeometry().getCoordinates();
updateVehicleLabel(id, coordinates, vehicle.speed);
}
});
});
}
// WebSocket
function connectWebSocket() {
if (!SockJS || !StompJS) {
console.error('SockJS或StompJS库未加载');
return;
}
try {
// 使SockJSSTOMP
const socket = new SockJS('http://10.0.0.124:8081/ws');
stompClient = new StompJS.Client({
webSocketFactory: () => socket,
connectHeaders: {},
debug: function(str) {
console.log('STOMP Debug: ' + str);
},
reconnectDelay: 3000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});
//
stompClient.onConnect = (frame) => {
console.log('WebSocket连接成功');
wsConnected.value = true;
//
stompClient.subscribe('/topic/realtime', (message) => {
handleWsMessage(message.body);
});
};
stompClient.onStompError = (frame) => {
console.error('STOMP错误:', frame.headers['message']);
console.error('详细信息:', frame.body);
wsConnected.value = false;
};
stompClient.onWebSocketClose = () => {
console.log('WebSocket连接关闭');
wsConnected.value = false;
};
stompClient.onWebSocketError = (error) => {
console.error('WebSocket错误:', error);
wsConnected.value = false;
};
//
stompClient.activate();
} catch (error) {
console.error('创建WebSocket连接失败:', error);
}
}
// WebSocket
function handleWsMessage(message) {
try {
const data = JSON.parse(message);
//
switch (data.type) {
case 'position_update':
updateVehiclePosition(data.payload);
break;
case 'vehicle_command':
console.log('收到车辆控制指令:', data.payload);
break;
default:
//
break;
}
} catch (e) {
console.error('处理WebSocket消息出错:', e);
}
}
//
function cleanup() {
// WebSocket
if (stompClient) {
stompClient.deactivate();
stompClient = null;
}
//
if (vehicleLayer && props.map) {
props.map.removeLayer(vehicleLayer);
vehicleLayer = null;
}
//
if (props.map) {
Object.values(vehicles.value).forEach(vehicle => {
if (vehicle.overlay) {
console.log(`移除标签: ${vehicle.id}`);
props.map.removeOverlay(vehicle.overlay);
vehicle.overlay = null;
}
});
}
//
vehicles.value = {};
}
//
watch(() => props.map, (newMap) => {
if (newMap) {
createVehicleLayer();
}
});
//
onMounted(async () => {
// SockJSStompJS
try {
// 使
if (window.SockJS) {
SockJS = window.SockJS;
} else {
//
const sockjsModule = await import('sockjs-client');
SockJS = sockjsModule.default;
}
if (window.StompJs) {
StompJS = window.StompJs;
} else {
const stompModule = await import('@stomp/stompjs');
StompJS = stompModule;
}
// WebSocket
if (props.map) {
createVehicleLayer();
}
connectWebSocket();
} catch (error) {
console.error('加载WebSocket库失败:', error);
}
});
//
onUnmounted(() => {
cleanup();
});
//
defineExpose({
updateVehiclePosition,
wsConnected
});
</script>
<style scoped>
.vehicle-movement-control {
position: absolute;
bottom: 20px;
right: 20px;
z-index: 1000;
}
.ws-status {
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
background-color: rgba(255, 87, 34, 0.8);
color: white;
transition: background-color 0.3s ease;
}
.ws-status.connected {
background-color: rgba(76, 175, 80, 0.8);
}
/* 车辆标签样式 */
:deep(.vehicle-label) {
position: absolute;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
pointer-events: none;
transform: translateX(-50%);
text-align: center;
background-repeat: no-repeat;
background-position: center;
min-width: 80px;
min-height: 24px;
display: flex;
align-items: center;
justify-content: center;
z-index: 30; /* 确保标签在图标上方 */
transition: transform 0.3s ease, left 0.3s ease, top 0.3s ease; /* 平滑移动效果 */
will-change: transform, left, top; /* 提示浏览器优化这些属性的变化 */
bottom: 0; /* 确保标签底部对齐定位点 */
}
:deep(.vehicle-aircraft-in) {
/* 滑入航空器标签特殊样式,如果需要 */
}
:deep(.vehicle-aircraft-out) {
/* 滑出航空器标签特殊样式,如果需要 */
}
:deep(.vehicle-car) {
/* 车辆标签特殊样式,如果需要 */
}
</style>

View File

@ -1,388 +1,332 @@
<!DOCTYPE html>
<html>
<head>
<title>WebSocket 测试客户端 - STOMP + SockJS</title>
<meta charset="UTF-8">
<title>冲突检测WebSocket测试工具</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 20px;
}
h2 {
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.test-section {
border: 1px solid #ddd;
border-radius: 8px;
margin: 20px 0;
padding: 20px;
background: #fafafa;
}
.test-section h2 {
color: #555;
margin-top: 0;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
}
#messages {
width: 100%;
height: 600px;
overflow-y: auto;
border: 1px solid #ddd;
margin: 20px 0;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 12px;
background-color: #1e1e1e;
color: #fff;
border-radius: 4px;
.control-group {
margin: 15px 0;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.controls {
margin: 20px 0;
text-align: center;
.control-group label {
min-width: 120px;
font-weight: bold;
}
button {
padding: 10px 20px;
margin: 0 10px;
border: none;
select, input, button {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.connect-btn {
background-color: #28a745;
color: white;
}
.disconnect-btn {
background-color: #dc3545;
color: white;
}
.clear-btn {
background-color: #6c757d;
button {
background: #007bff;
color: white;
border: none;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
opacity: 0.8;
background: #0056b3;
}
button:disabled {
opacity: 0.5;
background: #6c757d;
cursor: not-allowed;
}
.status {
padding: 10px;
border-radius: 5px;
margin: 10px 0;
border-radius: 4px;
text-align: center;
font-weight: bold;
}
.status.connected {
background-color: #d4edda;
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.disconnected {
background-color: #f8d7da;
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.message-stats {
display: flex;
justify-content: space-around;
margin: 20px 0;
padding: 15px;
background-color: #f8f9fa;
.log-container {
border: 1px solid #ddd;
border-radius: 4px;
}
.stat-item {
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #007bff;
}
.stat-label {
height: 300px;
overflow-y: auto;
padding: 10px;
background: white;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
color: #6c757d;
}
/* 消息类型样式 */
.error { color: #ff6b6b; }
.success { color: #51cf66; }
.info { color: #74c0fc; }
.position { color: #ffd43b; }
.warning { color: #ff8c42; }
.command { color: #da77f2; }
.traffic-light { color: #69db7c; }
.collision { color: #ff6b6b; font-weight: bold; }
.system { color: #91a7ff; }
.log-entry {
margin: 2px 0;
padding: 2px 5px;
border-radius: 3px;
}
.log-info { background: #f8f9fa; }
.log-success { background: #d4edda; color: #155724; }
.log-error { background: #f8d7da; color: #721c24; }
.log-warning { background: #fff3cd; color: #856404; }
</style>
<!-- 引入SockJS和STOMP客户端库 -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.0/bundles/stomp.umd.min.js"></script>
</head>
<body>
<div class="container">
<h2>WebSocket 实时消息测试客户端</h2>
<p><strong>连接地址:</strong> http://10.0.0.124:8082/ws</p>
<p><strong>订阅主题:</strong> /topic/realtime</p>
<h1>冲突检测WebSocket测试工具</h1>
<div id="status" class="status disconnected">未连接</div>
<!-- 冲突检测WebSocket测试 -->
<div class="test-section">
<h2>🚗 冲突检测WebSocket (原生协议)</h2>
<div class="controls">
<button id="connectBtn" class="connect-btn" onclick="connect()">连接</button>
<button id="disconnectBtn" class="disconnect-btn" onclick="disconnect()" disabled>断开</button>
<button class="clear-btn" onclick="clearMessages()">清空日志</button>
<div class="control-group">
<label>服务器地址:</label>
<select id="collisionServerSelect">
<option value="ws://10.0.0.124:8080/collision">冲突检测WebSocket</option>
</select>
</div>
<div class="message-stats">
<div class="stat-item">
<div id="totalMessages" class="stat-number">0</div>
<div class="stat-label">总消息数</div>
<div class="control-group">
<button id="collisionConnectBtn" onclick="connectCollisionWebSocket()">连接</button>
<button id="collisionDisconnectBtn" onclick="disconnectCollisionWebSocket()" disabled>断开</button>
<button onclick="sendCollisionPing()">发送心跳</button>
<button onclick="sendCollisionSubscribe()">订阅消息</button>
</div>
<div class="stat-item">
<div id="positionMessages" class="stat-number">0</div>
<div class="stat-label">位置更新</div>
</div>
<div class="stat-item">
<div id="warningMessages" class="stat-number">0</div>
<div class="stat-label">预警消息</div>
</div>
<div class="stat-item">
<div id="commandMessages" class="stat-number">0</div>
<div class="stat-label">控制指令</div>
<div class="status disconnected" id="collisionStatus">状态:未连接</div>
<div>
<h4>日志:</h4>
<div class="log-container" id="collisionLog"></div>
</div>
</div>
<div id="messages"></div>
<!-- 控制面板 -->
<div class="test-section">
<h2>🎛️ 控制面板</h2>
<div class="control-group">
<button onclick="clearLogs()">清空日志</button>
<button onclick="showConnectionStatus()">显示连接状态</button>
<button onclick="performStressTest()">压力测试</button>
</div>
</div>
</div>
<script>
let stompClient = null;
const messagesDiv = document.getElementById('messages');
const statusDiv = document.getElementById('status');
const connectBtn = document.getElementById('connectBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
// 全局变量
let collisionWebSocket = null;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
// 消息计数器
let messageStats = {
total: 0,
position_update: 0,
collision_warning: 0,
vehicle_command: 0,
traffic_light_status: 0,
system_alert: 0
};
function updateStats() {
document.getElementById('totalMessages').textContent = messageStats.total;
document.getElementById('positionMessages').textContent = messageStats.position_update;
document.getElementById('warningMessages').textContent = messageStats.collision_warning;
document.getElementById('commandMessages').textContent = messageStats.vehicle_command;
// 通用日志函数
function log(containerId, message, type = 'info') {
const container = document.getElementById(containerId);
const timestamp = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.className = `log-entry log-${type}`;
entry.innerHTML = `[${timestamp}] ${message}`;
container.appendChild(entry);
container.scrollTop = container.scrollHeight;
}
function log(message, type = 'info') {
const div = document.createElement('div');
div.className = type;
div.innerHTML = `<span style="color: #888;">[${new Date().toLocaleTimeString()}]</span> ${message}`;
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
// 更新连接状态
function updateStatus(statusId, connected, message = '') {
const statusDiv = document.getElementById(statusId);
statusDiv.className = `status ${connected ? 'connected' : 'disconnected'}`;
statusDiv.textContent = `状态:${connected ? '已连接' : '未连接'}${message ? ' - ' + message : ''}`;
}
function clearMessages() {
messagesDiv.innerHTML = '';
messageStats = {
total: 0,
position_update: 0,
collision_warning: 0,
vehicle_command: 0,
traffic_light_status: 0,
system_alert: 0
};
updateStats();
}
function updateConnectionStatus(connected) {
if (connected) {
statusDiv.textContent = '已连接';
statusDiv.className = 'status connected';
connectBtn.disabled = true;
disconnectBtn.disabled = false;
} else {
statusDiv.textContent = '未连接';
statusDiv.className = 'status disconnected';
connectBtn.disabled = false;
disconnectBtn.disabled = true;
}
}
function connect() {
if (stompClient && stompClient.connected) {
log('已经连接,请先断开', 'error');
// =================== 冲突检测WebSocket ===================
function connectCollisionWebSocket() {
if (collisionWebSocket && collisionWebSocket.readyState === WebSocket.OPEN) {
log('collisionLog', 'WebSocket已经连接', 'warning');
return;
}
const serverUrl = document.getElementById('collisionServerSelect').value;
log('collisionLog', `连接到: ${serverUrl}`, 'info');
try {
// 创建SockJS连接
const socket = new SockJS('http://10.0.0.124:8081/ws');
stompClient = new StompJs.Client({
webSocketFactory: () => socket,
connectHeaders: {},
debug: function (str) {
console.log('STOMP Debug: ' + str);
},
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});
collisionWebSocket = new WebSocket(serverUrl);
stompClient.onConnect = function (frame) {
log('WebSocket连接成功!', 'success');
log('Frame: ' + frame, 'info');
updateConnectionStatus(true);
// 订阅实时消息主题
stompClient.subscribe('/topic/realtime', function (message) {
handleMessage(message);
});
log('已订阅主题: /topic/realtime', 'success');
collisionWebSocket.onopen = function(event) {
log('collisionLog', '冲突检测WebSocket连接成功!', 'success');
updateStatus('collisionStatus', true, '冲突检测');
document.getElementById('collisionConnectBtn').disabled = true;
document.getElementById('collisionDisconnectBtn').disabled = false;
reconnectAttempts = 0;
};
stompClient.onStompError = function (frame) {
log('STOMP错误: ' + frame.headers['message'], 'error');
log('详细信息: ' + frame.body, 'error');
updateConnectionStatus(false);
collisionWebSocket.onmessage = function(event) {
log('collisionLog', `收到消息: ${event.data}`, 'success');
try {
const message = JSON.parse(event.data);
handleCollisionMessage(message);
} catch (e) {
log('collisionLog', `非JSON消息: ${event.data}`, 'info');
}
};
stompClient.onWebSocketClose = function (event) {
log('WebSocket连接关闭', 'info');
updateConnectionStatus(false);
collisionWebSocket.onerror = function(error) {
log('collisionLog', `连接错误: ${error}`, 'error');
updateStatus('collisionStatus', false);
};
stompClient.onWebSocketError = function (error) {
log('WebSocket错误: ' + error, 'error');
updateConnectionStatus(false);
};
collisionWebSocket.onclose = function(event) {
log('collisionLog', `连接关闭: ${event.code} - ${event.reason}`, 'warning');
updateStatus('collisionStatus', false);
document.getElementById('collisionConnectBtn').disabled = false;
document.getElementById('collisionDisconnectBtn').disabled = true;
// 激活连接
stompClient.activate();
log('正在连接到 ws://10.0.0.124:8082/ws ...', 'info');
// 自动重连
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
log('collisionLog', `尝试重连 (${reconnectAttempts}/${maxReconnectAttempts})`, 'info');
setTimeout(connectCollisionWebSocket, 3000);
}
};
} catch (error) {
log('连接失败: ' + error.message, 'error');
updateConnectionStatus(false);
log('collisionLog', `连接失败: ${error.message}`, 'error');
updateStatus('collisionStatus', false);
}
}
function disconnect() {
if (!stompClient) {
log('未连接', 'error');
return;
function disconnectCollisionWebSocket() {
if (collisionWebSocket) {
collisionWebSocket.close();
collisionWebSocket = null;
log('collisionLog', '主动断开连接', 'info');
reconnectAttempts = maxReconnectAttempts;
}
}
stompClient.deactivate();
stompClient = null;
log('连接已断开', 'info');
updateConnectionStatus(false);
function sendCollisionPing() {
if (collisionWebSocket && collisionWebSocket.readyState === WebSocket.OPEN) {
collisionWebSocket.send('ping');
log('collisionLog', '发送心跳: ping', 'info');
} else {
log('collisionLog', 'WebSocket未连接', 'error');
}
}
function handleMessage(message) {
try {
const data = JSON.parse(message.body);
messageStats.total++;
if (messageStats[data.type]) {
messageStats[data.type]++;
function sendCollisionSubscribe() {
if (collisionWebSocket && collisionWebSocket.readyState === WebSocket.OPEN) {
const message = JSON.stringify({
type: 'subscribe',
topics: ['position_update', 'collision_warning', 'rule_violation'],
timestamp: Date.now()
});
collisionWebSocket.send(message);
log('collisionLog', '发送订阅请求', 'info');
} else {
log('collisionLog', 'WebSocket未连接', 'error');
}
}
updateStats();
// 根据消息类型进行不同的处理和显示
switch (data.type) {
function handleCollisionMessage(message) {
switch (message.type) {
case 'connection':
log('collisionLog', `连接确认: ${message.message}`, 'success');
break;
case 'pong':
log('collisionLog', '心跳响应', 'success');
break;
case 'position_update':
handlePositionUpdate(data);
// 修复:使用正确的字段名 object_id 而不是 vehicleId
const objectId = message.payload?.object_id || '未知';
const objectType = message.payload?.object_type || '';
log('collisionLog', `位置更新: ${objectId} (${objectType})`, 'info');
break;
case 'collision_warning':
handleCollisionWarning(data);
log('collisionLog', `碰撞预警: ${message.payload?.message || JSON.stringify(message.payload)}`, 'error');
break;
case 'rule_violation':
log('collisionLog', `规则违规: ${message.payload?.ruleType || '未知规则'}`, 'warning');
break;
case 'rule_execution_status':
log('collisionLog', `规则执行状态: ${message.payload?.status || '未知状态'}`, 'info');
break;
case 'rule_state_change':
log('collisionLog', `规则状态变更: ${message.payload?.newState || '未知状态'}`, 'info');
break;
case 'vehicle_command':
handleVehicleCommand(data);
log('collisionLog', `车辆指令: ${message.payload?.commandType || '未知指令'}`, 'info');
break;
case 'intersection_traffic_light_status':
handleTrafficLightStatus(data);
break;
case 'system_alert':
handleSystemAlert(data);
case 'traffic_light_status':
log('collisionLog', `红绿灯状态: ${message.payload?.intersection_id || '未知路口'}`, 'info');
break;
default:
log('收到未知类型消息: ' + JSON.stringify(data, null, 2), 'info');
}
} catch (e) {
log('消息解析错误: ' + e.message + ', 原始消息: ' + message.body, 'error');
log('collisionLog', `未知消息类型: ${message.type}`, 'info');
}
}
function handlePositionUpdate(data) {
const payload = data.payload;
log(`<strong>位置更新</strong> | 对象: ${payload.object_id} (${payload.object_type}) | ` +
`位置: (${payload.position?.latitude?.toFixed(6)}, ${payload.position?.longitude?.toFixed(6)}) | ` +
`航向: ${payload.heading}° | 速度: ${payload.speed} km/h`, 'position');
// =================== 控制面板 ===================
function clearLogs() {
document.getElementById('collisionLog').innerHTML = '';
log('collisionLog', '日志已清空', 'info');
}
function handleCollisionWarning(data) {
const payload = data.payload;
log(`<strong>🚨 碰撞预警!</strong> | 对象1: ${payload.object1_id} (${payload.object1_type}) | ` +
`对象2: ${payload.object2_id} (${payload.object2_type}) | ` +
`风险等级: <strong>${payload.risk_level}</strong> | 距离: ${payload.distance}m | ` +
`预计时间: ${payload.estimated_time}s`, 'collision');
function showConnectionStatus() {
const collisionStatus = collisionWebSocket ?
(collisionWebSocket.readyState === WebSocket.OPEN ? '已连接' : '未连接') : '未初始化';
log('collisionLog', `冲突检测WebSocket: ${collisionStatus}`, 'info');
}
function handleVehicleCommand(data) {
const payload = data.payload;
const commandTypes = {
'SIGNAL': '🚥 信号灯',
'ALERT': '⚠️ 告警',
'WARNING': '⚠️ 预警',
'RESUME': '✅ 恢复',
'PARKING': '🅿️ 停车'
};
const reasons = {
'TRAFFIC_LIGHT': '红绿灯控制',
'AIRCRAFT_CROSSING': '航空器交叉',
'COLLISION_WARNING': '碰撞预警',
'SPECIAL_VEHICLE': '特勤车辆',
'AIRCRAFT_PUSH': '航空器推出',
'RESUME_TRAFFIC': '恢复通行',
'PARKING_SIDE': '安全停车'
};
function performStressTest() {
log('collisionLog', '开始压力测试...', 'info');
log(`<strong>车辆控制指令</strong> | 车辆: ${payload.vehicleId} | ` +
`指令: ${commandTypes[payload.commandType] || payload.commandType} | ` +
`原因: ${reasons[payload.reason] || payload.reason} | ` +
`描述: ${payload.description || '无'}`, 'command');
for (let i = 0; i < 10; i++) {
setTimeout(() => {
if (collisionWebSocket && collisionWebSocket.readyState === WebSocket.OPEN) {
collisionWebSocket.send(`ping_${i}`);
}
}, i * 100);
}
}
function handleTrafficLightStatus(data) {
const payload = data.payload;
log(`<strong>🚦 红绿灯状态</strong> | 路口: ${payload.intersection_id} | ` +
`南北: ${payload.ns_status} | 东西: ${payload.ew_status}`, 'traffic-light');
}
function handleSystemAlert(data) {
const payload = data.payload;
log(`<strong>⚠️ 系统告警</strong> | ${payload.alert_level} | ${payload.title} | ` +
`组件: ${payload.component} | 描述: ${payload.description}`, 'system');
}
// 页面加载完成后的初始化
// 页面加载完成
window.onload = function() {
log('WebSocket测试客户端已就绪', 'success');
log('请点击"连接"按钮开始测试', 'info');
log('collisionLog', '冲突检测WebSocket测试就绪', 'info');
};
// 页面关闭时自动断开连接
// 页面关闭时清理连接
window.onbeforeunload = function() {
if (stompClient && stompClient.connected) {
disconnect();
}
if (collisionWebSocket) collisionWebSocket.close();
};
</script>
</body>

View File

@ -1,65 +0,0 @@
# WebSocket 使用说明
## 1. 引入 WebSocket 工具
```js
import { createWebSocket } from '@/utils/websocket';
```
## 2. 创建 WebSocket 实例
```js
const ws = createWebSocket('ws://你的后端ws地址');
```
## 3. 监听事件
```js
// 连接成功
ws.on('open', () => {
console.log('WebSocket 已连接');
});
// 接收消息
ws.on('message', (data) => {
console.log('收到消息:', data);
// 这里可以处理接收到的数据
});
// 监听错误
ws.on('error', (err) => {
console.error('WebSocket 错误:', err);
});
// 监听关闭
ws.on('close', () => {
console.log('WebSocket 已关闭');
});
```
## 4. 发送数据
```js
// 发送对象(会自动转为 JSON 字符串)
ws.send({ type: 'yourType', payload: '你的数据' });
// 发送字符串
ws.send('hello server');
```
## 5. 关闭连接
建议在页面销毁时关闭 WebSocket 连接:
```js
import { onUnmounted } from 'vue';
onUnmounted(() => {
ws.close();
});
```
## 6. 其他说明
- 支持自动重连,默认最大重连 10 次,每次间隔 3 秒。
- 支持事件解绑:
```js
ws.off('message', callback);
```
- 支持多页面/组件复用,内部采用单例模式。
---
如需更复杂的用法或有特殊需求,请联系开发维护者。

View File

@ -20,6 +20,11 @@
<!-- 车辆列表 -->
<Eventlist v-if="showEventList" />
<!-- 车辆移动控制组件 -->
<VehicleMovementControl :map="map" ref="vehicleMovementRef" v-if="map" />
<!-- 绘制工具栏 -->
<!-- <div class="draw-toolbar">
<div class="toolbar-title">绘制工具</div>
@ -36,11 +41,12 @@
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { ref, onMounted, onUnmounted, watch } from 'vue';
import OpenLayersMap from '../../components/map/OpenLayersMap.vue';
import CarAlarm from '../../components/map/info/carClarm.vue';
import Eventlist from '../../components/map/info/eventlist.vue';
import AlarmNotification from '../../components/map/info/AlarmNotification.vue';
import VehicleMovementControl from '../../components/map/controls/VehicleMovementControl.vue';
import btn_left from '../../assets/images/btn_left.png';
import btn_right from '../../assets/images/btn_right.png';
@ -48,6 +54,11 @@ const showEventList = ref(false); // 默认不显示列表
const showAlarmPanel = ref(false); //
const alarmCount = ref(4); //
const mapRef = ref(null);
const vehicleMovementRef = ref(null);
const map = ref(null); //
//
const isDevelopment = process.env.NODE_ENV === 'development';
function toggleEventList() {
showEventList.value = !showEventList.value;
@ -94,12 +105,35 @@ function importRouteData() {
}
}
onMounted(() => {
document.querySelector('.app-main')?.classList.add('platform-no-padding');
//
if (mapRef.value) {
//
setTimeout(() => {
map.value = mapRef.value.map;
console.log('地图实例已获取:', map.value);
//
// testVehicleMovement();
}, 1000);
}
});
onUnmounted(() => {
document.querySelector('.app-main')?.classList.remove('platform-no-padding');
});
//
watch(() => mapRef.value?.map, (newMap) => {
if (newMap) {
map.value = newMap;
console.log('地图实例已更新:', map.value);
}
});
</script>
<style scoped lang="scss">
@ -173,6 +207,31 @@ onUnmounted(() => {
right: 405px;
}
/* 测试按钮 - 仅开发阶段使用 */
.test-controls {
position: absolute;
bottom: 20px;
left: 20px;
z-index: 2000;
}
.test-btn {
padding: 8px 16px;
background-color: rgba(33, 150, 243, 0.8);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
}
.test-btn:hover {
background-color: rgba(33, 150, 243, 1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
/* 绘制工具栏 */
.draw-toolbar {
position: absolute;