Vue3纯前端同源跨窗口通信移动AGV小车

1.右侧控制页面

html 复制代码
<template>
  <div class="point-command">
    <h3>AGV小车模拟仿真</h3>
    <div class="point-container">
      <el-button
        v-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 = pointNumber
  setTimeout(() => {
    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.左侧小车移动页面

html 复制代码
<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) return
  
  let currentIndex = 0
  
  const 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>
相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax