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

文章目录

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


学习内容来源: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.添加任务搜索功能

接下来为任务看板添加搜索功能

编辑 src\screens\ViewBoard\utils.ts(新增 useTasksSearchParams 为后续 SearchPanel 中数据联动做准备):

import { useMemo } from "react";
import { useLocation } from "react-router";
import { useProject } from "utils/project";
import { useUrlQueryParam } from "utils/url";...
export const useTasksSearchParams = () => {const [param, setParam] = useUrlQueryParam(["name","typeId","processorId","tagId",]);const projectId = useProjectIdInUrl();return useMemo(() => ({projectId,typeId: Number(param.typeId) || undefined,processorId: Number(param.processorId) || undefined,tagId: Number(param.tagId) || undefined,name: param.name,}),[projectId, param]);
};
...

新建 src\components\task-type-select.tsx(仿照 UserSelect 改造出一个 TaskTypeSelect):

import { useTaskTypes } from "utils/task-type";
import { IdSelect } from "./id-select";export const TaskTypeSelect = (props: React.ComponentProps<typeof IdSelect>) => {const { data: taskTypes } = useTaskTypes();return <IdSelect options={taskTypes || []} {...props} />;
};

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

import { useSetUrlSearchParam } from "utils/url"
import { useTasksSearchParams } from "../utils"
import { Row } from "components/lib"
import { Button, Input } from "antd"
import { UserSelect } from "components/user-select"
import { TaskTypeSelect } from "components/task-type-select"export const SearchPanel = () => {const searchParams = useTasksSearchParams()const setSearchParams = useSetUrlSearchParam()const reset = () => {setSearchParams({typeId: undefined,processorId: undefined,tagId: undefined,name: undefined})}return <Row marginBottom={4} gap={true}><Input style={{width: '20rem'}} placeholder='任务名' value={searchParams.name}onChange={e => setSearchParams({name: e.target.value})}/><UserSelect defaultOptionName="经办人" value={searchParams.processorId}onChange={val => setSearchParams({processorId: val})}/><TaskTypeSelect defaultOptionName="类型" value={searchParams.typeId}onChange={val => setSearchParams({typeId: val})}/><Button onClick={reset}>清除筛选器</Button></Row>
}

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

...
import { SearchPanel } from "./components/SearchPanel";export const ViewBoard = () => {...return (<div><h1>{currentProject?.name}看板</h1><SearchPanel/><ColumnsContainer>...</ColumnsContainer></div>);
};
...

查看功能和效果:
效果图

5.优化看板样式

功能实现一部分了,接下来优化样式

编辑 src\components\lib.tsx(新增 ViewContainer 处理内边距):

export const ViewContainer = styled.div`padding: 3.2rem;width: 100%;display: flex;flex-direction: column;
`

编辑 src\authenticated-app.tsx(调整 Main 样式,垂直占满):

...
const Main = styled.main`display: flex;/* overflow: hidden; */
`;

编辑 src\screens\ViewBoard\index.tsx(应用 ViewContainer ,增加 Loading 调整 ColumnsContainer 样式并暴露出来,使其触底):

...
import { useProjectInUrl, useTasksSearchParams, useViewBoardSearchParams } from "./utils";
...
import { ViewContainer } from "components/lib";
import { useTasks } from "utils/task";
import { Spin } from "antd";export const ViewBoard = () => {...const { data: viewboards, isLoading: viewBoardIsLoading } = useViewboards(useViewBoardSearchParams());const { isLoading: taskIsLoading } = useTasks(useTasksSearchParams())const isLoading = taskIsLoading || viewBoardIsLoadingreturn (<ViewContainer><h1>{currentProject?.name}看板</h1><SearchPanel />{isLoading ? <Spin/> : <ColumnsContainer>...</ColumnsContainer>}</ViewContainer>);
};const ColumnsContainer = styled.div`display: flex;overflow-x: scroll;flex: 1;
`;

编辑 src\screens\ProjectDetail\index.tsx(引入 Menu 并调整整个组件样式,Menu 高亮状态从路由中获取):

