You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue实现类Minecraft物品栏拖拽功能失效,求故障排查

Vue3 拖拽交换网格块失效问题修复

问题背景

尝试复刻Minecraft物品栏的拖拽交换功能,基于Vue3+TypeScript实现,但拖拽网格块到另一个网格块时,两者的内容和样式无任何交换反应,浏览器调试工具也无报错。参考原生JS拖拽教程适配Vue,未解决问题。

现有代码

GridBlock.vue

<script setup lang="ts">
import { ref, type Ref } from 'vue'

//const props = defineProps(['item'])

const selected: Ref<HTMLElement | null> = ref(null)

const randomColor = () => {
  //#1f3f5f
  return `#${Math.floor(Math.random() * 10)}f${Math.floor(Math.random() * 10)}f${Math.floor(Math.random() * 10)}f`
}

const handleClick = (e: Event) => {
  let target = e.target as HTMLElement
  target.style.backgroundColor = randomColor()
}

const handleDragStart = (e: DragEvent) => {
  let target = e.target as HTMLElement
  selected.value = target
}

const handleDragOver = (e: DragEvent) => {
  e.preventDefault()
}

const handleDrop = (e: DragEvent) => {
  e.preventDefault()

  let target = e.currentTarget as HTMLElement

  if (selected.value && selected.value !== target) {
    const temp = target.innerHTML
    target.innerHTML = selected.value.innerHTML
    selected.value.innerHTML = temp
    selected.value = null
  }
}
</script>

<template>
  <div
    class="grid-block"
    draggable="true"
    @click="handleClick"
    @dragstart="handleDragStart"
    @dragover="handleDragOver"
    @drop="handleDrop"
  >
    <div class="item-block">
      {{ Math.floor(Math.random() * 100) }}
    </div>
  </div>
</template>

<style>
.grid-block {
  width: 4rem;
  height: 4rem;
  margin-left: auto;
  margin-right: auto;
  margin-top: 2rem;
  border: 2px solid red;
  background-color: beige;
}
.item-block {
  width: 100%;
  height: 100%;
  background-color: black;
}
</style>

GridDisplay.vue

<script setup lang="ts">
import GridBlock from './GridBlock.vue'

function range(start: number, end: number): number[] {
  return Array.from({ length: end - start + 1 }, (_, i) => start + i)
}

const blocks = range(1, 16)
</script>

<template>
  <div class="grid-container">
    <GridBlock v-for="i in blocks" :key="i" :id="i"></GridBlock>
  </div>
</template>

<style>
.grid-container {
  display: grid;
  grid-template-columns: repeat(4, 7.4rem);
  grid-template-rows: repeat(4, 7.4rem);
  border: 1px solid red;
  width: 25%;
  margin-left: auto;
  margin-right: auto;
  background-color: cadetblue;
}
</style>

问题根源

  1. 组件状态隔离:每个GridBlock组件独立维护selected变量,跨组件无法共享选中状态,拖拽目标组件无法获取发起拖拽的组件的选中元素。
  2. 绕过响应式系统:直接操作DOM的innerHTML交换内容,Vue的响应式机制无法感知,后续重新渲染会覆盖手动修改的DOM内容。
  3. 模板动态值无绑定:{{ Math.floor(Math.random() * 100) }}是每次渲染时生成的临时值,未绑定到组件状态,交换DOM后会被Vue重新渲染覆盖。
  4. 点击事件目标错误:点击事件的target可能指向内部的.item-block元素,而非外层的.grid-block,导致样式修改对象错误。

修复方案

核心思路

  • 将所有网格块的状态(数值、颜色)提升到父组件GridDisplay统一管理,通过props传递给子组件。
  • 子组件通过emit事件通知父组件拖拽开始、拖拽结束,由父组件处理数据交换逻辑。
  • 移除直接操作DOM的代码,完全通过响应式数据驱动视图更新。

修改后的代码

GridDisplay.vue(父组件)

<script setup lang="ts">
import GridBlock from './GridBlock.vue'
import { ref } from 'vue'

