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

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

文章目录

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


学习内容来源: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.看板列表开发准备工作

之前的项目详情进入看板页的路由有个小问题,点击浏览器返回按钮回不去,原因如下:

  • 路由列表是栈结构,每访问一个路由都会 push 一个新路由进去,当点击返回,就会将上一个路由置于栈顶;而进入项目详情页(从'projects''projects/1')默认重定向子路由是看板页(projects/1/viewboard),返回上一个路由时,默认又会重定向到看板页路由。列表栈示例如下:
  • ['projects', 'projects/1', 'projects/1/viewboard']

接下来解决一下这个问题,编辑 src\screens\ProjectDetail\index.tsx (重定向标签新增属性 replace,在重定向时直接替换原路由):

...
export const ProjectDetail = () => {return (<div>...<Routes>...<Route index element={<Navigate to="viewboard" replace/>} /></Routes></div>);
};

为了方便后续类型统一调用,将 src\screens\ProjectList\components\List.tsxinterface Project 提取到 src\types 目录下

视频中 是用 WebStorm ,博主用的是 VSCode:

  • 在需要重构的变量上右击,选择重构(快捷键 Ctrl + Shift + R),选择 Move to a new file,默认同变量名的文件会创建在当前文件所在同一级目录下,其他引用位置也相应改变,涉及引用位置:
    • src\utils\project.ts
    • src\screens\ProjectList\components\SearchPanel.tsx
    • src\screens\ProjectList\components\List.tsx
  • 拖动新生成的文件到 src\types 目录下,可以看到其他引用位置也相应改变
  • 相关功能文档:TypeScript Programming with Visual Studio Code

src\screens\ProjectList\components\SearchPanel.tsxinterface User 也执行同样操作,涉及引用位置:

  • src\screens\ProjectList\components\SearchPanel.tsx
  • src\screens\ProjectList\components\List.tsx
  • src\auth-provider.ts
  • src\context\auth-context.tsx
  • src\utils\use-users.ts

看板页还需要以下两个类型,新建一下:

  • src\types\Viewboard.ts:
export interface Viewboard {id: number;name: string;projectId: number;
}
  • src\types\Task.ts
export interface Task {id: number;name: string;projectId: number;processorId: number; // 经办人taskGroupId: number; // 任务组kanbanId: number;typeId: number;      // bug or tasknote: string;
}

接下来创建数据请求的 hook:

src\utils\viewboard.ts:

import { cleanObject } from "utils";
import { useHttp } from "./http";
import { Viewboard } from "types/Viewboard";
import { useQuery } from "react-query";export const useViewboards = (param?: Partial<Viewboard>) => {const client = useHttp();return useQuery<Viewboard[]>(["viewboards", param], () =>client("kanbans", { data: cleanObject(param || {}) }));
};

src\utils\task.ts:

import { cleanObject } from "utils";
import { useHttp } from "./http";
import { Task } from "types/Task";
import { useQuery } from "react-query";export const useTasks = (param?: Partial<Task>) => {const client = useHttp();return useQuery<Task[]>(["tasks", param], () =>client("tasks", { data: cleanObject(param || {}) }));
};

2.看板列表初步开发

接下来开始开发看板列表,展示需要用到项目数据,可以提取一个从 url 获取 projectId,再用 id 获取项目数据的 hook

新建 src\screens\ViewBoard\utils.ts

import { useLocation } from "react-router"
import { useProject } from "utils/project"export const useProjectIdInUrl = () => {const { pathname } = useLocation()const id = pathname.match(/projects\/(\d+)/)?.[1]return Number(id)
}export const useProjectInUrl = () => useProject(useProjectIdInUrl())export const useViewBoardSearchParams = () => ({projectId: useProjectIdInUrl()})export const useViewBoardQueryKey = () => ['viewboards', useViewBoardSearchParams()]export const useTasksSearchParams = () => ({projectId: useProjectIdInUrl()})export const useTasksQueryKey = () => ['tasks', useTasksSearchParams()]

注意:每一个 useXXXQueryKey 都要确保返回值第一项 与后续列表请求 useXXXuseQuery 的第一个参数保持一致,否则后续增删改都无法正常自动重新请求列表,问题排查比较困难

为看板定制一个展示列组件(任务列表),供每个类型来使用

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

import { Viewboard } from "types/Viewboard";
import { useTasks } from "utils/task";
import { useTasksSearchParams } from "../utils";export const ViewboardColumn = ({viewboard}:{viewboard: Viewboard}) => {const { data: allTasks } = useTasks(useTasksSearchParams())const tasks = allTasks?.filter(task => task.kanbanId === viewboard.id)return <div><h3>{viewboard.name}</h3>{tasks?.map(task => <div key={task.id}>{task.name}</div>)}</div>
}

