现代 React Web 开发实战——kanban实现卡片拖拽

news2025/7/22 5:50:59

前提摘要:
学习宋一玮 React 新版本 + 函数组件 &Hooks 优先
开篇就是函数组件+Hooks
实现的效果如下:
学到第11篇了 照葫芦画瓢,不过老师在讲解的过程中没有考虑拖拽目标项边界问题,我稍微处理了下这样就实现拖拽流畅了

在这里插入图片描述

下面就是主要的代码了,实现拖拽(src/App.js):
核心在于标记当前项,来源项,目标项,并且在拖拽完成时对数据处理,更新每一组数据(useState);

/** @jsxImportSource @emotion/react */
// 上面代码是使用emotion的关键CSS-in-JS
import React, { useEffect, useState, useRef } from "react";
import { css } from "@emotion/react";
import "./App.css";
const MINUTE = 60 * 1000;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
const UPDATE_INTERVAL = MINUTE;
// const ongoingList = [{ title: "进行任务", status: "2022-11-09 15:29" }];
// const doneList = [{ title: "完成任务", status: "2022-11-09 15:59" }];
const KanbanBoard = ({ children }) => (
  <main
    css={css`
      flex: 10;
      display: flex;
      flex-direction: row;
      gap: 1rem;
      margin: 0 1rem 1rem;
    `}
  >
    {children}
  </main>
);
const KanbanColumn = ({
  children,
  className,
  title,
  setIsDragSource = () => { },
  setIsDragTarget = () => { },
  onDrop,
}) => {
  const combinedClassName = `kanban-column ${className}`;
  return (
    <section
      className={combinedClassName}
      onDragStart={() => setIsDragSource(true)}
      onDragOver={(evt) => {
        evt.preventDefault();
        evt.dataTransfer.dropEffect = "move";
        setIsDragTarget(true);
      }}
      onDragLeave={(evt) => {
        evt.preventDefault();
        evt.dataTransfer.dropEffect = "none";
        setIsDragTarget(false);
      }}
      onDrop={(evt) => {
        evt.preventDefault();
        onDrop && onDrop(evt);
      }}
      onDragEnd={(evt) => {
        evt.preventDefault();
        setIsDragSource(false);
        setIsDragTarget(false);
      }}
    >
      <h2>{title}</h2>
      <ul>{children}</ul>
    </section>
  );
};
const KanbanCard = ({ title, status, onDragStart }) => {
  const [displayTime, setDisplayTime] = useState(status);
  useEffect(() => {
    const updateDisplayTime = () => {
      const timePassed = new Date() - new Date(status);
      let relativeTime = "刚刚";
      if (MINUTE <= timePassed && timePassed < HOUR) {
        relativeTime = `${Math.ceil(timePassed / MINUTE)} 分钟前`;
      } else if (HOUR <= timePassed && timePassed < DAY) {
        relativeTime = `${Math.ceil(timePassed / HOUR)} 小时前`;
      } else if (DAY <= timePassed) {
        relativeTime = `${Math.ceil(timePassed / DAY)} 天前`;
      }

      setDisplayTime(relativeTime);
    };
    const intervalId = setInterval(updateDisplayTime, UPDATE_INTERVAL);
    updateDisplayTime();
    return function cleanup() {
      clearInterval(intervalId);
    };
  }, [status]);
  const handleDragStart = (evt) => {
    evt.dataTransfer.effectAllowed = "move";
    evt.dataTransfer.setData("text/plain", title);
    onDragStart && onDragStart(evt);
  };
  return (
    <li className="kanban-card" draggable onDragStart={handleDragStart}>
      <div className="card-title">{title}</div>
      <div className="card-status">{displayTime}</div>
    </li>
  );
};
const AddKanbanCard = ({ onSubmit }) => {
  const [title, setTitle] = useState("");
  const handleChange = (evt) => {
    setTitle(evt.target.value);
  };
  const handleKeyDown = (evt) => {
    if (evt.key === "Enter") onSubmit(title);
  };
  const inputElem = useRef(null);
  useEffect(() => {
    inputElem.current.focus();
  });
  return (
    <li className="kanban-card">
      <h4>添加新卡片</h4>
      <div className="card-title">
        <input
          ref={inputElem}
          type="text"
          value={title}
          onChange={handleChange}
          onKeyDown={handleKeyDown}
        ></input>
      </div>
    </li>
  );
};
const DATE_STORE_KEY = "kanban_data_store";
const COLUMN_KEY_TODO = "todo";
const COLUMN_KEY_ONGONING = "ongoing";
const COLUMN_KEY_DONE = "done";
function App() {
  const [todoList, setTodoList] = useState([
    { title: "开发任务-1", status: "2022-05-22 18:15" },
  ]);
  const [ongoingList, setOngoingList] = useState([
    { title: "进行任务-1", status: "2022-08-22 18:15" },
  ]);
  const [doneList, setDoneList] = useState([
    { title: "完成任务-1", status: "2022-10-22 18:15" },
  ]);
  const [showAdd, setShowAdd] = useState(false);
  const handleAdd = (evt) => {
    setShowAdd(true);
  };
  const handleSubmit = (title) => {
    // todoList.unshift({title,status:new Date().toDateString()});
    setTodoList((current) => [{ title, status: new Date() + " " }, ...current]);
    setShowAdd(false);
  };
  const handleSaveAll = () => {
    const data = JSON.stringify({
      todoList,
      ongoingList,
      doneList,
    });
    window.localStorage.setItem(DATE_STORE_KEY, data);
  };
  useEffect(() => {
    const data = window.localStorage.getItem(DATE_STORE_KEY);
    setTimeout(() => {
      if (data) {
        const kanbanColumnData = JSON.parse(data);
        setTodoList(kanbanColumnData.todoList);
      }
    }, 1000);
  });
  const [draggedItem, setDraggedItem] = useState(null);
  const [dragSource, setDragSource] = useState(null);
  const [dragTarget, setDragTarget] = useState(null);
  const handleDrop = (evt) => {
    if (!draggedItem || !dragSource || !dragTarget || dragSource === dragTarget) { return; }
    const updaters = {
      [COLUMN_KEY_TODO]: setTodoList,
      [COLUMN_KEY_ONGONING]: setOngoingList,
      [COLUMN_KEY_DONE]: setDoneList
    };
    if (dragSource) {
      updaters[dragSource]((currentStat) => {
        return currentStat.filter((item) => !Object.is(item, draggedItem));
      });
    }
    if (dragTarget) {
      updaters[dragTarget]((currentStat) => {
        if (currentStat.length > 0) {
          return [draggedItem, ...currentStat]
        } else {
          return [draggedItem]
        }
      })
    }
  };
  return (
    <div className="App">
      <header className="App-header">
        <h1>
          我的看板<button onClick={handleSaveAll}>保存所有卡片</button>{" "}
        </h1>
      </header>
      <KanbanBoard>
        <KanbanColumn
          className="column-todo"
          title={
            <>
              待处理
              <button disabled={showAdd} onClick={handleAdd}>
                &#8853;添加新卡片
              </button>{" "}
            </>
          }
          setIsDragSource={(isSrc) =>
            setDragSource(isSrc ? COLUMN_KEY_TODO : null)
          }
          setIsDragTarget={(isTarget) =>
            setDragTarget(isTarget ? COLUMN_KEY_TODO : null)
          }
          onDrop={handleDrop}
        >
          {/* <h2>
            待处理
            <button disabled={showAdd} onClick={handleAdd}>
              &#8853;添加新卡片
            </button>{" "}
          </h2> */}
          {/* <ul> */}
          {showAdd && <AddKanbanCard onSubmit={handleSubmit} />}
          {todoList && todoList.map((item) => (
            <KanbanCard
              {...item}
              key={item.title}
              onDragStart={() => setDraggedItem(item)}
            />
          ))}
          {/* </ul> */}
        </KanbanColumn>
        <KanbanColumn className="column-ongoing" title={"进行中"} setIsDragSource={(isSrc) =>
          setDragSource(isSrc ? COLUMN_KEY_ONGONING : null)
        }
          setIsDragTarget={(isTarget) =>
            setDragTarget(isTarget ? COLUMN_KEY_ONGONING : null)
          }
          onDrop={handleDrop}>
          {/* <h2>进行中</h2>
          <ul> */}
          {ongoingList && ongoingList.map((item) => (
            <KanbanCard
              {...item}
              key={item.title}
              onDragStart={() => setDraggedItem(item)}
            />
          ))}
          {/* </ul> */}
        </KanbanColumn>
        <KanbanColumn className="column-done" title={"已处理"} setIsDragSource={(isSrc) =>
          setDragSource(isSrc ? COLUMN_KEY_DONE : null)
        }
          setIsDragTarget={(isTarget) =>
            setDragTarget(isTarget ? COLUMN_KEY_DONE : null)
          }
          onDrop={handleDrop}>
          {/* <h2>已处理</h2>
          <ul> */}
          {doneList && doneList.map((item) => (
            <KanbanCard
              {...item}
              key={item.title}
              onDragStart={() => setDraggedItem(item)}
            />
          ))}
          {/* </ul> */}
        </KanbanColumn>
      </KanbanBoard>
    </div>
  );
}

