文章目录
- 一、项目起航:项目初始化与配置
- 二、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 |
prettier | 2.8.4 |
json-server | 0.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>);
};
...
查看功能和效果,点击 +创建任务
输入框出现,点击输入框以外的地方输入框隐藏,输入新增任务名后回车,即可看到新任务:
部分引用笔记还在草稿阶段,敬请期待。。。