编辑 src\screens\ViewBoard\index.tsx

import { useDocumentTitle } from "utils";
import { useViewboards } from "utils/viewboard";
import { useProjectInUrl, useViewBoardSearchParams } from "./utils";
import { ViewboardColumn } from "./components/ViewboardCloumn"
import styled from "@emotion/styled";export const ViewBoard = () => {useDocumentTitle('看板列表')const {data: currentProject} = useProjectInUrl()const {data: viewboards, } = useViewboards(useViewBoardSearchParams())return <div><h1>{currentProject?.name}看板</h1><ColumnsContainer>{viewboards?.map(vbd => <ViewboardColumn viewboard={vbd} key={vbd.id}/>)}</ColumnsContainer></div>;
};const ColumnsContainer = styled.div`display: flex;overflow: hidden;margin-right: 2rem;
`

通过代码可知:viewboards.map 后 ViewboardColumn 渲染多次,其中 useTasks 也同时执行多次,但是仔细看浏览器开发者工具可发现,相应请求并没有执行多次,而是只执行了一次,这是因为 react-query 的缓存机制(默认两秒内发送的多个key相同且的参数相同的请求只执行最后一次)

访问看板列表可看到如下内容且三种状态任务横向排列即为正常:

待完成
管理登录界面开发开发中
管理注册界面开发
权限管理界面开发
UI开发
自测已完成
单元测试
性能优化

3.添加task, bug 图标

任务的类型接口并不直接返回,而是只返回一个 typeId,并不能明确标识任务类型,需要单独访问接口来获取具体任务类型

新建 src\types\TaskType.ts

export interface TaskType {id: number;name: string;
}

新建 src\utils\task-type.ts

import { useHttp } from "./http";
import { useQuery } from "react-query";
import { TaskType } from "types/TaskType";export const useTaskTypes = () => {const client = useHttp();return useQuery<TaskType[]>(["taskTypes"], () =>client("tasks"));
};

将以下两个 svg 文件拷贝到 src\assets

bug.svg
在这里插入图片描述

<svg xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xlinkHref="http://www.w3.org/1999/xlink"><!-- Generator: Sketch 3.5.2 (25235) - http://www.bohemiancoding.com/sketch --><title>bug</title><desc>Created with Sketch.</desc><defs/><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"><g id="bug" sketch:type="MSArtboardGroup"><g id="Bug" sketch:type="MSLayerGroup" transform="translate(1.000000, 1.000000)"><rect id="Rectangle-36" fill="#E5493A" sketch:type="MSShapeGroup" x="0" y="0" width="14" height="14" rx="2"/><path d="M10,7 C10,8.657 8.657,10 7,10 C5.343,10 4,8.657 4,7 C4,5.343 5.343,4 7,4 C8.657,4 10,5.343 10,7" id="Fill-2" fill="#FFFFFF" sketch:type="MSShapeGroup"/></g></g></g>
</svg>

task.svg
在这里插入图片描述

<svg xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" width="16px" height="16px" viewBox="0 0 16 16" version="1.1"><!-- Generator: Sketch 3.5.2 (25235) - http://www.bohemiancoding.com/sketch --><title>task</title><desc>Created with Sketch.</desc><defs/><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"><g id="task" sketch:type="MSArtboardGroup"><g id="Task" sketch:type="MSLayerGroup" transform="translate(1.000000, 1.000000)"><rect id="Rectangle-36" fill="#4BADE8" sketch:type="MSShapeGroup" x="0" y="0" width="14" height="14" rx="2"/><g id="Page-1" transform="translate(4.000000, 4.500000)" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" sketch:type="MSShapeGroup"><path d="M2,5 L6,0" id="Stroke-1"/><path d="M2,5 L0,3" id="Stroke-3"/></g></g></g></g>
</svg>

直接使用可能会有如下报错:

Compiled with problems:XERROR in ./src/assets/task.svgModule build failed (from ./node_modules/@svgr/webpack/lib/index.js):
SyntaxError: unknown file: Namespace tags are not supported by default. React's JSX doesn't support namespace tags. You can set `throwIfNamespace: false` to bypass this warning.

skety:type 这种类型的标签属性改成 sketchType 驼峰这样才能被 JSX 接受。

  • 编译有问题: ./src/assets/bug.svg 中的错误-慕课网
  • reactjs - SyntaxError: unknown: Namespace tags are not supported by default - Stack Overflow