export default App;

这时拖拽基本完成,此时有一个bug,就是待处理添加新卡片的时候,拖拽之后的数据出现混乱!!如下所示:
在这里插入图片描述
首先问题定位,移动的来源项出现了问题,看代码之后发现拖拽处理来源项没有问题,那一定是那块调用更新todaList出现了问题,问题定位在useEffect,使用时如果useEffect的第二个参数不传就在组件所有更新都执行(即任何时候),传个空数组仅在挂载和卸载的时候执行,或者传个你想要去进行更新时候去执行(默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执行。)
更改代码如下:

  useEffect(() => {
    const data = window.localStorage.getItem(DATE_STORE_KEY);
    setTimeout(() => {
      if (data) {
        const kanbanColumnData = JSON.parse(data);
        setTodoList(kanbanColumnData.todoList);
      }
    }, 1000);
  },[]);

useEffect新增空数组效果展示:
在这里插入图片描述

学习一个新的框架总是会进行对比,
一:React的单项数据流和Vue中的双向绑定有什么区别?
在我看了Vue 双向绑定其实是语法糖罢了,其原理其实是Object.defineProperty()对数据进行劫持,监听到变化就去对数据进行更改;
而React 中的单项数据流做到了对原有数据的保护,你不能去直接去对Props进行更改,而是在需要赋值给State,然后再SetState中去进行更改,当然Hooks中提供了useState方法,使得开发者更加方便的去对数据进行处理和更改(我可太喜欢Hooks了很省事!!)
二:JSX 是什么?
学习React的时候,写组件的时候写页面元素是用JSX来写的,即Render里面是用JSX来实现的,渲染之后其实质是React.createElement,他只是语法糖 实现React组件的一部分而已,对比Vue中Template,(我更喜欢Vue的实现,更符合开发者)不过Vue也可用JSX来实现;
三:函数式组件(Hooks)与类组件(Class)优缺点?
宋一玮老师的数据表明函数式比类组件使用更多,并且函数组件基本上涵盖了类组件的功能点,除了(只有类组件才能成为错误边界)
从React官网中开局也是用类组件来领进门的,我是看完了官网的基础才来学习课程的才觉得学习没有那么吃力反而能加深理解;(老师的反其道而行可能不太适合初学者)
四:CSS可否想JS一样应用在组件中?
可以的,使用emotion来应用到JSX中(主要代码中有使用),不过CSS中传入JS数据确实很方便但是在运行emotion时会创建大量的<style>标签,有可能影响页面性能。
CSS-in-JS 技术能帮我们做到样式隔离、提升组件样式的可维护性、可复用性。
五:常用的hooks——useState,useEffect、useRef
继续学习,持续输出!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/7765.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Flink基础原理

