当前位置: 首页 > news >正文

【实战】十一、看板页面及任务组页面开发(四) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(二十六)

文章目录

    • 一、项目起航:项目初始化与配置
    • 二、React 与 Hook 应用:实现项目列表
    • 三、TS 应用:JS神助攻 - 强类型
    • 四、JWT、用户认证与异步请求
    • 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
    • 六、用户体验优化 - 加载中和错误状态处理
    • 七、Hook,路由,与 URL 状态管理
    • 八、用户选择器与项目编辑功能
    • 九、深入React 状态管理与Redux机制
    • 十、用 react-query 获取数据,管理缓存
    • 十一、看板页面及任务组页面开发
      • 1~3
      • 4~6
      • 7&8
      • 9.拖拽实现
      • 10.拖拽持久化


学习内容来源:React + React Hook + TS 最佳实践-慕课网


相对原教程,我在学习开始时(2023.03)采用的是当前最新版本:

版本
react & react-dom^18.2.0
react-router & react-router-dom^6.11.2
antd^4.24.8
@commitlint/cli & @commitlint/config-conventional^17.4.4
eslint-config-prettier^8.6.0
husky^8.0.3
lint-staged^13.1.2
prettier2.8.4
json-server0.17.2
craco-less^2.0.0
@craco/craco^7.1.0
qs^6.11.0
dayjs^1.11.7
react-helmet^6.1.0
@types/react-helmet^6.1.6
react-query^6.1.0
@welldone-software/why-did-you-render^7.0.1
@emotion/react & @emotion/styled^11.10.6

具体配置、操作和内容会有差异,“坑”也会有所不同。。。


一、项目起航:项目初始化与配置

  • 一、项目起航:项目初始化与配置

二、React 与 Hook 应用:实现项目列表

  • 二、React 与 Hook 应用:实现项目列表

三、TS 应用:JS神助攻 - 强类型

  • 三、 TS 应用:JS神助攻 - 强类型

四、JWT、用户认证与异步请求

  • 四、 JWT、用户认证与异步请求(上)

  • 四、 JWT、用户认证与异步请求(下)

五、CSS 其实很简单 - 用 CSS-in-JS 添加样式

  • 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(上)

  • 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(下)

六、用户体验优化 - 加载中和错误状态处理

  • 六、用户体验优化 - 加载中和错误状态处理(上)

  • 六、用户体验优化 - 加载中和错误状态处理(中)

  • 六、用户体验优化 - 加载中和错误状态处理(下)

七、Hook,路由,与 URL 状态管理

  • 七、Hook,路由,与 URL 状态管理(上)

  • 七、Hook,路由,与 URL 状态管理(中)

  • 七、Hook,路由,与 URL 状态管理(下)

八、用户选择器与项目编辑功能

  • 八、用户选择器与项目编辑功能(上)

  • 八、用户选择器与项目编辑功能(下)

九、深入React 状态管理与Redux机制

  • 九、深入React 状态管理与Redux机制(一)

  • 九、深入React 状态管理与Redux机制(二)

  • 九、深入React 状态管理与Redux机制(三)

  • 九、深入React 状态管理与Redux机制(四)

  • 九、深入React 状态管理与Redux机制(五)

十、用 react-query 获取数据,管理缓存

  • 十、用 react-query 获取数据,管理缓存(上)

  • 十、用 react-query 获取数据,管理缓存(下)

十一、看板页面及任务组页面开发

1~3

  • 十一、看板页面及任务组页面开发(一)

4~6

  • 十一、看板页面及任务组页面开发(二)

7&8

  • 十一、看板页面及任务组页面开发(三)

9.拖拽实现

接下来的内容将会是整个课程最难的部分,相关知识也不是很常用

  • 安装拖拽功能库 react-beautiful-dnd 及相关类型文件库
npm i react-beautiful-dnd ## --force
npm i @types/react-beautiful-dnd -D ## --force 

接下来在原功能库的基础上自行二次封装

