文章目录
相对原教程,我在学习开始时(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~6
7.编辑任务功能
接下来新建编辑任务的组件:
先准备好调用编辑任务接口和获取任务详情的 Hook
,编辑 src\utils\task.ts
:
...
import {
useAddConfig, useEditConfig } from "./use-optimistic-options";
export const useEditTask = (queryKey: QueryKey) => {
const client = useHttp();
return useMutation(
(params: Partial<Task>) =>
client(`tasks/${
params.id}`, {
method: "PATCH",
data: params,
}),
useEditConfig(queryKey)
);
};
export const useTask = (id?: number) => {
const client = useHttp();
return useQuery<Task>(["task", id], () => client(`tasks/${
id}`), {
enabled: Boolean(id),
});
};
编辑 src\screens\ViewBoard\utils.ts
(新增 useTasksModal
):
...
// import { useDebounce } from "utils";
import {
useTask } from "utils/task";
...
export const useTasksSearchParams = () => {
const [param] = useUrlQueryParam([
"name",
"typeId",
"processorId",
"tagId",
]);
const projectId = useProjectIdInUrl();
// const debouncedName = useDebounce(param.name)
return useMemo(
() => ({
projectId,
typeId: Number(param.typeId) || undefined,
processorId: Number(param.processorId) || undefined,
tagId: Number(param.tagId) || undefined,
// name: debouncedName,
name: param.name,
}),
// [projectId, param, debouncedName]
[projectId, param]
);
};
...
export const useTasksModal = () => {
const [{
editingTaskId }, setEditingTaskId] = useUrlQueryParam(['editingTaskId'])
const {
data: editingTask, isLoading } = useTask(Number(editingTaskId))
const startEdit = useCallback((id: number) => {
setEditingTaskId({
editingTaskId: id})
}, [setEditingTaskId])
const close = useCallback(() => {
setEditingTaskId({
editingTaskId: ''})
}, [setEditingTaskId])
return {
editingTaskId,
editingTask,
startEdit,
close,
isLoading
}
}
视频中使用
useDebounce
使得完全停止输入后才开始搜索,避免输入过程中频繁搜索造成系统资源浪费,且影响用户体验,博主这样更改后中文输入法无法正常使用。。。后续再解决
新建组件:src\screens\ViewBoard\components\taskModal.tsx
:
import {
useForm } from "antd/lib/form/Form"
import {
useTasksModal, useTasksQueryKey } from "../utils"
import {
useEditTask } from "utils/task"
import {
useEffect } from "react"
import {
Form, Input, Modal } from "antd"
import {
UserSelect } from "components/user-select"
import {
TaskTypeSelect } from "components/task-type-select"
const layout = {
labelCol: {
span: 8},
wrapperCol: {
span: 16}
}
export const TaskModal = () => {
const [form] = useForm()
const {
editingTaskId, editingTask, close } = useTasksModal()
const {
mutateAsync: editTask, isLoading: editLoading } = useEditTask(useTasksQueryKey())
const onCancel = () => {
close()
form.resetFields()
}
const onOk = async () => {
await editTask({
...editingTask, ...form.getFieldsValue()})
close()
}
useEffect(() => {
form.setFieldsValue(editingTask)
}, [form, editingTask])
return <Modal
forceRender={
true}
onCancel={
onCancel}
onOk={
onOk}
okText={
"确认"}
cancelText={
"取消"}
confirmLoading={
editLoading}
title={
"编辑任务"}
open={
!!editingTaskId}
>
<Form {
...layout} initialValues={
editingTask} form={
form}>
<Form.Item
label={
"任务名"}
name={
"name"}
rules={
[{
required: true, message: "请输入任务名" }]}
>
<Input />
</Form.Item>
<Form.Item label={
"经办人"} name={
"processorId"}>
<UserSelect defaultOptionName={
"经办人"} />
</Form.Item>
<Form.Item label={
"类型"} name={
"typeId"}>
<TaskTypeSelect />
</Form.Item>
</Form>
</Modal>
}
注意:与
Drawer
一样,在Modal
组件中使用通过useForm()
提取的form
绑定的Form
时,需要添加forceRender
属性,否则在页面打开时绑定不到会有报错,参见:【实战】React 实战项目常见报错 —— Instance created by ‘useForm’ is not connected to any Form element. Forget…
编辑:src\screens\ViewBoard\index.tsx
(引入 TaskModal
):
...
import {
TaskModal } from "./components/taskModal";
export const ViewBoard = () => {
...
return (
<ViewContainer>
...
<TaskModal/>
</ViewContainer>
);
};
...
编辑:src\screens\ViewBoard\components\ViewboardCloumn.tsx
(引入 useTasksModal
使得点击 任务卡片 可以打开 TaskModal
进行编辑):
...
import {
useTasksModal, useTasksSearchParams } from "../utils";
...
export const ViewboardColumn = ({
viewboard }: {
viewboard: Viewboard }) => {
...
const {
startEdit } = useTasksModal()
return (
<Container>
...
<TasksContainer>
{
tasks?.map((task) => (
<Card onClick={
() => startEdit(task.id)} style={
{
marginBottom: "0.5rem", cursor: 'pointer' }} key={
task.id}>
...
</Card>
))}
...
</TasksContainer>
</Container>
);
};
...
查看功能和效果,点击 任务卡片 后 TaskModal
出现,编辑并确认后即可看到修改后的任务(用了乐观更新,完全无感):
8.看板和任务删除功能
接下来先实现一个小功能,搜索结果中关键字高亮
新建 src\screens\ViewBoard\components\mark.tsx
:
export const Mark = ({
name, keyword}: {
name: string, keyword: string}) => {
if(!keyword) {
return <>{
name}</>
}
const arr = name.split(keyword)
return <>
{
arr.map((str, index) => <span key={
index}>
{
str}
{
index === arr.length -1 ? null : <span style={
{
color: '#257AFD' }}>{
keyword}</span>
}
</span>)
}
</>
}
编辑 src\screens\ViewBoard\components\ViewboardCloumn.tsx
(引入 Task
并将 TaskCard
单独提取出来):
...
import {
Task } from "types/Task";
import {
Mark } from "./mark";
...
const TaskCard = ({
task}: {
task: Task}) => {
const {
startEdit } = useTasksModal();
const {
name: keyword } = useTasksSearchParams()
return <Card
onClick={
() => startEdit(task.id)}
style={
{
marginBottom: "0.5rem", cursor: "pointer" }}
key={
task.id}
>
<p>
<Mark keyword={
keyword} name={
task.name}/>
</p>
<TaskTypeIcon id={
task.id} />
</Card>
}
export const ViewboardColumn = ({
viewboard }: {
viewboard: Viewboard }) => {
const {
data: allTasks } = useTasks(useTasksSearchParams());
const tasks = allTasks?.filter((task) => task.kanbanId === viewboard.id);
return (
<Container>
<h3>{
viewboard.name}</h3>
<TasksContainer>
{
tasks?.map((task) => <TaskCard task={
task}/>)}
<CreateTask kanbanId={
viewboard.id} />
</TasksContainer>
</Container>
);
};
...
查看效果:
下面开始开发删除功能
编辑 src\utils\viewboard.ts
(创建并导出 useDeleteViewBoard
):
...
export const useDeleteViewBoard = (queryKey: QueryKey) => {
const client = useHttp();
return useMutation(
(id?: number) =>
client(`kanbans/${
id}`, {
method: "DELETE",
}),
useDeleteConfig(queryKey)
);
};
编辑 src\screens\ViewBoard\components\ViewboardCloumn.tsx
:
...
import {
Button, Card, Dropdown, MenuProps, Modal, Row } from "antd";
import {
useDeleteViewBoard } from "utils/viewboard";
...
export const ViewboardColumn = ({
viewboard }: {
viewboard: Viewboard }) => {
const {
data: allTasks } = useTasks(useTasksSearchParams());
const tasks = allTasks?.filter((task) => task.kanbanId === viewboard.id);
return (
<Container>
<Row>
<h3>{
viewboard.name}</h3>
<More viewboard={
viewboard}/>
</Row>
<TasksContainer>
{
tasks?.map((task) => <TaskCard task={
task}/>)}
<CreateTask kanbanId={
viewboard.id} />
</TasksContainer>
</Container>
);
};
const More = ({
viewboard }: {
viewboard: Viewboard }) => {
const {
mutateAsync: deleteViewBoard} = useDeleteViewBoard(useViewBoardQueryKey())
const startDelete = () => {
Modal.confirm({
okText: '确定',
cancelText: '取消',
title: '确定删除看板吗?',
onOk() {
deleteViewBoard(viewboard.id)
}
})
}
const items: MenuProps["items"] = [
{
key: 1,
label: "删除",
onClick: startDelete,
},
];
return <Dropdown menu={
{
items }}>
<Button type="link" onClick={
(e) => e.preventDefault()}>
...
</Button>
</Dropdown>
}
...
测试一下删除看板,功能正常
下面是删除任务功能
编辑 src\utils\task.ts
(创建并导出 useDeleteTask
):
...
export const useDeleteTask = (queryKey: QueryKey) => {
const client = useHttp();
return useMutation(
(id?: number) =>
client(`tasks/${
id}`, {
method: "DELETE",
}),
useDeleteConfig(queryKey)
);
};
编辑 src\screens\ViewBoard\components\taskModal.tsx
:
...
import {
useDeleteTask, useEditTask } from "utils/task";
export const TaskModal = () => {
...
const {
mutateAsync: deleteTask } = useDeleteTask(useTasksQueryKey());
...
const startDelete = () => {
close();
Modal.confirm({
okText: '确定',
cancelText: '取消',
title: '确定删除看板吗?',
onOk() {
deleteTask(Number(editingTaskId));
}
})
}
return (
<Modal {
...}>
<Form {
...}>
...
</Form>
<div style={
{
textAlign: 'right' }}>
<Button style={
{
fontSize: '14px'}} size="small" onClick={
startDelete}>删除</Button>
</div>
</Modal>
);
};
测试一下删除任务,功能正常
部分引用笔记还在草稿阶段,敬请期待。。。