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

文章目录

    • 一、项目起航:项目初始化与配置
    • 二、React 与 Hook 应用:实现项目列表
    • 三、TS 应用:JS神助攻 - 强类型
    • 四、JWT、用户认证与异步请求
    • 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
    • 六、用户体验优化 - 加载中和错误状态处理
    • 七、Hook,路由,与 URL 状态管理
    • 八、用户选择器与项目编辑功能
    • 九、深入React 状态管理与Redux机制
    • 十、用 react-query 获取数据,管理缓存
    • 十一、看板页面及任务组页面开发
      • 1~3
      • 4~6
      • 7&8
      • 9&10
      • 11.排序乐观更新
      • 12.任务组页面 (上)


学习内容来源: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~6

  • 十一、看板页面及任务组页面开发(二)

7&8

  • 十一、看板页面及任务组页面开发(三)

9&10

  • 十一、看板页面及任务组页面开发(四)

11.排序乐观更新

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

至此拖拽大功告成!

12.任务组页面 (上)

看板页面开发完,接下来是任务组页面

新建 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>
  );
};

查看页面,可以正常删除任务组(建议功能尝试放到完成创建功能之后再尝试,你懂的。。。)


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

你可能感兴趣的:(react.js,前端,前端框架)