新建 src\components\grag-and-drop.tsx

import React, { ReactNode } from "react"
import { Draggable, DraggableProps, Droppable, DroppableProps, DroppableProvided, DroppableProvidedProps } from "react-beautiful-dnd"// 删除原有 “函数类型” children,使用 ReactNode 类型的 children
type DropProps = Omit<DroppableProps, 'children'> & { children: ReactNode }export const Drop = ({ children, ...props }: DropProps) => {return (<Droppable {...props}>{(provided) => {if (React.isValidElement(children)) {return React.cloneElement(children, {...provided.droppableProps,ref: provided.innerRef,provided,});}return <div />;}}</Droppable>);
};type DropChildProps =Partial<{ provided: DroppableProvided } & DroppableProvidedProps> &React.HTMLAttributes<HTMLDivElement>export const DropChild = React.forwardRef<HTMLDivElement, DropChildProps>(({children, ...props}, ref) => <div ref={ref} {...props} >{children}{props.provided?.placeholder}</div>
);type DragProps = Omit<DraggableProps, 'children'> & { children: ReactNode }
export const Drag = ({children, ...props}: DragProps) => {return <Draggable {...props}>{(provided => {if(React.isValidElement(children)) {return React.cloneElement(children, {...provided.draggableProps,...provided.dragHandleProps,ref: provided.innerRef})}return <div/>})}</Draggable>
}

forwardRef 是用作转发的,经过包裹后的组件可以传入 ref 属性

  • Refs 转发 – React

这步报错:ref: provided.innerRef

Argument of type '{ ref: (element: HTMLElement | null) => void; 'data-rbd-drag-handle-draggable-id'?: string | undefined; 'data-rbd-drag-handle-context-id'?: string | undefined; 'aria-describedby'?: string | undefined; ... 7 more ...; onTransitionEnd?: React.TransitionEventHandler<...> | undefined; }' is not assignable to parameter of type 'Partial<unknown> & Attributes'.Object literal may only specify known properties, and 'ref' does not exist in type 'Partial<unknown> & Attributes'.

接下来使用这个组件

编辑 src\screens\ViewBoard\index.tsx

...
import { DragDropContext } from "react-beautiful-dnd";
import { Drag, Drop, DropChild } from "components/grag-and-drop";export const ViewBoard = () => {useDocumentTitle("看板列表");const { data: currentProject } = useProjectInUrl();const { data: viewboards, isLoading: viewBoardIsLoading } = useViewboards(useViewBoardSearchParams());const { isLoading: taskIsLoading } = useTasks(useTasksSearchParams());const isLoading = taskIsLoading || viewBoardIsLoading;return (<DragDropContext onDragEnd={() => {}}><ViewContainer><h1>{currentProject?.name}看板</h1><SearchPanel />{isLoading ? (<Spin />) : (<Drop type='COLUMN' direction='horizontal' droppableId="viewboard"><ColumnsContainer>{viewboards?.map((vbd, index) => (<Drag key={vbd.id} draggableId={'viewboard' + vbd.id} index={index}><ViewboardColumn viewboard={vbd} key={vbd.id} /></Drag>))}<CreateViewBoard /></ColumnsContainer></Drop>)}<TaskModal /></ViewContainer></DragDropContext>);
};export const ColumnsContainer = styled(DropChild)`display: flex;overflow-x: scroll;flex: 1;
`;

编辑 src\screens\ViewBoard\components\ViewboardCloumn.tsx 使组件可以透传 props 以及通过 forwardRef 转发 传入 ref:

...
export const ViewboardColumn = React.forwardRef<HTMLDivElement, { viewboard: Viewboard }>(({ viewboard, ...props }, ref) => {const { data: allTasks } = useTasks(useTasksSearchParams());const tasks = allTasks?.filter((task) => task.kanbanId === viewboard.id);return (<Container {...props} ref={ref}><Row><h3>{viewboard.name}</h3><More viewboard={viewboard} key={viewboard.id}/></Row><TasksContainer>{tasks?.map((task) => (<TaskCard key={task.id} task={task} />))}<CreateTask kanbanId={viewboard.id} /></TasksContainer></Container>);
});

