Vue3纯前端同源跨窗口通信移动AGV小车
1.右侧控制页面
<template><div class="point-command"><h3>AGV小车模拟仿真</h3><div class="point-container"><el-buttonv-for="n in 22":key="n":class="{ 'active-point': activePoint === n }"type="primary"circle@click="handlePointClick(n)">{{ n }}</el-button></div></div>
</template><script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'const channel = ref(null)
const activePoint = ref(null)const handlePointClick = (pointNumber) => {// 发送消息channel.value.postMessage({type: "POINT_SELECTED",pointId: pointNumber,timestamp: Date.now(),})// 按钮动画效果activePoint.value = pointNumbersetTimeout(() => {activePoint.value = null}, 300)
}onMounted(() => {channel.value = new BroadcastChannel("agv-channel")
})onBeforeUnmount(() => {channel.value?.close()
})
</script><style scoped>
.point-command {padding: 30px;background: #fff;border-radius: 12px;box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);max-width: 800px;margin: 0 auto;display: flex;flex-direction: column;align-items: center;
}h3 {color: #2c3e50;font-size: 24px;margin-bottom: 25px;font-weight: 600;
}.point-container {width: 100%;background: #f8f9fa;border-radius: 8px;padding: 20px;display: flex;flex-wrap: wrap;gap: 15px;
}.el-button.is-circle {width: 60px;height: 60px;font-size: 20px;font-weight: 600;transition: all 0.3s ease;margin: 0;
}.el-button.is-circle:hover {transform: translateY(-2px);box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
}/* 添加按钮激活效果 */
.active-point {transform: scale(1.15);box-shadow: 0 0 15px rgba(64, 158, 255, 0.4);transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
</style>
2.左侧小车移动页面
<template><div class="home-page"><div class="path-container"><div class="center-line"></div><div v-for="point in points" :key="point.id"class="point":style="{ left: point.x + '%', top: point.y + '%' }"><span class="point-number">{{ point.id }}</span></div><div class="moving-box":style="{ left: currentPosition.x + '%', top: currentPosition.y + '%' }"></div></div></div>
</template><script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'const channel = ref(null)
const points = ref([// 上边框{ id: 1, x: 5, y: 0 },{ id: 2, x: 15, y: 0 },{ id: 3, x: 25, y: 0 },{ id: 4, x: 35, y: 0 },{ id: 5, x: 45, y: 0 },// 中间线{ id: 6, x: 50, y: 10 },{ id: 7, x: 50, y: 25 },{ id: 8, x: 50, y: 40 },{ id: 9, x: 50, y: 55 },{ id: 10, x: 50, y: 70 },{ id: 11, x: 50, y: 85 },// 右边框{ id: 12, x: 100, y: 15 },{ id: 13, x: 100, y: 30 },{ id: 14, x: 100, y: 45 },{ id: 15, x: 100, y: 60 },{ id: 16, x: 100, y: 75 },{ id: 17, x: 100, y: 90 },// 下边框{ id: 18, x: 85, y: 100 },{ id: 19, x: 70, y: 100 },{ id: 20, x: 55, y: 100 },{ id: 21, x: 40, y: 100 },{ id: 22, x: 25, y: 100 }
])
const currentPosition = ref({x: 5,y: 0
})const handleChannelMessage = (event) => {if (event.data.type === "POINT_SELECTED") {moveToPoint(event.data.pointId)}
}const moveToPoint = (pointId) => {const targetPoint = points.value.find(p => p.id === pointId)if (targetPoint) {moveAlongPath(targetPoint)}
}// 计算并执行路径移动 - 沿着边线和中心线移动
const moveAlongPath = (targetPoint) => {const current = { ...currentPosition.value }const target = { ...targetPoint }// 如果已经在目标位置,直接返回if (current.x === target.x && current.y === target.y) {return}// 计算路径const path = calculatePath(current, target)// 执行路径移动executePath(path)
}// 计算沿边线的路径
const calculatePath = (start, end) => {const path = []// 定义关键路径点const pathNodes = {// 上边框节点topBorder: (x) => ({ x, y: 0 }),// 右边框节点 rightBorder: (y) => ({ x: 100, y }),// 下边框节点bottomBorder: (x) => ({ x, y: 100 }),// 左边框节点leftBorder: (y) => ({ x: 0, y }),// 中心线节点centerLine: (y) => ({ x: 50, y })}// 判断点在哪条线上const getLineType = (point) => {if (point.y === 0) return 'top'if (point.x === 100) return 'right'if (point.y === 100) return 'bottom'if (point.x === 0) return 'left'if (point.x === 50) return 'center'return 'unknown'}const startLine = getLineType(start)const endLine = getLineType(end)// 如果在同一条线上,直接移动if (startLine === endLine) {path.push(end)return path}// 不同线之间的移动策略if (startLine === 'top') {if (endLine === 'center') {// 从上边框到中心线:先到(50,0)再到目标path.push({ x: 50, y: 0 })path.push(end)} else if (endLine === 'right') {// 从上边框到右边框:先到(100,0)再到目标path.push({ x: 100, y: 0 })path.push(end)} else if (endLine === 'bottom') {// 从上边框到下边框:通过中心线path.push({ x: 50, y: 0 })path.push({ x: 50, y: 100 })path.push(end)}} else if (startLine === 'center') {if (endLine === 'top') {// 从中心线到上边框:先到(50,0)再到目标path.push({ x: 50, y: 0 })path.push(end)} else if (endLine === 'right') {// 从中心线到右边框:找最近的转折点if (start.y <= 50) {// 上半部分:通过上边框path.push({ x: 50, y: 0 })path.push({ x: 100, y: 0 })path.push(end)} else {// 下半部分:通过下边框path.push({ x: 50, y: 100 })path.push({ x: 100, y: 100 })path.push(end)}} else if (endLine === 'bottom') {// 从中心线到下边框:先到(50,100)再到目标path.push({ x: 50, y: 100 })path.push(end)}} else if (startLine === 'right') {if (endLine === 'top') {// 从右边框到上边框:先到(100,0)再到目标path.push({ x: 100, y: 0 })path.push(end)} else if (endLine === 'center') {// 从右边框到中心线:找最近的路径if (start.y <= 50) {// 上半部分:通过上边框path.push({ x: 100, y: 0 })path.push({ x: 50, y: 0 })path.push(end)} else {// 下半部分:通过下边框path.push({ x: 100, y: 100 })path.push({ x: 50, y: 100 })path.push(end)}} else if (endLine === 'bottom') {// 从右边框到下边框:先到(100,100)再到目标path.push({ x: 100, y: 100 })path.push(end)}} else if (startLine === 'bottom') {if (endLine === 'center') {// 从下边框到中心线:先到(50,100)再到目标path.push({ x: 50, y: 100 })path.push(end)} else if (endLine === 'right') {// 从下边框到右边框:先到(100,100)再到目标path.push({ x: 100, y: 100 })path.push(end)} else if (endLine === 'top') {// 从下边框到上边框:通过中心线path.push({ x: 50, y: 100 })path.push({ x: 50, y: 0 })path.push(end)}}return path
}// 执行路径移动
const executePath = (path) => {if (path.length === 0) returnlet currentIndex = 0const moveToNext = () => {if (currentIndex < path.length) {currentPosition.value = { ...path[currentIndex] }currentIndex++// 如果还有下一个点,延迟后继续移动if (currentIndex < path.length) {setTimeout(moveToNext, 600) // 0.6秒间隔}}}moveToNext()
}onMounted(() => {channel.value = new BroadcastChannel("agv-channel")channel.value.addEventListener("message", handleChannelMessage)
})onBeforeUnmount(() => {channel.value?.close()
})
</script><style scoped>
.home-page {padding: 20px;height: 100vh;display: flex;justify-content: center;align-items: center;
}.path-container {position: relative;width: 800px;height: 600px;background: #f5f5f5;border-radius: 8px;border: 2px solid #409eff;
}.center-line {position: absolute;top: 0;left: 50%;width: 2px;height: 100%;background: #409eff;opacity: 0.5;
}.point {position: absolute;width: 24px;height: 24px;background: #409eff;border-radius: 50%;transform: translate(-50%, -50%);display: flex;justify-content: center;align-items: center;cursor: pointer;transition: all 0.3s ease;
}.point:hover {transform: translate(-50%, -50%) scale(1.2);box-shadow: 0 0 10px #409eff;
}.point-number {color: white;font-size: 12px;font-weight: bold;
}.moving-box {position: absolute;width: 30px;height: 30px;background: #67c23a;border-radius: 4px;transform: translate(-50%, -50%);transition: all 0.5s ease;z-index: 1;
}
</style>