import { Link, Navigate } from "react-router-dom";
import { Route, Routes, useLocation } from "react-router";
import { TaskGroup } from "screens/TaskGroup";
import { ViewBoard } from "screens/ViewBoard";
import styled from "@emotion/styled";
import { Menu } from "antd";const useRouteType = () => {const pathEnd = useLocation().pathname.split('/')return pathEnd[pathEnd.length - 1]
}export const ProjectDetail = () => {const routeType = useRouteType()return (<Container><Aside><Menu mode="inline" selectedKeys={[routeType]}><Menu.Item key='viewboard'><Link to="viewboard">看板</Link></Menu.Item><Menu.Item key='taskgroup'><Link to="taskgroup">任务组</Link></Menu.Item></Menu></Aside><Main><Routes><Route path="/viewboard" element={<ViewBoard />} /><Route path="/taskgroup" element={<TaskGroup />} /><Route index element={<Navigate to="viewboard" replace />} /></Routes></Main></Container>);
};const Aside = styled.aside`background-color: rgb(244, 245, 247);display: flex;
`const Main = styled.div`display: flex;box-shadow: -5px 0 5px -5px rgbs(0, 0, 0, 0.1);overflow: hidden;
`const Container = styled.div`display: grid;grid-template-columns: 16rem 1fr;width: 100%;
`

查看功能和效果:
效果图

6.创建看板与任务

接下来新建创建看板的组件:

先准备好调用新增看板接口的 Hook,编辑 src\utils\viewboard.ts

...
export const useAddViewboard = (queryKey: QueryKey) => {const client = useHttp();return useMutation((params: Partial<Viewboard>) =>client(`kanbans`, {method: "POST",data: params,}),useAddConfig(queryKey));
};

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

import { useState } from "react"
import { useProjectIdInUrl, useViewBoardQueryKey } from "../utils"
import { useAddViewboard } from "utils/viewboard"
import { Input } from "antd"
import { Container } from "./ViewboardCloumn"export const CreateViewBoard = () => {const [name, setName] = useState('')const projectId = useProjectIdInUrl()const { mutateAsync: addViewBoard } = useAddViewboard(useViewBoardQueryKey())const submit = async () => {await addViewBoard({name, projectId})setName('')}return <Container><Inputsize="large"placeholder="新建看板名称"onPressEnter={submit}value={name}onChange={evt => setName(evt.target.value)}/></Container>
}

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

...
import { CreateViewBoard } from "./components/CreateViewboard";export const ViewBoard = () => {...return (<ViewContainer>...{isLoading ? <Spin/> : <ColumnsContainer>{viewboards?.map((vbd) => (<ViewboardColumn viewboard={vbd} key={vbd.id} />))}<CreateViewBoard/></ColumnsContainer>}</ViewContainer>);
};
...

查看功能和效果,输入新增看板名后回车,即可看到新看板:
效果图

接下来新建创建任务的组件:

先准备好调用新增任务接口的 Hook,编辑 src\utils\task.ts

...
import { QueryKey, useMutation, useQuery } from "react-query";
import { useAddConfig } from "./use-optimistic-options";...
export const useAddTask = (queryKey: QueryKey) => {const client = useHttp();return useMutation((params: Partial<Task>) =>client(`tasks`, {method: "POST",data: params,}),useAddConfig(queryKey));
};

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

import { useEffect, useState } from "react";
import { useProjectIdInUrl, useTasksQueryKey } from "../utils";
import { Card, Input } from "antd";
import { useAddTask } from "utils/task";export const CreateTask = ({kanbanId}: {kanbanId: number}) => {const [name, setName] = useState("");const { mutateAsync: addTask } = useAddTask(useTasksQueryKey());const projectId = useProjectIdInUrl();const [inputMode, setInputMode] = useState(false)const submit = async () => {await addTask({ name, projectId, kanbanId });setName("");setInputMode(false)};const toggle = () => setInputMode(mode => !mode)useEffect(() => {if (!inputMode) {setName('')}}, [inputMode])if (!inputMode) {return <div onClick={toggle}>+创建任务</div>}return (<Card><InputonBlur={toggle}placeholder="需要做些什么"autoFocus={true}onPressEnter={submit}value={name}onChange={(evt) => setName(evt.target.value)}/></Card>);
};