一、Flink的概述 我感觉就是一个实时的流处理程序,可以实时的从数据源读取数据,然后根据设置好的一系列算法, 对数据进行处理,最终输出到目的存储介质&#xff08;数据库、缓存等&#xff09;中去,和jdk1.8里面的数据流处理很像, 也有并行流、map、fifter等处理。二、Flink的基…

实验八 数据处理与多项式处理(matlab)

实验八 数据处理与多项式处理 1.1实验目的 1.2实验内容 1.3流程图 1.4程序清单 1.5运行结果及分析 1.6实验的收获与体会 1.1实验目的 1&#xff0c;掌握数据统计和分析的方法&#xff1b; 2&#xff0c;掌握数值插值与曲线拟合的方法&#xff1b; 3&#xff0…

如何使用 .Net Core 实现数据库迁移 (Database Migration)

当我们在编写基于数据库的应用程序时&#xff0c;随着需求的增加和改变&#xff0c;我们需要升级我们的数据库&#xff0c;变更数据库表的字段&#xff0c;当我们的系统的不同版本被部署到了不同的客户那里&#xff0c;在需要给客户升级时&#xff0c;我们如何实现数据库模式 (…

注解和反射

注解和反射注解元注解反射注解 注解和注释的区别 注解 annotation写在程序之中&#xff0c;程序可以识别&#xff0c;做出相应的动作处理&#xff0c;具有检查和约束程序的作用 注释 comment 写在程序之中&#xff0c;供人参考&#xff0c;提示使用&#xff0c;程序会自动忽…

