文章目錄
- 一、項目起航:項目初始化與配置
- 二、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>);
};
...
查看功能和效果,點擊 +創建任務
輸入框出現,點擊輸入框以外的地方輸入框隱藏,輸入新增任務名后回車,即可看到新任務:
部分引用筆記還在草稿階段,敬請期待。。。