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

news/2024/11/17 3:50:10/

文章目录

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


学习内容来源: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.编辑任务功能

接下来新建编辑任务的组件:

先准备好调用编辑任务接口和获取任务详情的 Hook,编辑 src\utils\task.ts

...
import { useAddConfig, useEditConfig } from "./use-optimistic-options";export const useEditTask = (queryKey: QueryKey) => {const client = useHttp();return useMutation((params: Partial<Task>) =>client(`tasks/${params.id}`, {method: "PATCH",data: params,}),useEditConfig(queryKey));
};export const useTask = (id?: number) => {const client = useHttp();return useQuery<Task>(["task", id], () => client(`tasks/${id}`), {enabled: Boolean(id),});
};

编辑 src\screens\ViewBoard\utils.ts(新增 useTasksModal):

...
// import { useDebounce } from "utils";
import { useTask } from "utils/task";
...export const useTasksSearchParams = () => {const [param] = useUrlQueryParam(["name","typeId","processorId","tagId",]);const projectId = useProjectIdInUrl();// const debouncedName = useDebounce(param.name)return useMemo(() => ({projectId,typeId: Number(param.typeId) || undefined,processorId: Number(param.processorId) || undefined,tagId: Number(param.tagId) || undefined,// name: debouncedName,name: param.name,}),// [projectId, param, debouncedName][projectId, param]);
};...export const useTasksModal = () => {const [{ editingTaskId }, setEditingTaskId] = useUrlQueryParam(['editingTaskId'])const { data: editingTask, isLoading } = useTask(Number(editingTaskId))const startEdit = useCallback((id: number) => {setEditingTaskId({editingTaskId: id})}, [setEditingTaskId])const close = useCallback(() => {setEditingTaskId({editingTaskId: ''})}, [setEditingTaskId])return {editingTaskId,editingTask,startEdit,close,isLoading}
}

视频中使用 useDebounce 使得完全停止输入后才开始搜索,避免输入过程中频繁搜索造成系统资源浪费,且影响用户体验,博主这样更改后中文输入法无法正常使用。。。后续再解决

新建组件:src\screens\ViewBoard\components\taskModal.tsx

import { useForm } from "antd/lib/form/Form"
import { useTasksModal, useTasksQueryKey } from "../utils"
import { useEditTask } from "utils/task"
import { useEffect } from "react"
import { Form, Input, Modal } from "antd"
import { UserSelect } from "components/user-select"
import { TaskTypeSelect } from "components/task-type-select"const layout = {labelCol: {span: 8},wrapperCol: {span: 16}
}export const TaskModal = () => {const [form] = useForm()const { editingTaskId, editingTask, close } = useTasksModal()const { mutateAsync: editTask, isLoading: editLoading } = useEditTask(useTasksQueryKey())const onCancel = () => {close()form.resetFields()}const onOk = async () => {await editTask({...editingTask, ...form.getFieldsValue()})close()}useEffect(() => {form.setFieldsValue(editingTask)}, [form, editingTask])return <ModalforceRender={true}onCancel={onCancel}onOk={onOk}okText={"确认"}cancelText={"取消"}confirmLoading={editLoading}title={"编辑任务"}open={!!editingTaskId}><Form {...layout} initialValues={editingTask} form={form}><Form.Itemlabel={"任务名"}name={"name"}rules={[{ required: true, message: "请输入任务名" }]}><Input /></Form.Item><Form.Item label={"经办人"} name={"processorId"}><UserSelect defaultOptionName={"经办人"} /></Form.Item><Form.Item label={"类型"} name={"typeId"}><TaskTypeSelect /></Form.Item></Form></Modal>
}

注意:与 Drawer 一样,在Modal 组件中使用通过 useForm() 提取的 form 绑定的 Form 时,需要添加 forceRender 属性,否则在页面打开时绑定不到会有报错,参见:【实战】React 实战项目常见报错 —— Instance created by ‘useForm’ is not connected to any Form element. Forget…

编辑:src\screens\ViewBoard\index.tsx(引入 TaskModal):

...
import { TaskModal } from "./components/taskModal";export const ViewBoard = () => {...return (<ViewContainer>...<TaskModal/></ViewContainer>);
};
...