svg 文件 修改后的源码如下:

  • bug.svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xlinkHref="http://www.w3.org/1999/xlink" xmlnsSketch="http://www.bohemiancoding.com/sketch/ns"><!-- Generator: Sketch 3.5.2 (25235) - http://www.bohemiancoding.com/sketch --><title>bug</title><desc>Created with Sketch.</desc><defs></defs><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketchType="MSPage"><g id="bug" sketchType="MSArtboardGroup"><g id="Bug" sketchType="MSLayerGroup" transform="translate(1.000000, 1.000000)"><rect id="Rectangle-36" fill="#E5493A" sketchType="MSShapeGroup" x="0" y="0" width="14" height="14" rx="2"></rect><path d="M10,7 C10,8.657 8.657,10 7,10 C5.343,10 4,8.657 4,7 C4,5.343 5.343,4 7,4 C8.657,4 10,5.343 10,7" id="Fill-2" fill="#FFFFFF" sketchType="MSShapeGroup"></path></g></g></g>
</svg>
  • task.svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg"xmlnsSketch="http://www.bohemiancoding.com/sketch/ns"><!-- Generator: Sketch 3.5.2 (25235) - http://www.bohemiancoding.com/sketch --><title>task</title><desc>Created with Sketch.</desc><defs></defs><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketchType="MSPage"><g id="task" sketchType="MSArtboardGroup"><g id="Task" sketchType="MSLayerGroup" transform="translate(1.000000, 1.000000)"><rect id="Rectangle-36" fill="#4BADE8" sketchType="MSShapeGroup" x="0" y="0" width="14" height="14" rx="2"></rect><g id="Page-1" transform="translate(4.000000, 4.500000)" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" sketchType="MSShapeGroup"><path d="M2,5 L6,0" id="Stroke-1"></path><path d="M2,5 L0,3" id="Stroke-3"></path></g></g></g></g>
</svg>

编辑 src\screens\ViewBoard\components\ViewboardCloumn.tsx(引入图标,并美化):

import { Viewboard } from "types/Viewboard";
import { useTasks } from "utils/task";
import { useTasksSearchParams } from "../utils";
import { useTaskTypes } from "utils/task-type";
import taskIcon from "assets/task.svg";
import bugIcon from "assets/bug.svg";
import styled from "@emotion/styled";
import { Card } from "antd";const TaskTypeIcon = ({ id }: { id: number }) => {const { data: taskTypes } = useTaskTypes();const name = taskTypes?.find((taskType) => taskType.id === id)?.name;if (!name) {return null;}return <img alt='task-icon' src={name === "task" ? taskIcon : bugIcon} />;
};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) => (<Card style={{marginBottom: '0.5rem'}} key={task.id}><div>{task.name}</div><TaskTypeIcon id={task.id} /></Card>))}</TasksContainer></Container>);
};export const Container = styled.div`min-width: 27rem;border-radius: 6px;background-color: rgb(244, 245, 247);display: flex;flex-direction: column;padding: .7rem .7rem 1rem;margin-right: 1.5rem;
`const TasksContainer = styled.div`overflow: scroll;flex: 1;::-webkit-scrollbar {display: none;}
`

查看效果:
效果图


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

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

相关文章:

  • 深入源码分析kubernetes informer机制(三)Resync
  • FL Studio 21最新for Windows-21.1.0.3267中文解锁版安装激活教程及更新日志
  • HTML详解连载(4)
  • STM32 LL库+STM32CubeMX--点亮板载LED
  • 【HBZ分享】ES的评分score机制的原理
  • 函数递归专题(案例超详解一篇讲通透)
  • leetcode-413. 等差数列划分(java)
  • 从零开始学习 Java:简单易懂的入门指南之MAth、System(十二)
  • 人工智能原理概述 - ChatGPT 背后的故事
  • 【Linux】以太网协议——数据链路层
  • Neo4j之MATCH基础
  • Python实验代码合集
  • Less和Sass的原理和用法
  • c# List<T>.Aggregate
  • 软件测试常用工具总结(测试管理、单元测试、接口测试、自动化测试、性能测试、负载测试等)
  • Hadoop组件
  • jeecg-boot批量导入问题注意事项
  • Django图书商城系统实战开发 - 实现会员管理
  • Kafka如何解决消息丢失的问题
  • 我只记得512天在CSDN的日子
  • pycharm,VSCode 几个好用的插件
  • springboot 使用zookeeper实现分布式ID
  • git cherry-pick
  • 转行软件测试四个月学习,第一次面试经过分享
  • ECS服务器安装docker
  • 高等数学教材啃书汇总重难点(三)微分中值定理与导数的应用
  • 域名列表是什么?
  • 数据库操作不再困难,MyBatis动态Sql标签解析
  • Android 网络编程-网络请求
  • Mac下全选,使用pynput,怎样调用command键?