<template>
<!-- <div id="inputContainer" class="inputContainer">
<input type="number" id="positionInput" value="1100" style="border: 1px solid black;font-size: 24px;"
placeholder="输入水平位置" />
<button class="styled-button" @click="method.refresh">机器人移动</button>
</div> -->
<div>
<h2 style="text-align: center;display: inline;">{{ data.titleContent }}</h2>
<span style="float: right;">{{ data.refreshDatetime }}</span>
<span style="float: right;">
<span class="legend" style="background-color:burlywood;"></span>
<span>准备中</span>
<span class="legend" style="background-color: blue;"></span>
<span>请求上料</span>
<span class="legend" style="background-color: green;"></span>
<span>处理中</span>
<span class="legend" style="background-color: red;"></span>
<span>完成</span>
<span>——</span>
</span>
</div>
<div style="background-color: white;width: 1440px;height: 840px;position: relative;">
<canvas id="pathCanvas" class="pathStyle"></canvas>
<svg class="path" viewBox="0 0 1440 840">
<path id="linePath" stroke="aqua" stroke-width="2" fill="none" />
<image class="arrow" xlink:href="../../../assets/img/Rotor.png" transform="translate(-20,-20)">
<animateMotion repeatCount="indefinite" dur="5s" keyPoints="0;1" keyTimes="0;1">
<mpath href="#linePath" />
</animateMotion>
</image>
</svg>
<img id="background" class="background" src="../../../assets/img/P6144BackImage.png" alt="背景" />
<div class="grid-container">
<div v-for="(row, rowIndex) in data.grid" :key="rowIndex" class="grid-cell">
<div v-for="(cell, cellIndex) in row" :key="cellIndex" class="grid-cell"
@mouseover="method.showCoordinates(rowIndex, cellIndex)" @mouseleave="method.hideCoordinates"
:class="{
'preparing-status-cell': method.setPreparingStatus(rowIndex, cellIndex),
'request-loading-status-cell': method.setRequestLoadingStatus(rowIndex, cellIndex),
'processing-status-cell': method.setProcessingStatus(rowIndex, cellIndex),
'finished-status-cell': method.setFinishedStatus(rowIndex, cellIndex),
}">
<div class="container">
<div class="station-font">{{ method.getStationName(rowIndex, cellIndex) }}</div>
</div>
</div>
</div>
</div>
<div class="track"></div>
<img id="kuka" class="kuka" src="../../../assets/img/kuka.png" alt="机器人" />
<!-- 坐标显示元素 -->
<div v-if="data.showCoordinates" class="coordinates-display">
{{ data.currentCoordinates }}
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue'
import * as signalR from '@microsoft/signalr';
import { P6144StationVO } from '../../../types/WCS/Status'
const SERVER_URL = `${import.meta.env.VITE_BASE_PATH}:${import.meta.env.VITE_SERVER_PORT}`
interface FromTo {
From: string;
To: string;
Action: string;
}
interface Point {
x: number;
y: number;
}
interface StatusScreenVO {
Type: string;
Content: string;
}
interface PublishTaskContentVO {
Title: string;
AnimationX: number;
}
enum StationStatusEnum {
Preparing,
RequestLoading,
Processing,
Finished,
}
enum Direction {
Right,
Left,
Down,
Up
}
const data = reactive({
refreshDatetime: '',
titleContent: 'title',
grid: ref(Array.from({ length: 24 }, () => Array(28))),
positionInput: ref<HTMLInputElement | null>(null),
imageElement: ref<HTMLImageElement | null>(null),
connection: ref<signalR.HubConnection | null>(null),
hubConnection: new signalR.HubConnectionBuilder()
.withUrl(SERVER_URL + '/TaskAnimationHub')
.configureLogging(signalR.LogLevel.Information)
.withServerTimeout(3600 * 1 * 1000)
.build(),
stationName: ref<{ x: number; y: number, station_code: string, station_name: string }[]>([
{ x: 2, y: 2, station_code: 'OP010', station_name: '' },
]),
preparingLocation: ref<{ x: number; y: number, station_name: string }[]>([
{ x: 2, y: 2, station_name: 'OP010' },
]),
requestLoadingLocation: ref<{ x: number; y: number, station_name: string }[]>([
{ x: 2, y: 8, station_name: 'OP040' },
]),
processingLocation: ref<{ x: number; y: number, station_name: string }[]>([
{ x: 12, y: 2, station_name: 'OP070' },
]),
finishedLocation: ref<{ x: number; y: number, station_name: string }[]>([
{ x: 12, y: 8, station_name: 'OP100' },
]),
showCoordinates: false,
currentCoordinates: '',
lineWidth: 4,
lineColor: 'aqua',
triangleLength: 12,
spaceBetween: 40,
animationInProgress: false,
})
// 定义方法对象
const method = reactive({
refresh: () => {
if (data.positionInput) {
const position = parseInt(data.positionInput.value, 14)
if (!isNaN(position) && data.imageElement) {
data.imageElement.style.left = position + 'px'
} else {
alert('请输入一个有效的数字')
}
}
},
drawTriangle: (ctx: CanvasRenderingContext2D, point: Point, direction: Direction) => {
const length = data.triangleLength;
ctx.strokeStyle = data.lineColor;
ctx.lineWidth = data.lineWidth;
ctx.beginPath();
switch (direction) {
case Direction.Right:
ctx.moveTo(point.y - length, point.x - length);
ctx.lineTo(point.y, point.x);
ctx.lineTo(point.y - length, point.x + length);
break;
case Direction.Left:
ctx.moveTo(point.y + length, point.x - length);
ctx.lineTo(point.y, point.x);
ctx.lineTo(point.y + length, point.x + length);
break;
case Direction.Down:
ctx.moveTo(point.y - length, point.x - length);
ctx.lineTo(point.y, point.x);
ctx.lineTo(point.y + length, point.x - length);
break;
case Direction.Up:
ctx.moveTo(point.y + length, point.x + length);
ctx.lineTo(point.y, point.x);
ctx.lineTo(point.y - length, point.x + length);
break;
}
//ctx.closePath();
ctx.stroke();
},
drawPath: (from: string, to: string,action:string) => {
//找from to的坐标
const height = 30;
const width = 60;
const trackHeight = 410;
const fromX: number = data.stationName.find(station => station.station_code === from)?.x as number;
const fromY: number = data.stationName.find(station => station.station_code === from)?.y as number;
const toX: number = data.stationName.find(station => station.station_code === to)?.x as number;
const toY: number = data.stationName.find(station => station.station_code === to)?.y as number;
const takePoint: Point = { x: (fromX) * height + height / 2, y: (fromY) * width + width / 2 };
const takeTrack: Point = { x: trackHeight, y: fromY * width + width / 2 };
const putTrack: Point = { x: trackHeight, y: toY * width + width / 2 };
const putPoint: Point = { x: toX * height + height / 2, y: toY * width + width / 2 };
const canvas = document.getElementById('pathCanvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d');
const pathElement = document.getElementById('linePath') as unknown as SVGPathElement;
if(action==="Finished" && ctx){
ctx.clearRect(0, 0, canvas.width, canvas.height);
pathElement.setAttribute('d', '');
return;
}
if (pathElement) {
const d = `
M${takePoint.y},${takePoint.x}
L${takeTrack.y},${takeTrack.x}
L${putTrack.y},${putTrack.x}
L${putPoint.y},${putPoint.x}`;
pathElement.setAttribute('d', d);
}
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height); // 清除之前绘制的路径
ctx.lineWidth = data.lineWidth;
ctx.strokeStyle = data.lineColor;
ctx.beginPath();
ctx.moveTo(takePoint.y, takePoint.x); // 起始点
ctx.lineTo(takeTrack.y, takeTrack.x); // 终点
ctx.stroke(); // 执行绘制
ctx.beginPath();
ctx.moveTo(takeTrack.y, takeTrack.x); // 起始点
ctx.lineTo(putTrack.y, putTrack.x); // 终点
ctx.stroke(); // 执行绘制
ctx.beginPath();
ctx.moveTo(putTrack.y, putTrack.x); // 起始点
ctx.lineTo(putPoint.y, putPoint.x); // 终点
ctx.stroke(); // 执行绘制
// 绘制箭头 takePoint水平方向小于takeTrack,则箭头向下
if (takePoint.x < takeTrack.x) {
method.drawTriangle(ctx, takeTrack, Direction.Down);
} else {
method.drawTriangle(ctx, takeTrack, Direction.Up);
}
// 绘制箭头takeTrack垂直方向小于putTrack,则箭头向右
if (takeTrack.y < putTrack.y) {
// while (takeTrack.y < putTrack.y) {
// method.drawTriangle(ctx, takeTrack, Direction.Right);
// takeTrack.y += data.spaceBetween;
// }
method.drawTriangle(ctx, putTrack, Direction.Right);
} else {
// while (putTrack.y < takeTrack.y) {
// method.drawTriangle(ctx, putTrack, Direction.Left);
// putTrack.y += data.spaceBetween;
// }
method.drawTriangle(ctx, putTrack, Direction.Left);
}
//绘制箭头 putTrack水平方向小于putPoint,则箭头向下
if (putTrack.x < putPoint.x) {
method.drawTriangle(ctx, putPoint, Direction.Down);
} else {
method.drawTriangle(ctx, putPoint, Direction.Up);
}
}
},
signalR_Listen: () => {
data.hubConnection.on('ReciveMsg', (message: string) => {
console.log('Received message:', message);
const parsedData = JSON.parse(message);
const statusScreen: StatusScreenVO = parsedData as StatusScreenVO;
var type = statusScreen.Type;
if (type === "RobotAnimation") {
const robotAnimation = JSON.parse(statusScreen.Content) as PublishTaskContentVO;
var title = JSON.parse(robotAnimation.Title) as FromTo;
data.titleContent = "从:" + title.From + " 到:" + title.To + " 动作:" + title.Action;
//console.log('Received Content:', taskInfo.Title);
if (data.imageElement) {
//console.log('AnimationX:', robotAnimation.AnimationX);
data.imageElement.style.left = robotAnimation.AnimationX + 'px';
}
method.drawPath(title.From, title.To,title.Action);
};
if (type === "StationStatus") {
const stationStatus = JSON.parse(statusScreen.Content) as P6144StationVO[];
// console.log('StationStatus:', stationStatus);
//data.preparingLocation.length = 0;
data.stationName = stationStatus.map(item => ({ x: item.status_x, y: item.status_y, station_code: item.station_code, station_name: item.station_name }));
data.preparingLocation = stationStatus
.filter(item => item.status === StationStatusEnum[StationStatusEnum.Preparing])
.map(item => ({ x: item.status_x, y: item.status_y, station_name: item.station_code }));
// console.log('StationStatusEnum Preparing:', StationStatusEnum[StationStatusEnum.Preparing]);
// console.log('preparingLocation Content:', data.preparingLocation);
//data.requestLoadingLocation.length = 0;
data.requestLoadingLocation = stationStatus
.filter(item => item.status === StationStatusEnum[StationStatusEnum.RequestLoading])
.map(item => ({ x: item.status_x, y: item.status_y, station_name: item.station_code }));
//data.processingLocation.length = 0;
data.processingLocation = stationStatus
.filter(item => item.status === StationStatusEnum[StationStatusEnum.Processing])
.map(item => ({ x: item.status_x, y: item.status_y, station_name: item.station_code }));
//data.finishedLocation.length = 0;
data.finishedLocation = stationStatus
.filter(item => item.status === StationStatusEnum[StationStatusEnum.Finished])
.map(item => ({ x: item.status_x, y: item.status_y, station_name: item.station_code }));
};
data.refreshDatetime = "刷新时间:" + (new Date()).toLocaleString('cn-ZH', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
});
},
signalRConnect: async () => {
try {
await data.hubConnection.start();
console.log('Connected!');
data.connection = data.hubConnection; // 保存连接实例以便后续使用
} catch (err) {
console.error('SignalR Connection Error:', err);
}
},
setPreparingStatus: (row: number, col: number) => {
return data.preparingLocation.some(coord => coord.x === col && coord.y === row);
},
setRequestLoadingStatus: (row: number, col: number) => {
return data.requestLoadingLocation.some(coord => coord.x === col && coord.y === row);
},
setProcessingStatus: (row: number, col: number) => {
return data.processingLocation.some(coord => coord.x === col && coord.y === row);
},
setFinishedStatus: (row: number, col: number) => {
return data.finishedLocation.some(coord => coord.x === col && coord.y === row);
},
getStationName: (row, col) => {
const coordinate = data.stationName.find(coord => coord.x === col && coord.y === row);
return coordinate ? coordinate.station_code : ""; // 如果找到匹配的坐标,则返回其status,否则返回空字符串
},
showCoordinates: (row, col) => {
var stationName = data.stationName.find(coord => coord.x === col && coord.y === row)?.station_name;
data.showCoordinates = true;
data.currentCoordinates = `${stationName},Row: ${row}, Col: ${col}`;
//console.log(stationName);
},
hideCoordinates: () => {
data.showCoordinates = false;
data.currentCoordinates = '';
}
})
const MAX_RECONNECT_ATTEMPTS = 1200; // 最大重连尝试次数
let reconnectAttempts = 0; // 当前重连尝试次数
// 添加一个用于重连的方法
const reconnect = () => {
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
console.log(`Attempting to reconnect... (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
setTimeout(async () => {
try {
await data.hubConnection.start();
console.log('Reconnected!');
reconnectAttempts = 0; // 重置重连尝试次数
method.signalR_Listen(); // 重新绑定事件监听器
} catch (err) {
reconnect(); // 如果连接失败,则递归尝试重新连接
}
}, 2000); // 等待2秒后重试连接
} else {
console.error('Max reconnect attempts reached.');
}
};
// 使用 onMounted 来获取 DOM 元素引用
onMounted(() => {
data.positionInput = document.getElementById('positionInput') as HTMLInputElement
data.imageElement = document.getElementById('kuka') as HTMLImageElement
method.signalRConnect();
method.signalR_Listen();
// 监听连接断开事件,并尝试重新连接
data.hubConnection.onclose(async (error) => {
if (error) {
console.error('Connection closed with error:', error);
}
reconnect(); // 当连接断开时尝试重新连接
});
const canvas = document.getElementById('pathCanvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d');
if (ctx) {
const scaleFactor = 1;
const scaledWidth = 1440 * scaleFactor;
const scaledHeight = 840 * scaleFactor;
canvas.width = scaledWidth;
canvas.height = scaledHeight;
ctx.scale(scaleFactor, scaleFactor);
ctx.lineWidth = 1;
ctx.strokeStyle = 'green';
}
})
</script>
<style lang="less" scoped>
.kuka {
position: absolute;
top: 350px;
left: 300px;
transform: translateX(0%);
z-index: 30;
width: 100px;
height: auto;
transition: left 3s ease-in-out;
}
.track {
position: absolute;
top: 350px;
transform: translateX(0%);
z-index: 20;
width: 890px;
height: 120px;
background-color: rgb(190, 190, 114);
opacity: 0.9;
margin-left: 300px;
margin-right: 250px;
}
.background {
z-index: 9;
position: absolute;
top: 0px;
left: 0%;
transform: translateX(0%);
width: 1440px;
height: 840px;
}
.inputContainer {
margin-bottom: 20px;
}
.grid-container {
display: grid;
grid-template-columns: repeat(24, 60px);
grid-template-rows: repeat(28, 30px);
width: 100%;
height: 100%;
grid-gap: 0px;
background-color: white;
z-index: 10;
//position: absolute;
}
.grid-cell {
width: 60px;
height: 30px;
background-color: white;
margin: 0;
padding: 0;
//border-radius:30%;
position: relative;
// opacity: 0.5;
// z-index: 12;
}
.container {
//position: relative;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
//animation: blink 2s infinite;
}
@keyframes blink {
/* 从0%(完全不透明)到100%(完全透明),然后再回到0% */
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
.legend {
width: 12px;
/* 圆点的宽度 */
height: 12px;
/* 圆点的高度 */
// border-radius: 50%;
/* 将矩形元素转换为圆形 */
display: inline-block;
}
.preparing-status-cell {
background-color: burlywood;
border: 1px solid black;
z-index: 16;
/* 提高 z-index 值 */
position: relative;
}
.request-loading-status-cell {
background-color: blue;
border: 1px solid black;
z-index: 16;
/* 提高 z-index 值 */
position: relative;
}
.processing-status-cell {
background-color: green;
border: 1px solid black;
z-index: 16;
/* 提高 z-index 值 */
position: relative;
}
.finished-status-cell {
background-color: red;
border: 1px solid black;
z-index: 16;
/* 提高 z-index 值 */
position: relative;
}
.station-font {
display: flex;
align-items: flex-end;
justify-content: center;
height: 70%;
color: white;
font-size: 10px;
font-weight: bold;
}
.coordinates-display {
position: absolute;
top: 10px;
left: 10px;
padding: 5px;
background-color: white;
color: black;
border-radius: 5px;
z-index: 50;
}
.pathStyle {
position: absolute;
z-index: 25;
pointer-events: none;
top: 0;
left: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f0f0;
}
.path {
position: absolute;
pointer-events: none;
width: 1440px;
height: 840px;
z-index: 25;
top: 0;
}
.arrow {
fill: lightgray;
width: 40px;
height: 40px;
animation: moveAlongPath 4s linear infinite alternate;
z-index: 25;
// border: black 1px solid;
}
@keyframes moveAlongPath {
0% {
offset-distance: 0%;
}
100% {
offset-distance: 100%;
}
}
</style>
HTML SVG路径动画
于 2024-08-01 09:37:06 首次发布
1432

被折叠的 条评论
为什么被折叠?