编辑:src\screens\ViewBoard\components\ViewboardCloumn.tsx(引入 useTasksModal 使得点击 任务卡片 可以打开 TaskModal 进行编辑):

...
import { useTasksModal, useTasksSearchParams } from "../utils";
...export const ViewboardColumn = ({ viewboard }: { viewboard: Viewboard }) => {...const { startEdit } = useTasksModal()return (<Container>...<TasksContainer>{tasks?.map((task) => (<Card onClick={() => startEdit(task.id)} style={{ marginBottom: "0.5rem", cursor: 'pointer' }} key={task.id}>...</Card>))}...</TasksContainer></Container>);
};
...

查看功能和效果,点击 任务卡片 后 TaskModal 出现,编辑并确认后即可看到修改后的任务(用了乐观更新,完全无感):
在这里插入图片描述

8.看板和任务删除功能

接下来先实现一个小功能,搜索结果中关键字高亮

新建 src\screens\ViewBoard\components\mark.tsx

export const Mark = ({name, keyword}: {name: string, keyword: string}) => {if(!keyword) {return <>{name}</>}const arr = name.split(keyword)return <>{arr.map((str, index) => <span key={index}>{str}{index === arr.length -1 ? null : <span style={{ color: '#257AFD' }}>{keyword}</span>}</span>)}</>
}

编辑 src\screens\ViewBoard\components\ViewboardCloumn.tsx(引入 Task 并将 TaskCard 单独提取出来):

...
import { Task } from "types/Task";
import { Mark } from "./mark";...const TaskCard = ({task}: {task: Task}) => {const { startEdit } = useTasksModal();const { name: keyword } = useTasksSearchParams()return <CardonClick={() => startEdit(task.id)}style={{ marginBottom: "0.5rem", cursor: "pointer" }}key={task.id}><p><Mark keyword={keyword} name={task.name}/></p><TaskTypeIcon id={task.id} /></Card>
}export const ViewboardColumn = ({ viewboard }: { viewboard: Viewboard }) => {const { data: allTasks } = useTasks(useTasksSearchParams());const tasks = allTasks?.filter((task) => task.kanbanId === viewboard.id);return (<Container><h3>{viewboard.name}</h3><TasksContainer>{tasks?.map((task) => <TaskCard task={task}/>)}<CreateTask kanbanId={viewboard.id} /></TasksContainer></Container>);
};
...

查看效果:

在这里插入图片描述

下面开始开发删除功能

编辑 src\utils\viewboard.ts(创建并导出 useDeleteViewBoard):

...
export const useDeleteViewBoard = (queryKey: QueryKey) => {const client = useHttp();return useMutation((id?: number) =>client(`kanbans/${id}`, {method: "DELETE",}),useDeleteConfig(queryKey));
};

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

...
import { Button, Card, Dropdown, MenuProps, Modal, Row } from "antd";
import { useDeleteViewBoard } from "utils/viewboard";...export const ViewboardColumn = ({ viewboard }: { viewboard: Viewboard }) => {const { data: allTasks } = useTasks(useTasksSearchParams());const tasks = allTasks?.filter((task) => task.kanbanId === viewboard.id);return (<Container><Row><h3>{viewboard.name}</h3><More viewboard={viewboard}/></Row><TasksContainer>{tasks?.map((task) => <TaskCard task={task}/>)}<CreateTask kanbanId={viewboard.id} /></TasksContainer></Container>);
};const More = ({ viewboard }: { viewboard: Viewboard }) => {const {mutateAsync: deleteViewBoard} = useDeleteViewBoard(useViewBoardQueryKey())const startDelete = () => {Modal.confirm({okText: '确定',cancelText: '取消',title: '确定删除看板吗?',onOk() {deleteViewBoard(viewboard.id)}})}const items: MenuProps["items"] = [{key: 1,label: "删除",onClick: startDelete,},];return <Dropdown menu={{ items }}><Button type="link" onClick={(e) => e.preventDefault()}>...</Button></Dropdown>
}
...

测试一下删除看板,功能正常

下面是删除任务功能

编辑 src\utils\task.ts(创建并导出 useDeleteTask):

