文章目录
-
- 一、项目起航:项目初始化与配置
- [二、React 与 Hook 应用:实现项目列表](#二、React 与 Hook 应用:实现项目列表)
- [三、TS 应用:JS神助攻 - 强类型](#三、TS 应用:JS神助攻 - 强类型)
- 四、JWT、用户认证与异步请求
- [五、CSS 其实很简单 - 用 CSS-in-JS 添加样式](#五、CSS 其实很简单 - 用 CSS-in-JS 添加样式)
- [六、用户体验优化 - 加载中和错误状态处理](#六、用户体验优化 - 加载中和错误状态处理)
- [七、Hook,路由,与 URL 状态管理](#七、Hook,路由,与 URL 状态管理)
- 八、用户选择器与项目编辑功能
- [九、深入React 状态管理与Redux机制](#九、深入React 状态管理与Redux机制)
- [十、用 react-query 获取数据,管理缓存](#十、用 react-query 获取数据,管理缓存)
- 十一、看板页面及任务组页面开发
相对原教程,我在学习开始时(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 应用:实现项目列表
三、TS 应用:JS神助攻 - 强类型
四、JWT、用户认证与异步请求
五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
六、用户体验优化 - 加载中和错误状态处理
七、Hook,路由,与 URL 状态管理
八、用户选择器与项目编辑功能
九、深入React 状态管理与Redux机制
十、用 react-query 获取数据,管理缓存
十一、看板页面及任务组页面开发
1~3
4.添加任务搜索功能
接下来为任务看板添加搜索功能
编辑 src\screens\ViewBoard\utils.ts
(新增 useTasksSearchParams
为后续 SearchPanel
中数据联动做准备):
js
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
):
js
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
:
js
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
):
js
...
import { SearchPanel } from "./components/SearchPanel";
export const ViewBoard = () => {
...
return (
<div>
<h1>{currentProject?.name}看板</h1>
<SearchPanel/>
<ColumnsContainer>...</ColumnsContainer>
</div>
);
};
...
查看功能和效果:
5.优化看板样式
功能实现一部分了,接下来优化样式
编辑 src\components\lib.tsx
(新增 ViewContainer
处理内边距):
js
export const ViewContainer = styled.div`
padding: 3.2rem;
width: 100%;
display: flex;
flex-direction: column;
`
编辑 src\authenticated-app.tsx
(调整 Main
样式,垂直占满):
js
...
const Main = styled.main`
display: flex;
/* overflow: hidden; */
`;
编辑 src\screens\ViewBoard\index.tsx
(应用 ViewContainer
,增加 Loading
调整 ColumnsContainer
样式并暴露出来,使其触底):
js
...
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 || viewBoardIsLoading
return (
<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
高亮状态从路由中获取):
js
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
:
js
...
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
:
js
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>
<Input
size="large"
placeholder="新建看板名称"
onPressEnter={submit}
value={name}
onChange={evt => setName(evt.target.value)}
/>
</Container>
}
编辑:src\screens\ViewBoard\index.tsx
(引入 CreateViewBoard
):
js
...
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
:
js
...
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
:
js
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>
<Input
onBlur={toggle}
placeholder="需要做些什么"
autoFocus={true}
onPressEnter={submit}
value={name}
onChange={(evt) => setName(evt.target.value)}
/>
</Card>
);
};
编辑:src\screens\ViewBoard\components\ViewboardCloumn.tsx
(引入 CreateTask
):
js
...
import { CreateTask } from "./CreateTask";
...
export const ViewboardColumn = ({ viewboard }: { viewboard: Viewboard }) => {
...
return (
<Container>
<h3>{viewboard.name}</h3>
<TasksContainer>
...
<CreateTask kanbanId={viewboard.id}/>
</TasksContainer>
</Container>
);
};
...
查看功能和效果,点击 +创建任务
输入框出现,点击输入框以外的地方输入框隐藏,输入新增任务名后回车,即可看到新任务:
部分引用笔记还在草稿阶段,敬请期待。。。