50天50个小项目 (React19 + Tailwindcss V4) ✨ | DragNDrop(拖拽占用组件)

📅 我们继续 50 个小项目挑战!------ DragNDrop 组件

仓库地址:https://gitee.com/hhm-hhm/50days50projects.git

构建一个支持拖拽交互的图片拖放组件。该组件允许用户将一张图片从一个容器拖动并释放到另一个"空位"中,并带有视觉反馈(如悬停高亮、背景变化等)。

🌀 组件目标

  • 创建多个"空位"容器
  • 默认展示一张可拖动的图片
  • 支持拖拽交互并投放到任意空位
  • 投放后更新对应位置的图片状态
  • 拖拽过程中提供视觉反馈(如悬停样式)
  • 使用 TailwindCSS快速构建现代 UI 界面

🔧 DragNDrop.tsx组件实现

TypeScript 复制代码
import React, { useState } from 'react'

const DragNDrop: React.FC = () => {
    const [filledIndex, setFilledIndex] = useState<number>(0)
    const [isHovered, setIsHovered] = useState<boolean[]>(Array(5).fill(false))

    const imageUrls = [
        'https://picsum.photos/id/10/150/150',
        'https://picsum.photos/id/11/150/150',
        'https://picsum.photos/id/12/150/150',
        'https://picsum.photos/id/13/150/150',
        'https://picsum.photos/id/14/150/150',
    ]

    const empties = Array.from({ length: 5 }, (_, i) => i)

    // 拖拽开始:设置被拖拽的元素标识(这里用 filledIndex)
    const dragStart = (e: React.DragEvent<HTMLDivElement>) => {
        e.dataTransfer.setData('text/plain', filledIndex.toString())
        e.dataTransfer.effectAllowed = 'move'
    }

    const dragEnd = () => {
        // 可选:添加拖拽结束效果(如重置样式)
    }

    const dragOver = (e: React.DragEvent<HTMLDivElement>) => {
        e.preventDefault() // 必须阻止默认行为才能触发 drop
    }

    const dragEnter = (index: number) => {
        setIsHovered((prev) => {
            const newState = [...prev]
            newState[index] = true
            return newState
        })
    }

    const dragLeave = (index: number) => {
        setIsHovered((prev) => {
            const newState = [...prev]
            newState[index] = false
            return newState
        })
    }

    const dragDrop = (index: number, e: React.DragEvent<HTMLDivElement>) => {
        e.preventDefault()
        const draggedIndexStr = e.dataTransfer.getData('text/plain')
        const draggedIndex = parseInt(draggedIndexStr, 10)

        if (!isNaN(draggedIndex) && draggedIndex !== index) {
            setFilledIndex(index)
        }

        // 清除所有 hover 状态
        setIsHovered(Array(5).fill(false))
    }

    return (
        <div className="flex h-screen items-center justify-center overflow-hidden bg-gray-900">
            {empties.map((_, index) => (
                <div
                    key={index}
                    className={`m-2 h-36 w-36 border-4 border-black bg-white ${
                        isHovered[index] ? 'border-dashed border-black bg-gray-800' : ''
                    }`}
                    onDragOver={dragOver}
                    onDragEnter={() => dragEnter(index)}
                    onDragLeave={() => dragLeave(index)}
                    onDrop={(e) => dragDrop(index, e)}>
                    {index === filledIndex && (
                        <div
                            className="h-full w-full cursor-move bg-cover transition-all duration-200 ease-in-out"
                            style={{ backgroundImage: `url(${imageUrls[index]})` }}
                            draggable
                            onDragStart={dragStart}
                            onDragEnd={dragEnd}
                        />
                    )}
                </div>
            ))}
            <div className="fixed right-20 bottom-5 text-2xl text-red-500">CSDN@Hao_Harrision</div>
        </div>
    )
}

export default DragNDrop

🔧 转换说明