...
export const useDeleteTask = (queryKey: QueryKey) => {const client = useHttp();return useMutation((id?: number) =>client(`tasks/${id}`, {method: "DELETE",}),useDeleteConfig(queryKey));
};

编辑 src\screens\ViewBoard\components\taskModal.tsx

...
import { useDeleteTask, useEditTask } from "utils/task";export const TaskModal = () => {...const { mutateAsync: deleteTask } = useDeleteTask(useTasksQueryKey());...const startDelete = () => {close();Modal.confirm({okText: '确定',cancelText: '取消',title: '确定删除看板吗?',onOk() {deleteTask(Number(editingTaskId));}})}return (<Modal {...}><Form {...}>...</Form><div style={{ textAlign: 'right' }}><Button style={{fontSize: '14px'}} size="small" onClick={startDelete}>删除</Button></div></Modal>);
};

测试一下删除任务,功能正常


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


http://www.ppmy.cn/news/1051264.html

相关文章

rk3568 适配以太网(千兆)

rk3568 适配以太网——RTL8211 千兆以太网(Gigabit Ethernet)的传输速度为1 Gbps(千兆位每秒),而百兆以太网(Fast Ethernet)的传输速度为100 Mbps(百兆位每秒)。因此,在相同的网络条件下,千兆网可以提供更高的数据传输速率,比百兆网快10倍。千兆网的更高传输速度使…

线程面试题-1

看的博客里面总结的线程的八股文 1、线程安全的集合有哪些&#xff1f;线程不安全的呢&#xff1f; 线程安全的&#xff1a; Hashtable&#xff1a;比HashMap多了个线程安全。 ConcurrentHashMap:是一种高效但是线程安全的集合。 Vector&#xff1a;比Arraylist多了个同步化…

ClickHouse(二十一):Clickhouse SQL DDL操作-临时表及视图

进入正文前&#xff0c;感谢宝子们订阅专题、点赞、评论、收藏&#xff01;关注IT贫道&#xff0c;获取高质量博客内容&#xff01; &#x1f3e1;个人主页&#xff1a;含各种IT体系技术&#xff0c;IT贫道_Apache Doris,大数据OLAP体系技术栈,Kerberos安全认证-CSDN博客 &…

不负众望~历时4年修炼,这本册子终于成书了(文末赠书)

名字&#xff1a;阿玥的小东东 学习&#xff1a;Python、C/C 主页链接&#xff1a;阿玥的小东东的博客_CSDN博客-python&&c高级知识,过年必备,C/C知识讲解领域博主 目录 精进Spring Boot首选读物 “小册”变“大书”&#xff0c;彻底弄懂Spring Boot 全方位配套资源…

07 mysql5.6.x docker 启动, 无 config 目录导致客户端连接认证需要 10s

前言 呵呵 最近再一次 环境部署的过程中碰到了这样的一个问题 我基于 docker 启动了一个 mysql 服务, 然后 挂载出了 数据目录 和 配置目录, 没有手动复制配置目录出来, 所以配置目录是空的 然后 我基于 docker 启动了一个 nacos, 配置数据库设置为上面的这个 mysql 然后 启…

uniapp 自定义手机顶部状态栏(适配状态栏高度)

开启页面自定义导航栏功能 uniapp 在 pages.json 页面设置了全局的 globalStyle 的 "navigationStyle": "custom" 或单页面的 style 的 "navigationStyle": "custom" 之后页面顶部就没有自带的导航栏了&#xff0c;这时用户可自定义该…

剑指Offer33.二叉搜索树的后序遍历序列 C++

1、题目描述 输入一个整数数组&#xff0c;判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true&#xff0c;否则返回 false。假设输入的数组的任意两个数字都互不相同。 参考以下这颗二叉搜索树&#xff1a; 5 / 2 6 / 1 3 示例 1&#xff1a; 输入: [1,6,3,2,…

Python XML处理中级篇:深入探索lxml库

lxml库是Python中处理XML和HTML文档的强大库&#xff0c;提供了丰富的API以进行各种操作。在初级篇中&#xff0c;我们介绍了如何使用lxml库解析、访问和修改XML文档。在这篇中级篇中&#xff0c;我们将更深入地探讨如何使用lxml库&#xff0c;包括如何创建XML文档&#xff0c;…