学习内容来源: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 应用:实现项目列表
- 三、 TS 应用:JS神助攻 - 强类型
- 四、 JWT、用户认证与异步请求(上)
- 四、 JWT、用户认证与异步请求(下)
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(上)
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(下)
- 六、用户体验优化 - 加载中和错误状态处理(上)
- 六、用户体验优化 - 加载中和错误状态处理(中)
- 六、用户体验优化 - 加载中和错误状态处理(下)
- 七、Hook,路由,与 URL 状态管理(上)
- 七、Hook,路由,与 URL 状态管理(中)
- 七、Hook,路由,与 URL 状态管理(下)
- 八、用户选择器与项目编辑功能(上)
- 八、用户选择器与项目编辑功能(下)
- 九、深入React 状态管理与Redux机制(一)
- 九、深入React 状态管理与Redux机制(二)
- 九、深入React 状态管理与Redux机制(三)
- 九、深入React 状态管理与Redux机制(四)
- 九、深入React 状态管理与Redux机制(五)
- 十、用 react-query 获取数据,管理缓存(上)
- 十、用 react-query 获取数据,管理缓存(下)
- 十一、看板页面及任务组页面开发(一)
- 十一、看板页面及任务组页面开发(二)
- 十一、看板页面及任务组页面开发(三)
- 十一、看板页面及任务组页面开发(四)
新建 src\utils\reorder.ts
(这部分视频没有详细讲。。。之后项目中如果用到可以深挖一下):
/**
* 在本地对排序进行乐观更新
* @param fromId 要排序的项目的id
* @param type 'before' | 'after'
* @param referenceId 参照id
* @param list 要排序的列表, 比如tasks, kanbans
*/
export const reorder = ({
fromId,
type,
referenceId,
list,
}: {
list: { id: number }[];
fromId: number;
type: "after" | "before";
referenceId: number;
}) => {
const copiedList = [...list];
// 找到fromId对应项目的下标
const movingItemIndex = copiedList.findIndex((item) => item.id === fromId);
if (!referenceId) {
return insertAfter([...copiedList], movingItemIndex, copiedList.length - 1);
}
const targetIndex = copiedList.findIndex((item) => item.id === referenceId);
const insert = type === "after" ? insertAfter : insertBefore;
return insert([...copiedList], movingItemIndex, targetIndex);
};
/**
* 在list中,把from放在to的前边
* @param list
* @param from
* @param to
*/
const insertBefore = (list: unknown[], from: number, to: number) => {
const toItem = list[to];
const removedItem = list.splice(from, 1)[0];
const toIndex = list.indexOf(toItem);
list.splice(toIndex, 0, removedItem);
return list;
};
/**
* 在list中,把from放在to的后面
* @param list
* @param from
* @param to
*/
const insertAfter = (list: unknown[], from: number, to: number) => {
const toItem = list[to];
const removedItem = list.splice(from, 1)[0];
const toIndex = list.indexOf(toItem);
list.splice(toIndex + 1, 0, removedItem);
return list;
};
编辑 src\utils\use-optimistic-options.ts
(在之前写的配置中调用,完成乐观更新):
...
export const useReorderViewboardConfig = (queryKey: QueryKey) =>
useConfig(queryKey, (target, old) => reorder({ list: old, ...target }));
export const useReorderTaskConfig = (queryKey: QueryKey) =>
useConfig(queryKey, (target, old) => {
const orderedList = reorder({ list: old, ...target }) as Task[];
return orderedList.map((item) =>
item.id === target.fromId
? { ...item, kanbanId: target.toKanbanId }
: item
);
});
由于 task 的排序有可能是跨 面板 的,因此会复杂一些
查看效果,发现在拖拽到其他面板后,若是原面板为空,拖不回去了。。。因此需要为 DropChild
加一个最小高度
编辑 src\screens\ViewBoard\components\ViewboardCloumn.tsx
:
...
export const ViewboardColumn = React.forwardRef<...>((...) => {
...
return (
<Container {...props} ref={ref}>
...
<TasksContainer>
<Drop {...}>
<DropChild style={{minHeight: '5px'}}>
...
</DropChild>
</Drop>
<CreateTask kanbanId={viewboard.id} />
</TasksContainer>
</Container>
);
});
...
至此拖拽大功告成!
看板页面开发完,接下来是任务组页面
新建 src\types\TaskGroup.ts
:
export interface TaskGroup {
id: number;
name: string;
projectId: number;
// 开始时间
start: number;
// 结束时间
end: number;
}
新建 src\utils\taskGroup.ts
(与看板 Viewboard
(kanban
) 类似,可以复制后修改):
import { cleanObject } from "utils";
import { useHttp } from "./http";
import { TaskGroup } from "types/TaskGroup";
import { QueryKey, useMutation, useQuery } from "react-query";
import {
useAddConfig,
useDeleteConfig,
} from "./use-optimistic-options";
export const useTaskGroups = (param?: Partial<TaskGroup>) => {
const client = useHttp();
return useQuery<TaskGroup[]>(["taskgroups", param], () =>
client("epics", { data: cleanObject(param || {}) })
);
};
export const useAddTaskGroup = (queryKey: QueryKey) => {
const client = useHttp();
return useMutation(
(params: Partial<TaskGroup>) =>
client(`epics`, {
method: "POST",
data: params,
}),
useAddConfig(queryKey)
);
};
export const useDeleteTaskGroup = (queryKey: QueryKey) => {
const client = useHttp();
return useMutation(
(id?: number) =>
client(`epics/${id}`, {
method: "DELETE",
}),
useDeleteConfig(queryKey)
);
};
新建 src\screens\TaskGroup\utils.ts
:
import { useProjectIdInUrl } from "screens/ViewBoard/utils";
export const useTaskGroupSearchParams = () => ({
projectId: useProjectIdInUrl(),
});
export const useTaskGroupsQueryKey = () => [
"taskgroups",
useTaskGroupSearchParams(),
];
修改 src\types\Task.ts
(属性字段需要和实际数据一致。。。):
export interface Task {
id: number;
name: string;
projectId: number;
processorId: number; // 经办人
epicId: number; // 任务组(原 taskGroupId)
kanbanId: number;
typeId: number; // bug or task
note: string;
}
编辑 src\screens\TaskGroup\index.tsx
(之前新建路由时创建过,页面布局有一部分与看板相同,可以拿过来 src\screens\ViewBoard\index.tsx
):
import { Row, ViewContainer } from "components/lib";
import { useProjectInUrl } from "screens/ViewBoard/utils";
import { useTaskGroups } from "utils/taskGroup";
import { useTaskGroupSearchParams, useTaskGroupsQueryKey } from "./utils";
import { Button, List, Modal } from "antd";
import dayjs from "dayjs";
import { useTasks } from "utils/task";
import { Link } from "react-router-dom";
import { TaskGroup } from "types/TaskGroup";
import { useState } from "react";
export const TaskGroupIndex = () => {
const { data: currentProject } = useProjectInUrl();
const { data: taskGroups } = useTaskGroups(useTaskGroupSearchParams());
const { data: tasks } = useTasks({ projectId: currentProject?.id });
return (
<ViewContainer>
<Row between={true}>
<h1>{currentProject?.name}任务组</h1>
<Button onClick={() => setEpicCreateOpen(true)} type={"link"}>
创建任务组
</Button>
</Row>
<List
style={{ overflow: "scroll" }}
dataSource={taskGroups}
itemLayout={"vertical"}
renderItem={(taskGroup) => (
<List.Item>
<List.Item.Meta
title={
<Row between={true}>
<span>{taskGroup.name}</span>
<Button onClick={() => {}} type={"link"}>
删除
</Button>
</Row>
}
description={
<div>
<div>开始时间:{dayjs(taskGroup.start).format("YYYY-MM-DD")}</div>
<div>结束时间:{dayjs(taskGroup.end).format("YYYY-MM-DD")}</div>
</div>
}
/>
<div>
{tasks
?.filter((task) => task.epicId === taskGroup.id)
.map((task) => (
<Link
to={`/projects/${currentProject?.id}/viewboard?editingTaskId=${task.id}`}
key={task.id}
>
{task.name}
</Link>
))}
</div>
</List.Item>
)}
/>
</ViewContainer>
);
};
查看页面效果,点击对应任务会跳转到看板并打开任务编辑窗口
编辑 src\screens\TaskGroup\index.tsx
(新增删除任务组功能):
...
import { useDeleteTaskGroup, useTaskGroups } from "utils/taskGroup";
export const TaskGroupIndex = () => {
...
const { mutate: deleteTaskGroup } = useDeleteTaskGroup(useTaskGroupsQueryKey());
const confirmDeleteEpic = (taskGroup: TaskGroup) => {
Modal.confirm({
title: `确定删除项目组:${taskGroup.name}`,
content: "点击确定删除",
okText: "确定",
onOk() {
deleteTaskGroup(taskGroup.id);
},
});
};
return (
<ViewContainer>
<Row between={true}>...</Row>
<List
style={{ overflow: "scroll" }}
dataSource={taskGroups}
itemLayout={"vertical"}
renderItem={(taskGroup) => (
<List.Item>
<List.Item.Meta
title={
<Row between={true}>
<span>{taskGroup.name}</span>
<Button onClick={() => confirmDeleteEpic(taskGroup)} type={"link"}
>
删除
</Button>
</Row>
}
description={...}
/>
<div>...</div>
</List.Item>
)}
/>
</ViewContainer>
);
};
查看页面,可以正常删除任务组(建议功能尝试放到完成创建功能之后再尝试,你懂的。。。)
部分引用笔记还在草稿阶段,敬请期待。。。