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>
问题根源
- 组件状态隔离:每个
GridBlock组件独立维护selected变量,跨组件无法共享选中状态,拖拽目标组件无法获取发起拖拽的组件的选中元素。 - 绕过响应式系统:直接操作DOM的
innerHTML交换内容,Vue的响应式机制无法感知,后续重新渲染会覆盖手动修改的DOM内容。 - 模板动态值无绑定:
{{ Math.floor(Math.random() * 100) }}是每次渲染时生成的临时值,未绑定到组件状态,交换DOM后会被Vue重新渲染覆盖。 - 点击事件目标错误:点击事件的
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>
修复说明
- 状态提升:父组件统一管理所有网格块的
value和color,确保数据唯一来源,跨组件状态共享。 - 响应式驱动:所有视图更新通过修改响应式数据实现,符合Vue的设计理念,避免直接操作DOM。
- 事件通信:子组件通过
emit传递拖拽事件,父组件处理核心交换逻辑,组件职责清晰。 - 修正点击事件:点击事件直接修改当前块的
color属性,确保样式修改正确且响应式更新。
内容的提问来源于stack exchange,提问作者Dirk
相关产品推荐
相关产品推荐