// 定义每个网格块的数据结构
interface BlockData {
  id: number
  value: number
  color: string
}

// 生成随机颜色的工具函数
const randomColor = () => {
  return `#${Math.floor(Math.random() * 10)}f${Math.floor(Math.random() * 10)}f${Math.floor(Math.random() * 10)}f`
}

// 初始化16个网格块的数据
const blocks = ref<BlockData[]>(
  Array.from({ length: 16 }, (_, idx) => ({
    id: idx + 1,
    value: Math.floor(Math.random() * 100),
    color: randomColor()
  }))
)

// 记录当前选中的块ID
let draggedBlockId: number | null = null

// 处理拖拽开始事件
const handleDragStart = (blockId: number) => {
  draggedBlockId = blockId
}

// 处理拖拽结束事件,交换数据
const handleDrop = (targetBlockId: number) => {
  if (!draggedBlockId || draggedBlockId === targetBlockId) return

  // 找到选中块和目标块的索引
  const draggedIdx = blocks.value.findIndex(b => b.id === draggedBlockId)
  const targetIdx = blocks.value.findIndex(b => b.id === targetBlockId)

  // 交换两个块的数据
  ;[blocks.value[draggedIdx], blocks.value[targetIdx]] = [blocks.value[targetIdx], blocks.value[draggedIdx]]
  
  draggedBlockId = null
}
</script>

<template>
  <div class="grid-container">
    <GridBlock
      v-for="block in blocks"
      :key="block.id"
      :block-data="block"
      @drag-start="handleDragStart(block.id)"
      @drop="handleDrop(block.id)"
    ></GridBlock>
  </div>
</template>

<style>
.grid-container {
  display: grid;
  grid-template-columns: repeat(4, 7.4rem);
  grid-template-rows: repeat(4, 7.4rem);
  border: 1px solid red;
  width: 25%;
  margin-left: auto;
  margin-right: auto;
  background-color: cadetblue;
}
</style>

GridBlock.vue(子组件)

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'

// 定义props类型
interface BlockData {
  id: number
  value: number
  color: string
}

const props = defineProps<{
  blockData: BlockData
}>()

// 定义emit事件
const emit = defineEmits<{
  'drag-start': [blockId: number]
  'drop': [blockId: number]
}>()

const handleDragStart = () => {
  emit('drag-start', props.blockData.id)
}

const handleDragOver = (e: DragEvent) => {
  e.preventDefault()
}

const handleDrop = (e: DragEvent) => {
  e.preventDefault()
  emit('drop', props.blockData.id)
}

// 修改当前块的颜色
const handleClick = () => {
  props.blockData.color = `#${Math.floor(Math.random() * 10)}f${Math.floor(Math.random() * 10)}f${Math.floor(Math.random() * 10)}f`
}
</script>

<template>
  <div
    class="grid-block"
    draggable="true"
    @click="handleClick"
    @dragstart="handleDragStart"
    @dragover="handleDragOver"
    @drop="handleDrop"
    :style="{ backgroundColor: blockData.color }"
  >
    <div class="item-block">
      {{ blockData.value }}
    </div>
  </div>
</template>

<style>
.grid-block {
  width: 4rem;
  height: 4rem;
  margin-left: auto;
  margin-right: auto;
  margin-top: 2rem;
  border: 2px solid red;
}
.item-block {
  width: 100%;
  height: 100%;
  background-color: black;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>

修复说明

  1. 状态提升:父组件统一管理所有网格块的value和color,确保数据唯一来源,跨组件状态共享。
  2. 响应式驱动:所有视图更新通过修改响应式数据实现,符合Vue的设计理念,避免直接操作DOM。
  3. 事件通信:子组件通过emit传递拖拽事件,父组件处理核心交换逻辑,组件职责清晰。
  4. 修正点击事件:点击事件直接修改当前块的color属性,确保样式修改正确且响应式更新。

内容的提问来源于stack exchange,提问作者Dirk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 15:00:54