10.拖拽持久化

拖拽的时候 看板之间的间隔应该是不变的

编辑 src\screens\ViewBoard\index.tsx(调整组件层级并显式使用 DropChild):

...
export const ViewBoard = () => {...return (<DragDropContext onDragEnd={() => {}}><ViewContainer><h1>{currentProject?.name}看板</h1><SearchPanel />{isLoading ? (<Spin />) : (<ColumnsContainer><Drop type="COLUMN" direction="horizontal" droppableId="viewboard"><DropChild style={{display: 'flex'}}>{viewboards?.map((vbd, index) => (<Dragkey={vbd.id}draggableId={"viewboard" + vbd.id}index={index}><ViewboardColumn viewboard={vbd} key={vbd.id} /></Drag>))}</DropChild></Drop><CreateViewBoard /></ColumnsContainer>)}<TaskModal /></ViewContainer></DragDropContext>);
};export const ColumnsContainer = styled.div`display: flex;overflow-x: scroll;flex: 1;
`;

接下来做 任务拖拽排序

编辑 src\screens\ViewBoard\components\ViewboardCloumn.tsx:

...
import { Drag, Drop, DropChild } from "components/grag-and-drop";
...
export const ViewboardColumn = React.forwardRef<HTMLDivElement,{ viewboard: Viewboard }
>(({ viewboard, ...props }, ref) => {const { data: allTasks } = useTasks(useTasksSearchParams());const tasks = allTasks?.filter((task) => task.kanbanId === viewboard.id);return (<Container {...props} ref={ref}><Row><h3>{viewboard.name}</h3><More viewboard={viewboard} key={viewboard.id} /></Row><TasksContainer><Drop type="Row" direction="vertical" droppableId={'task' + viewboard.id}><DropChild>{tasks?.map((task, taskIndex) => (<Dragkey={task.id}draggableId={"task" + task.id}index={taskIndex}><TaskCard key={task.id} task={task} /></Drag>))}</DropChild></Drop><CreateTask kanbanId={viewboard.id} /></TasksContainer></Container>);
});

拖拽功能好了,接下来将拖拽结果持久化到数据库中

编辑 src\utils\use-optimistic-options.ts(看板和任务排序 获取URL参数,为后续乐观更新做准备):

...
export const useReorderViewboardConfig = (queryKey: QueryKey) =>useConfig(queryKey, (target, old) => old ? [old, ...target] : []);export const useReorderTaskConfig = (queryKey: QueryKey) =>useConfig(queryKey, (target, old) => old || []);

编辑 src\utils\viewboard.ts(新增看板排序接口的 Custom Hook, SortPropsuseReorderTask 共用):

...
export interface SortProps {// 要重新排序的 itemfromId: number;// 目标 itemreferenceId: number;// 放在目标 Item 的前还是后type: 'before' | 'after';fromKanbanId?: number;toKanbanId?: number;
}export const useReorderViewboard = () => {const client = useHttp();return useMutation((params: SortProps) => {return client("kanbans/reorder", {data: params,method: "POST",});}, useReorderViewboardConfig(queryKey));
};

编辑 src\utils\task.ts(新增看板排序接口的 Custom Hook):

...
export const useReorderTask = (queryKey: QueryKey) => {const client = useHttp();return useMutation((params: SortProps) => {return client("tasks/reorder", {data: params,method: "POST",});}, useReorderTaskConfig(queryKey));
};

编辑 src\screens\ViewBoard\index.tsx(完善之前预留的 onDragEnd):

