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


学习内容来源: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
prettier2.8.4
json-server0.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.编辑任务功能

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

先准备好调用编辑任务接口和获取任务详情的 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>
  );
};

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


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/84595.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

GB28181国标平台测试软件NTV-GBC(包含服务器和模拟客户端)

GB28181国标平台测试软件NTV-GBC用于对GB28181国标平台进行测试(测试用例需要服务器软件&#xff0c;服务器软件可以是任何标准的国标平台&#xff0c;我们测试使用的是NTV-GBS&#xff09;&#xff0c;软件实现了设备注册、注销、目录查询&#xff0c;消息订阅、INVITE&#x…

剑指offer(C++)-JZ64:求1+2+3+...+n(算法-位运算)

作者&#xff1a;翟天保Steven 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 题目描述&#xff1a; 求123...n&#xff0c;要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句&…

STL---list

目录 1. list的介绍及使用 1.1 list的介绍 1.2 list的使用注意事项 2.list接口介绍及模拟实现 2.1构造​编辑 2.2容量 2.3修改 3.list迭代器 4.迭代器失效 5.模拟实现 6.vector和list的区别 1. list的介绍及使用 1.1 list的介绍 list的文档介绍 1. list是可以在常…

回归预测 | MATLAB实现BES-LSSVM秃鹰搜索算法优化最小二乘支持向量机多输入单输出回归预测(多指标,多图)

回归预测 | MATLAB实现BES-LSSVM秃鹰搜索算法优化最小二乘支持向量机多输入单输出回归预测&#xff08;多指标&#xff0c;多图&#xff09; 目录 回归预测 | MATLAB实现BES-LSSVM秃鹰搜索算法优化最小二乘支持向量机多输入单输出回归预测&#xff08;多指标&#xff0c;多图&a…

探索智能文字识别:技术、应用与发展前景

探索智能文字识别&#xff1a;技术、应用与发展前景 前言一张图全览大赛作品解读随心记你不对我对小结 智能文字识别体系化解读图像预处理文字定位和分割文字区域识别图像校正字体识别和匹配结果后处理小结 如何应对复杂场景下挑战复杂场景应对方法小结 人才时代对人才要求合合…

Facebook AI mBART:巴别塔的硅解

2018年&#xff0c;谷歌发布了BERT&#xff08;来自transformers的双向编码器表示&#xff09;&#xff0c;这是一种预训练的语言模型&#xff0c;在一系列自然语言处理&#xff08;NLP&#xff09;任务中对SOTA结果进行评分&#xff0c;并彻底改变了研究领域。类似的基于变压器…

Linux 上 离线部署GeoScene Server Py3 运行时环境

默认安装ArcGIS Pro的时候&#xff0c;会自动部署上Python3环境&#xff0c;所以在windows上不需要考虑这个问题&#xff0c;但是linux默认并不部署Py3&#xff0c;因此需要单独部署&#xff0c;具体部署可以参考Linux 上 ArcGIS Server 的 Python 3 运行时—ArcGIS Server | A…

亚马逊买家怎么留评

亚马逊买家可以按照以下步骤在购买后留下产品评价&#xff1a; 1、登录亚马逊账户&#xff1a;首先&#xff0c;在网页浏览器中打开亚马逊网站&#xff0c;登录你的亚马逊账户。 2、找到订单&#xff1a;在页面上找到并点击你购买过的商品的"我的订单"或"订单…

手机自动无人直播,实景无人直播真的有用吗?

继数字人直播之后&#xff0c;手机自动直播开始火热了起来&#xff0c;因为其门槛低&#xff0c;成本低&#xff0c;一部手机一个账号就可以实现直播&#xff0c;一时深受广大商家的好评。那么&#xff0c;手机自动无人直播究竟是如何实现自动直播的呢&#xff1f; 在传统的直…

Dockerfile快速搭建自己专属的LAMP环境

目录 编写Dockerfile 1.文件内容需求&#xff1a; 2.值得注意的是centos6官方源已下线&#xff0c;所以需要切换centos-vault源&#xff01; 3.Dockerfile内容 4.进入到 lamp 开始构建镜像 推送镜像到私有仓库 1.创建用户并添加到私有仓库&#xff1a;​编辑​编辑 2.推…

万宾科技22款产品入选《城市生命线安全工程监测技术产品名录》

2023年8月17日-18日&#xff0c;由北京市地下管线协会主办的2023首届城市生命线安全与发展大会在北京召开&#xff0c;本次大会汇聚中央及地方政府主管领导、院士专家、行业领袖、龙头代表、产业精英等。 大会聚焦安全监管智慧平台和燃气爆炸、城市内涝、地下管线交互风险、第三…

【云原生,k8s】Helm应用包管理器介绍

目录 一、为什么需要Helm&#xff1f; &#xff08;一&#xff09;Helm介绍 &#xff08;二&#xff09;Helm有3个重要概念&#xff1a; &#xff08;三&#xff09;Helm特点 二、Helm V3变化 &#xff08;一&#xff09;架构变化 &#xff08;二&#xff09;自动创建名…

R语言处理缺失数据(1)-mice

#清空 rm(listls()) gc()###生成模拟数据### #生成100个随机数 library(magrittr) set.seed(1) asd<-rnorm(100, mean 60, sd 10) %>% round #平均60&#xff0c;标准差10 #将10个数随机替换为NA NA_positions <- sample(1:100, 10) asd[NA_positions] <- NA #转…

SpringBoot + MyBatisPlus中乐观锁的实现 (精简demo)

乐观锁加注解Version后不需要手动进行加1操作。乐观锁是一种用于解决并发冲突的机制&#xff0c;在数据库中用于保护数据的一致性。Version注解是MyBatisPlus框架中的乐观锁注解&#xff0c;它会在更新数据时自动检查版本号是否一致&#xff0c;如果一致则进行更新操作&#xf…

ClickHouse(二十一):Clickhouse SQL DDL操作-临时表及视图

进入正文前&#xff0c;感谢宝子们订阅专题、点赞、评论、收藏&#xff01;关注IT贫道&#xff0c;获取高质量博客内容&#xff01; &#x1f3e1;个人主页&#xff1a;含各种IT体系技术&#xff0c;IT贫道_Apache Doris,大数据OLAP体系技术栈,Kerberos安全认证-CSDN博客 &…

Python功能制作之简单的3D特效

需要导入的库&#xff1a; pygame: 这是一个游戏开发库&#xff0c;用于创建多媒体应用程序&#xff0c;提供了处理图形、声音和输入的功能。 from pygame.locals import *: 导入pygame库中的常量和函数&#xff0c;用于处理事件和输入。 OpenGL.GL: 这是OpenGL的Python绑定…

奥威BI数据可视化工具:360度呈现数据,告别枯燥表格

随着企业数据量的不断增加&#xff0c;如何有效地进行数据分析与决策变得越来越重要。奥威BI数据可视化工具作为一款强大的数据分析工具&#xff0c;在帮助企业深入挖掘数据价值方面具有显著优势。 奥威BI数据可视化工具是一款基于数据仓库技术的数据分析工具&#xff0c;具有…

07-微信小程序-注册页面-模块化

07-微信小程序-注册页面 文章目录 注册页面使用 Page 构造器注册页面参数Object初始数据案例代码 生命周期回调函数组件事件处理函数setData()案例代码 生命周期模块化 注册页面 对于小程序中的每个页面&#xff0c;都需要在页面对应的 js 文件中进行注册&#xff0c;指定页面…

Leetcode-每日一题【剑指 Offer 34. 二叉树中和为某一值的路径】

题目 给你二叉树的根节点 root 和一个整数目标和 targetSum &#xff0c;找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 叶子节点 是指没有子节点的节点。 示例 1&#xff1a; 输入&#xff1a;root [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum 22输…

Newsprk Newspaper新闻报纸WordPress主题

Newsprk Newspaper新闻报纸WordPress主题对于任何使用 WordPress 技术构建的新闻和杂志网站来说都是一个有吸引力且时尚的主题。Newsprk – 报纸 WordPress 主题非常适合任何新闻/杂志或与以下类别匹配的任何特定业务&#xff0c;如博客、体育、时尚、科学、足球、政治、视频、…