【实战】十一、看板页面及任务组页面开发(三) —— 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

...
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>
  );
};

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


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

猜你喜欢

转载自blog.csdn.net/qq_32682301/article/details/132420837