编辑:src\screens\ViewBoard\components\ViewboardCloumn.tsx(引入 CreateTask):

...
import { CreateTask } from "./CreateTask";...
export const ViewboardColumn = ({ viewboard }: { viewboard: Viewboard }) => {...return (<Container><h3>{viewboard.name}</h3><TasksContainer>...<CreateTask kanbanId={viewboard.id}/></TasksContainer></Container>);
};
...

查看功能和效果,点击 +创建任务 输入框出现,点击输入框以外的地方输入框隐藏,输入新增任务名后回车,即可看到新任务:
效果图


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

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

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

相关文章

文心一言 VS 讯飞星火 VS chatgpt (76)-- 算法导论7.3 1题

一、如果用go语言&#xff0c;为什么我们分析随机化算法的期望运行时间&#xff0c;而不是其最坏运行时间呢? 文心一言&#xff1a; 在分析算法的性能时&#xff0c;期望运行时间是一个重要的指标&#xff0c;它描述了算法在平均情况下的表现。期望运行时间考虑了所有可能的…

musl libc ldso 动态加载研究笔记:02

前言 本篇继续研究 musl libc ldso 的动态加载过程中遇到的关键性的概念&#xff1a;到底要加载ELF 文件的哪些内容到 内存 当前如果遇到 ELF 动态加载&#xff0c;当前系统需要有【文件系统】&#xff0c;并且有较大的内存&#xff0c;因为 ELF 文件是无法直接运行的&#xf…

IDEA两种方法修改生成的jar包名字

方法一&#xff1a; 直接修改pom文件中的如下部分 <artifactId>excelreport</artifactId> <version>0.0.1-SNAPSHOT</version> <name>excelreport</name> <description>excelreport</description> 修改完成后&#xff0c;点…

SpringBoot3集成Kafka

标签&#xff1a;Kafka3.Kafka-eagle3&#xff1b; 一、简介 Kafka是一个开源的分布式事件流平台&#xff0c;常被用于高性能数据管道、流分析、数据集成和关键任务应用&#xff0c;基于Zookeeper协调的处理平台&#xff0c;也是一种消息系统&#xff0c;具有更好的吞吐量、内…

跟着美团学设计模式(感处)

读了着篇文章之后发现真的是&#xff0c;你的思想&#xff0c;你的思维是真的比比你拥有什么技术要强的。 注 开闭原则 开闭原则&#xff08;Open-Closed Principle&#xff09;是面向对象设计中的基本原则之一&#xff0c;它的定义是&#xff1a;一个软件实体应该对扩展开放…

python爬虫7:实战1

python爬虫7&#xff1a;实战1 前言 ​ python实现网络爬虫非常简单&#xff0c;只需要掌握一定的基础知识和一定的库使用技巧即可。本系列目标旨在梳理相关知识点&#xff0c;方便以后复习。 申明 ​ 本系列所涉及的代码仅用于个人研究与讨论&#xff0c;并不会对网站产生不好…

carla中lka实现(二)

前言&#xff1a; 首先计算之前检测出来的车道线的中线与输入图像的中线进行计算距离&#xff0c;&#xff0c;并设置不同的阈值对于不同的方向进行相关的调整。 一、车辆中心线 一般而言将摄像头架设在车辆的正中心轴上&#xff0c;所获得的图像的中间线极为车辆的中心。 …

QGraphicsView 实例3地图浏览器

主要介绍Graphics View框架&#xff0c;实现地图的浏览、放大、缩小&#xff0c;以及显示各个位置的视图、场景和地图坐标 效果图: mapwidget.h #ifndef MAPWIDGET_H #define MAPWIDGET_H #include <QLabel> #include <QMouseEvent> #include <QGraphicsView&…

WSL2 ubuntu子系统OpenCV调用本机摄像头的RTSP视频流做开发测试

文章目录 前言一、Ubuntu安装opencv库二、启动 Windows 本机的 RTSP 视频流下载解压 EasyDarwin查看本机摄像头设备开始推流 三、在ubuntu 终端编写代码创建目录及文件创建CMakeLists.txt文件启动 cmake 配置并构建 四、结果展示启动图形界面在图形界面打开终端找到 rtsp_demo运…

