【实战】十一、看板页面及任务组页面开发(三) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(二十五)

文章目录


学习内容来源: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、用户认证与异步请求


五、CSS 其实很简单 - 用 CSS-in-JS 添加样式


六、用户体验优化 - 加载中和错误状态处理



七、Hook,路由,与 URL 状态管理



八、用户选择器与项目编辑功能


九、深入React 状态管理与Redux机制





十、用 react-query 获取数据,管理缓存


十一、看板页面及任务组页面开发

1~3

4~6

7.编辑任务功能

接下来新建编辑任务的组件:

先准备好调用编辑任务接口和获取任务详情的 Hook,编辑 src\utils\task.ts

js 复制代码
...
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):

js 复制代码
...
// 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

js 复制代码
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):

js 复制代码
...
import { TaskModal } from "./components/taskModal";

export const ViewBoard = () => {
  ...

  return (
    <ViewContainer>
      ...
      <TaskModal/>
    </ViewContainer>
  );
};
...

编辑:src\screens\ViewBoard\components\ViewboardCloumn.tsx(引入 useTasksModal 使得点击 任务卡片 可以打开 TaskModal 进行编辑):

js 复制代码
...
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

js 复制代码
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 单独提取出来):

js 复制代码
...
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):

js 复制代码
...
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

js 复制代码
...
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):

js 复制代码
...
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

js 复制代码
...
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>
  );
};

测试一下删除任务,功能正常


部分引用笔记还在草稿阶段,敬请期待。。。

相关推荐
AlienZHOU7 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
刺客码10 小时前
Layui 表格固定列行高错位问题解决方案
前端框架·layui·jquery·web
Captaincc10 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师11 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen12 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒12 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow13 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81813 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007813 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明13 小时前
第 6 章 组件进阶
前端·vue.js