...
import { useReorderViewboard, useViewboards } from "utils/viewboard";
import {useProjectInUrl,useTasksQueryKey,useTasksSearchParams,useViewBoardQueryKey,useViewBoardSearchParams,
} from "./utils";
...
import { useReorderTask, useTasks } from "utils/task";
...
import { useCallback } from "react";export const ViewBoard = () => {...const onDragEnd = useDragEnd();return (<DragDropContext onDragEnd={onDragEnd}>...</DragDropContext>);
};export const useDragEnd = () => {const { data: viewboards } = useViewboards(useViewBoardSearchParams());const { mutate: reorderViewBoard } = useReorderViewboard(useViewBoardQueryKey());const { mutate: reorderTask } = useReorderTask(useTasksQueryKey());const { data: allTasks = [] } = useTasks(useTasksSearchParams());return useCallback(({ source, destination, type }: DropResult) => {if (!destination) {return;}// 看板排序if (type === "COLUMN") {const fromId = viewboards?.[source.index].id;const toId = viewboards?.[destination.index].id;if (!fromId || !toId || fromId === toId) {return;}const type = destination.index > source.index ? "after" : "before";reorderViewBoard({ fromId, referenceId: toId, type });}if (type === "ROW") {const fromKanbanId = +source.droppableId;const toKanbanId = +destination.droppableId;if (fromKanbanId === toKanbanId) {return;}const fromTask = allTasks.filter((task) => task.kanbanId === fromKanbanId)[source.index];const toTask = allTasks.filter((task) => task.kanbanId === toKanbanId)[destination.index];if (fromTask?.id === toTask?.id) {return;}reorderTask({fromId: fromTask?.id,referenceId: toTask?.id,fromKanbanId,toKanbanId,type:fromKanbanId === toKanbanId && destination.index > source.index? "after": "before",});}},[viewboards, reorderViewBoard, allTasks, reorderTask]);
};
...

至此,拖拽持久化完成,查看效果验证


部分引用笔记还在草稿阶段,敬请期待。。。

http://www.lryc.cn/news/143483.html

相关文章:

  • 解决docker无法执行定时任务问题
  • 【FreeRTOS】【STM32】中断详细介绍
  • stm32串口通信(PC--stm32;中断接收方式;附proteus电路图;开发方式:cubeMX)
  • 计算机毕设 基于机器学习与大数据的糖尿病预测
  • 【数据结构】——查找、散列表的相关习题
  • 提升Java开发效率:掌握HashMap的常见方法与基本原理
  • PostgreSQL系统概述
  • 掌握AI助手的魔法工具:解密Prompt(提示)在AIGC时代的应用「中篇」
  • git svn:使用 git 命令来管理 svn 仓库
  • 软考高级系统架构设计师系列论文九十一:论分布式数据库的设计与实现
  • GeoHash之存储篇
  • 后端项目开发:集成接口文档(swagger-ui)
  • 代码随想录训练营29天|●* 491.递增子序列 * 46.全排列 * 47.全排列 II
  • uniapp日期选择组件优化
  • AI驱动的大数据创新:探索软件开发中的机会和挑战
  • 国产化-银河麒麟V10系统及docker的安装
  • 计算机毕设 基于机器视觉的二维码识别检测 - opencv 二维码 识别检测 机器视觉
  • Redis原理剖析
  • 【送书活动】AI时代,程序员需要焦虑吗?
  • 什么是 JSON:理解和运用 JSON 的基本概念
  • CSDN每日一练 |『异或和』『生命进化书』『熊孩子拜访』2023-08-27
  • 整数拆分乘积最大
  • 浅谈 Linux 下 vim 的使用
  • leetcode:只出现一次的数字Ⅲ(详解)
  • 【vue3.0 使用组合式定义组件】
  • Tensor-动手学深度学习-李沐_笔记
  • Kafka生产者原理 kafka生产者发送流程 kafka消息发送到集群步骤 kafka如何发送消息 kafka详解
  • Uniapp笔记(七)uniapp打包
  • 软考高级系统架构设计师系列论文七十六:论基于构件的软件开发
  • 基于Thinkphp6框架全新UI的AI网址导航系统源码