功能 Vue 3 (Composition API) React + TS
响应式状态 const filledIndex = ref(0) const isHovered = ref([...]) const [filledIndex, setFilledIndex] = useState(0) const [isHovered, setIsHovered] = useState([...])
列表渲染 v-for="(empty, index) in empties" {empties.map((_, index) => <div key={index}>...)}
动态 class :class="[isHovered[index] && 'border-dashed ...']" 使用模板字符串或条件表达式: className={... ${isHovered[index] ? 'border-dashed bg-gray-800' : ''}}
事件绑定 @dragenter="dragEnter(index)" onDragEnter={() => dragEnter(index)}
事件对象类型 自动推导 显式标注:e: React.DragEvent<HTMLDivElement>
阻止默认行为 @dragover.prevent e.preventDefault() 必须在 onDragOver 中手动调用
拖拽数据传递 无显式设置(逻辑隐含) 必须通过 e.dataTransfer.setData('text/plain', value) 传递
获取拖拽数据 e.dataTransfer.getData('text/plain')
内联样式 :style="{ backgroundImage: url(...) }" style={``{ backgroundImage: \url(${url})` }}`

⚠️ 常见差异与注意事项

1. HTML5 拖拽在 React 中必须显式处理

  • Vue 的 .prevent 修饰符自动阻止默认行为;
  • React 中 必须手动调用 e.preventDefault()onDragOveronDrop 中,否则 drop 事件不会触发。

2. 状态更新不可变性

  • Vue 可直接修改 isHovered.value[index] = true

  • React 必须使用 不可变更新

    TypeScript 复制代码
    setIsHovered(prev => {
      const newState = [...prev];
      newState[index] = true;
      return newState;
    });

3. 事件处理器传参方式不同

  • Vue:@dragenter="dragEnter(index)" 直接传参;
  • React:需用箭头函数包裹:onDragEnter={() => dragEnter(index)}

4. draggable 属性

  • Vue:draggable="true"(字符串)
  • React:draggable(布尔属性,写成 <div draggable /> 即可,但写 draggable={true} 也合法)

5. CSS 过渡效果

  • Vue 使用 <style scoped> 定义 [draggable='true']
  • React 中建议:
    • 方式一:在全局 CSS 中定义(如 index.css);

    • 方式二(推荐):直接用 Tailwind 类实现过渡:

      复制代码
      className="transition-all duration-200 ease-in-out"

✅ 最佳实践建议

场景 推荐做法
拖拽标识传递 使用 dataTransfer.setData('text/plain', id) 传递唯一标识
防止无效移动 drop 中判断 if (from !== to) 再更新状态
hover 状态管理 使用数组记录每个格子的悬停状态,确保精准控制
图片 URL 管理 imageUrls 作为常量或 props,避免硬编码
无障碍与 UX 添加 cursor-moveuser-select: none 提升体验

🎨 TailwindCSS 样式重点讲解

类名 作用
h-screen, items-center, justify-center 全屏高度 + 内容居中布局
overflow-hidden 防止内容溢出
bg-sky-500 设置背景颜色为浅蓝色
h-36, w-36 设置每个容器的宽高为 36(9rem)
m-2 设置外边距为 2(0.5rem)
border-4, border-black 黑色边框
bg-white / bg-gray-800 默认和悬停状态下的背景颜色
border-dashed 悬停时边框变为虚线
cursor-pointer 设置图片区域为可点击
bg-cover 图片背景自适应填充
transition 添加拖拽过程中的平滑过渡动画
[🎯 TailwindCSS 样式说明]

🦌 路由组件 + 常量定义

router/index.tsxchildren数组中添加子路由

TypeScript 复制代码
{
    path: '/',
    element: <App />,
    children: [
       ...
       {
                path: '/DragNDrop',
                lazy: () =>
                    import('@/projects/DragNDrop.tsx').then((mod) => ({
                        Component: mod.default,
                    })),
            },
    ],
 },

constants/index.tsx 添加组件预览常量

TypeScript 复制代码
import demo21Img from '@/assets/pic-demo/demo-21.png'
省略部分....
export const projectList: ProjectItem[] = [
    省略部分....
     {
        id: 21,
        title: 'Drag-and-drop Occupation',
        image: demo21Img,
        link: 'DragNDrop',
    },

🚀 小结

进一步扩展的功能推荐:

  • ✅ 支持多张图片同时拖动
  • ✅ 支持图片预览拖拽(不立即改变原图位置)
  • ✅ 拖拽时高亮目标容器边界
  • ✅ 支持触摸设备拖拽交互(移动端适配)
  • ✅ 封装为可复用组件(支持 props 传入图片列表)

📅 明日预告: 我们将完成DrawApp组件,创建一个画板具有调节画笔粗细的功能,并且能够一键清除画板上的内容。🚀

原文链接:https://blog.csdn.net/qq_44808710/article/details/149103822

每天造一个轮子,码力暴涨不是梦!🚀

相关推荐
来杯三花豆奶2 小时前
Vue 2.0 Mixins 详解:从原理到实践的深度解析
前端·javascript·vue.js
code_YuJun2 小时前
脚手架开发工具——dotenv
前端
小鱼小鱼干2 小时前
【Gemini简直无敌了】掌间星河:通过MediaPipe实现手势控制粒子
react.js·gemini
San30.2 小时前
深度驱动:React Hooks 核心之 `useState` 与 `useEffect` 实战详解
前端·javascript·react.js
Mr_Swilder2 小时前
vscode没有js提示:配置jsconfig配置
前端
skywalk81632 小时前
使用Trae 自动编程:为小学生学汉语项目增加不同出版社教材的区分
服务器·前端·人工智能·trae
huohuopro3 小时前
LangChain | LangGraph V1教程 #3 从路由器到ReAct架构
前端·react.js·langchain
柒.梧.3 小时前
HTML入门指南:30分钟掌握网页基础
前端·javascript·html
用户54277848515403 小时前
Promise :从基础原理到高级实践
前端