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

React-DnD实现4x4数字拼图拖拽失效问题求助

4x4数字拼图拖拽问题修复

核心错误点

1. isDraggable方法逻辑缺陷

找到空白格后,原代码只要第一个条件不满足就立即返回false,导致仅空白格左侧的方块会被判定为可拖拽,上、下、右三个方向的方块会被错误判定为不可拖拽。

2. handleDrag方法参数传递错误

调用isDraggable时错误传入了toIndex[0]作为列参数,应该传递拖拽起始位置的列索引fromIndex[1]。

3. 直接修改State数组

shuffle2darray方法直接修改了this.state.numbers,违反React状态不可变原则,会导致状态更新异常。

4. Tile组件Ref绑定冲突

同时在内部div和外部div绑定drag,导致拖拽逻辑混乱。


修复后的完整代码

Board类(Number_puzzle.js)

import React from 'react';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndProvider } from 'react-dnd';
import { Tile } from './Tile'
import './Number_puzzle.css';

export class Number_puzzle extends React.Component {
    constructor(props){
        super(props);
        // 初始化时复制数组,避免直接修改原数据
        const initialNumbers = [ 
            [1,2,3,4],
            [5,6,7,8],
            [9,10,11,12],
            [13,14,15,-1] 
        ];
        this.shuffle2darray(initialNumbers);
        this.state = {
            numbers: initialNumbers
        };
        console.log("shuffled!")
    }

    shuffle2darray(numbers){
        // 复制数组避免修改原数据
        const arr = JSON.parse(JSON.stringify(numbers));
        for (let i = arr.length - 1; i > 0; i--) {
            for (let j = arr[i].length - 1; j > 0; j--) {
                const randomI = Math.floor(Math.random() * (i + 1));
                const randomJ = Math.floor(Math.random() * (j + 1));
                // 交换元素
                [arr[i][j], arr[randomI][randomJ]] = [arr[randomI][randomJ], arr[i][j]];
            }
        }
        // 将洗牌后的数据赋值回原数组
        Object.assign(numbers, arr);
    }

    isDraggable(tileRow, tileCol){
        let isAdjacent = false;
        for (let i = 0; i < this.state.numbers.length; i++) {
            for (let j = 0; j < this.state.numbers[i].length; j++) {
                if (this.state.numbers[i][j] === -1) {
                    // 检查所有四个相邻方向,只要满足一个就设为true
                    const isLeft = tileRow === i && tileCol === j - 1;
                    const isRight = tileRow === i && tileCol === j + 1;
                    const isTop = tileRow === i - 1 && tileCol === j;
                    const isBottom = tileRow === i + 1 && tileCol === j;
                    isAdjacent = isLeft || isRight || isTop || isBottom;
                    // 找到空白格后直接跳出循环,无需继续遍历
                    break;
                }
            }
            if (isAdjacent) break;
        }
        return isAdjacent;
    }

    handleDrag(fromIndex, toIndex) {
        // 检查拖拽起始位置是否和空白格相邻
        if (this.isDraggable(fromIndex[0], fromIndex[1])) {
            // 深拷贝二维数组,避免修改原state
            const numbers = JSON.parse(JSON.stringify(this.state.numbers));
            numbers[toIndex[0]][toIndex[1]] = numbers[fromIndex[0]][fromIndex[1]];
            numbers[fromIndex[0]][fromIndex[1]] = -1;
            this.setState({ numbers });
        }
    }

    render() {
        return (
            <DndProvider backend={HTML5Backend}>
                <div className="grid-container">
                    {this.state.numbers.map((row, i) => (
                        <React.Fragment key={i}>
                            {row.map((number, j) => (
                                <div key={`${i}${j}`}>
                                    <Tile
                                        number={number}
                                        position={[i, j]}
                                        isAdjacent={this.isDraggable(i, j)}
                                        onDrop={this.handleDrag.bind(this)}
                                    />
                                </div>
                            ))}
                        </React.Fragment>
                    ))}
                </div>
            </DndProvider>
        );
    }
}

Tile类(Tile.js)

import React from 'react';
import { useDrag, useDrop } from 'react-dnd';

export function Tile({ number, position, isAdjacent, onDrop }) {
    const [{ isDragging }, drag] = useDrag({
        type: 'tile',
        item: { position },
        canDrag: isAdjacent && number !== -1, // 空白格不可拖拽
        collect: (monitor) => ({
            isDragging: !!monitor.isDragging(),
        }),
    });

    const [{ isOver }, drop] = useDrop({
        accept: 'tile',
        canDrop: () => number === -1, // 仅空白格可接受拖拽
        drop: (item) => onDrop(item.position, position),
        collect: (monitor) => ({
            isOver: !!monitor.isOver(),
        }),
    });

    const style = {
        opacity: isDragging ? 0.5 : 1,
        backgroundColor: isAdjacent ? 'lightgreen' : 'white',
        border: isOver ? '2px dashed black' : 'none',
        cursor: isAdjacent ? 'move' : 'default'
    };

    // 统一绑定ref到容器div
    const ref = (node) => {
        drag(node);
        drop(node);
    };

    return (
        <div
            ref={ref}
            style={style}
        >
            {number !== -1 && <div>{number}</div>}
        </div>
    );
}

关键修复说明

  1. 修正isDraggable逻辑:遍历找到空白格后,同时检查四个相邻方向,只要满足任一条件就返回true,避免提前返回false。
  2. 修复handleDrag参数:正确传递拖拽起始位置的行列索引到isDraggable。
  3. 遵守状态不可变原则:洗牌和状态更新时都深拷贝数组,避免直接修改state。
  4. 简化Tile组件Ref绑定:将drag和drop统一绑定到容器div,移除冗余的onDragStart和内部div的ref,避免逻辑冲突。
  5. 优化拖拽权限判断:在Tile的canDrag中明确禁止空白格本身被拖拽,canDrop仅允许空白格接受拖拽,逻辑更清晰。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 23:17:21