云原生系统学习[Kubernetes]——02 Pod、Deployment、Service

云原生系统学习[Kubernetes]——02 Pod、Deployment、Service [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t9RomXCf-1668486830453)(./assets/image-20221103113345300.png)] 参考资料 什么是YAMLk8s官网文档k8s中文社区k8s-book 学多少&#…

Java:继承和多态

文章目录前言一、继承1.继承概念1.1 继承的语法1.2 父类成员方法1.2.1 子类访问父类的成员变量1.2.2 子类访问父类的成员方法1.3 super、this 关键字1.4 子类构造方法1.5 继承的方式1.6 final 关键字1.7 继承与组合二、多态2.1 多态的概念2.2 多态实现的条件2.3 对重写的认识2.…

Canvas 基础使用

一、基本的画布功能 创建 <canvas>元素时至少要设置 width 和 height 属性&#xff0c;这样才能告诉浏览器在多大面积上绘图。出现在标签包裹里的内容会在浏览器不支持 <canvas>元素时显示。比如&#xff1a; <canvas id"drawing" width"200&q…

PumpkinBook Reading(一)

绪论 基本术语 “算法”是指从数据中学得“模型”的具体方法&#xff0c;“算法”产出的结果称为“模型”&#xff0c;通常是具体的函数或者可抽象地看作为函数。 样本&#xff1a;也称为“示例”&#xff0c;是关于一个事件或对象的描述。因为要想让计算机能对现实生活中的事…

【Python开发】Flask开发实战:个人博客(三)

Flask开发实战&#xff1a;个人博客&#xff08;三&#xff09;在【Python开发】Flask开发实战&#xff1a;个人博客&#xff08;一&#xff09; 中&#xff0c;我们已经完成了 数据库设计、数据准备、模板架构、表单设计、视图函数设计、电子邮件支持 等总体设计的内容。 在【…

公众号裂变拉新,以婴儿辅食为诱饵,实现低成本获客!

大家好~我是娜娜 今天来给大家拆解一个关于食品行业精选公众号增长案例&#xff0c;通过公众号裂变拉新&#xff0c;任务拉新人数5000&#xff0c;留存率达到85%&#xff0c;活动裂变率达到1100.86%。活动数据也还在持续的上升当中。 该公众号的目标人群是新手爸妈&#xff0…

【Java】SpringBoot应用简单示例

SpringBoot应用简单示例SpringBoot应用简单示例HelloWorld搭建项目ResponseBody的作用ComponentScan排除扫描beanSpringBoot集成日志SpringBoot日志初始化原理消息转换器拦截器过滤器操作数据库Spring Data JpaDruid数据源Mybatis-Plus事务处理操作缓存AOP相关概念栗子定时任务…