linux系统服务学习(二)linux下yum源配置实战

文章目录 Linux下yum源配置实战一、Linux下软件包的管理1、软件安装方式2、源码安装的配置过程3、详解源码安装的配置过程&#xff08;定制&#xff09;4、详解编译过程5、安装过程6、axel多线程下载软件源码安装7、使用软链接解决command not found8、使用环境变量解决command…

软考A计划-系统集成项目管理工程师-收尾管理

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列 &#x1f449;关于作者 专注于Android/Unity和各种游…

中间件(二)dubbo负载均衡介绍

一、负载均衡概述 支持轮询、随机、一致性hash和最小活跃数等。 1、轮询 ① sequences&#xff1a;内部的序列计数器 ② 服务器接口方法权重一样&#xff1a;&#xff08;sequences1&#xff09;%服务器的数量&#xff08;决定调用&#xff09;哪个服务器的服务。 ③ 服务器…

opencv直方图与模板匹配

import cv2 #opencv读取的格式是BGR import numpy as np import matplotlib.pyplot as plt#Matplotlib是RGB %matplotlib inline def cv_show(img,name):cv2.imshow(name,img)cv2.waitKey()cv2.destroyAllWindows() 直方图 cv2.calcHist(images,channels,mask,histSize,ran…

Spring中Bean的生命周期以及Bean的单例与多例模式

一. Bean的生命周期 bean的生命周期可以表达为&#xff1a;bean的定义➡bean的初始化➡bean的使用➡bean的销毁 Bean的初始化过程 1&#xff09;通过XML、Java annotation&#xff08;注解&#xff09;以及Java Configuration&#xff08;配置类&#xff09; 等方式加载Bea…

2023+HuggingGPT: Solving AI Tasks with ChatGPT and itsFriends in Hugging Face

摘要&#xff1a; 语言是llm(例如ChatGPT)连接众多AI模型(例如hugs Face)的接口&#xff0c;用于解决复杂的AI任务。在这个概念中&#xff0c;llms作为一个控制器&#xff0c;管理和组织专家模型的合作。LLM首先根据用户请求规划任务列表&#xff0c;然后为每个任务分配专家模…

Unity 鼠标实现对物体的移动、缩放、旋转

文章目录 1. 代码2. 测试场景 1. 代码 using UnityEngine;public class ObjectManipulation : MonoBehaviour {// 缩放比例限制public float MinScale 0.2f;public float MaxScale 3.0f;// 缩放速率private float scaleRate 1f;// 新尺寸private float newScale;// 射线pri…

【Windows系统编程】03.远线程注入ShellCode

shellcode&#xff1a;本质上也是一段普通的代码&#xff0c;只不过特殊的编程手法&#xff0c;可以在任意环境下&#xff0c;不依赖于原有的依赖库执行。 远程线程 #include <iostream> #include <windows.h> #include <TlHelp32.h>int main(){HANDLE hPr…

Educational Codeforces Round 153 (Rated for Div. 2)ABC

Educational Codeforces Round 153 (Rated for Div. 2) 目录 A. Not a Substring题目大意思路核心代码 B. Fancy Coins题目大意思想核心代码 C. Game on Permutation题目大意思想核心代码 A. Not a Substring 题目大意 给定一个只包含“&#xff08;”和“&#xff09;”这两…

react-native-webview RN和html双向通信

rn登录后得到的token需要传递给网页&#xff0c;js获取到的浏览器信息需要传递给rn RN Index.js: import React from react import { WebView } from react-native-webview import useList from ./useListexport default function Index(props) {const { uri, jsCode, webVie…

iPhone删除的照片能恢复吗?不小心误删了照片怎么找回?

iPhone最近删除清空了照片还能恢复吗&#xff1f;大家都知道&#xff0c;照片对于我们来说是承载着美好回忆的一种形式。它记录着我们的平淡生活&#xff0c;也留住了我们的美好瞬间&#xff0c;具有极其重要的纪念价值。 照片不小心误删是一件非常难受的事&#xff0c;那么iP…