C语言解析JSON源码

它与 XML 的地位差不多&#xff0c;但就笔者而言&#xff0c;笔者更喜欢 JSON 的风格&#xff0c;因为它更符合我们的思维习惯&#xff0c;同样一份数据&#xff0c;JSON 格式的就是比 XML 要清晰明了一些。 最近笔者需要在 C语言 上解析 JSON 格式&#xff0c;在网上一顿找&am…

XC5VLX30T-2FF323I Virtex-5 LXT FPGA IC 产品参数

概述 Virtex-5 FPGA有-3&#xff0c;-2&#xff0c;-1速度等级&#xff0c;其中-3具有最高的性能。Virtex-5 FPGA直流和交流特性指定为商业和工业级别。除工作温度范围外&#xff0c;除非另有说明&#xff0c;所有直流和交流电气参数对于特定转速等级是相同的(即-1转速等级的工…

一夜登顶GitHub!字节内网数据结构与算法刷题笔记,看完直呼卧槽

网络上流传着一句段子“程序员两条腿&#xff0c;一条是算法&#xff0c;一条是英文&#xff0c;想跑的更远&#xff0c;这两条腿都不能弱”。英文&#xff0c;我们暂且不谈&#xff0c;我们先来谈谈算法。 算法之难&#xff0c;在于将精巧的逻辑&#xff0c;通过合适的数据结…

2 分钟,教你用 Serverless 每天给女朋友自动发土味情话

作者&#xff1a;安可 Serverless 简介 Serverless&#xff0c;中文意思是 “无服务器”&#xff0c;所谓的无服务器并非是说不需要依靠服务器等资源&#xff0c;而是说开发者再也不用过多考虑服务器的问题&#xff0c;可以更专注在产品代码上&#xff0c;同时计算资源也开始…

如何根据自己的SCI论文,匹配适合的期刊? - 易智编译EaseEditing

如何选择合适的目标期刊是需要慎重对待的问题&#xff0c;它决定了你论文的发表速度和被认可度。 可以遵循以下几个步骤来考虑&#xff1a; 1、从你论文的参考文献中选择合适的期刊&#xff08;如果引用文献较少&#xff0c;也可以从引文的参考文献中进行筛选&#xff09;&…

成功解决:ModuleNotFoundError: No module named ‘amp_C‘

在使用transformers时&#xff0c;在调用Trainer的时候遇到了这个问题&#xff0c;原因是apex包有问题&#xff0c; 这里有解决apex安装包的多一些教程 https://blog.csdn.net/Xidian185/article/details/122745427 https://blog.csdn.net/weixin_45225975/article/details/119…

倍福TwinCAT3中使用久同伺服

目录 一、测试设备说明 二、伺服通电和参数设置 1、恢复出厂参数设置 2、恢复出厂&#xff0c;重启后 3、伺服自己点动操作 4、增益、刚度调整 5、伺服零位设定 6、伺服转动一圈编码器脉冲量设定 7、参数保存 三、伺服操作面板 四、TwinCAT3工程配置 1、XML文件 2、…

【元宇宙欧米说】打造艺术与技术构建的交互式数字旅程

Web3 to Earn项目如何扩大应用功能和场景&#xff1f;在Web3时代怎么才能以更新颖、有趣的方式追赶潮流&#xff1f;各Web3领域项目及应用如何进行功能外延以满足用户需求&#xff1f; 11月17日晚上九点&#xff0c;ZenCats项目管理员Fred将以“打造艺术与技术构建的交互式数字…

编码格式转换方法

今天项目上遇到了需要将 SJIS(Shift-JIS) 格式与 UTF8 格式相互转换问题。 首先看一个编码格式问题引发的乱码现象&#xff0c;新建下面的文本文档&#xff0c;然后更名为 test.bat。 echo off echo test chinese character view 测试中文字符显示 pause双击运行 用 chcp 查…