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

news2024/11/18 22:35:17

文章目录

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


学习内容来源: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.添加任务搜索功能

接下来为任务看板添加搜索功能

编辑 src\screens\ViewBoard\utils.ts(新增 useTasksSearchParams 为后续 SearchPanel 中数据联动做准备):

import { useMemo } from "react";
import { useLocation } from "react-router";
import { useProject } from "utils/project";
import { useUrlQueryParam } from "utils/url";

...
export const useTasksSearchParams = () => {
  const [param, setParam] = useUrlQueryParam([
    "name",
    "typeId",
    "processorId",
    "tagId",
  ]);
  const projectId = useProjectIdInUrl();
  return useMemo(
    () => ({
      projectId,
      typeId: Number(param.typeId) || undefined,
      processorId: Number(param.processorId) || undefined,
      tagId: Number(param.tagId) || undefined,
      name: param.name,
    }),
    [projectId, param]
  );
};
...

新建 src\components\task-type-select.tsx(仿照 UserSelect 改造出一个 TaskTypeSelect):

import { useTaskTypes } from "utils/task-type";
import { IdSelect } from "./id-select";

export const TaskTypeSelect = (props: React.ComponentProps<typeof IdSelect>) => {
  const { data: taskTypes } = useTaskTypes();
  return <IdSelect options={taskTypes || []} {...props} />;
};

新建 src\screens\ViewBoard\components\SearchPanel.tsx

import { useSetUrlSearchParam } from "utils/url"
import { useTasksSearchParams } from "../utils"
import { Row } from "components/lib"
import { Button, Input } from "antd"
import { UserSelect } from "components/user-select"
import { TaskTypeSelect } from "components/task-type-select"

export const SearchPanel = () => {
  const searchParams = useTasksSearchParams()
  const setSearchParams = useSetUrlSearchParam()
  const reset = () => {
    setSearchParams({
      typeId: undefined,
      processorId: undefined,
      tagId: undefined,
      name: undefined
    })
  }
  return <Row marginBottom={4} gap={true}>
    <Input style={{width: '20rem'}} placeholder='任务名' value={searchParams.name}
      onChange={e => setSearchParams({name: e.target.value})}/>
    <UserSelect defaultOptionName="经办人" value={searchParams.processorId}
      onChange={val => setSearchParams({processorId: val})}/>
    <TaskTypeSelect defaultOptionName="类型" value={searchParams.typeId}
      onChange={val => setSearchParams({typeId: val})}/>
    <Button onClick={reset}>清除筛选器</Button>
  </Row>
}

编辑 src\screens\ViewBoard\index.tsx(引入 SearchPanel):

...
import { SearchPanel } from "./components/SearchPanel";

export const ViewBoard = () => {
  ...
  return (
    <div>
      <h1>{currentProject?.name}看板</h1>
      <SearchPanel/>
      <ColumnsContainer>...</ColumnsContainer>
    </div>
  );
};
...

查看功能和效果:
效果图

5.优化看板样式

功能实现一部分了,接下来优化样式

编辑 src\components\lib.tsx(新增 ViewContainer 处理内边距):

export const ViewContainer = styled.div`
  padding: 3.2rem;
  width: 100%;
  display: flex;
  flex-direction: column;
`

编辑 src\authenticated-app.tsx(调整 Main 样式,垂直占满):

...
const Main = styled.main`
  display: flex;
  /* overflow: hidden; */
`;

编辑 src\screens\ViewBoard\index.tsx(应用 ViewContainer ,增加 Loading 调整 ColumnsContainer 样式并暴露出来,使其触底):

...
import { useProjectInUrl, useTasksSearchParams, useViewBoardSearchParams } from "./utils";
...
import { ViewContainer } from "components/lib";
import { useTasks } from "utils/task";
import { Spin } from "antd";

export const ViewBoard = () => {
  ...
  const { data: viewboards, isLoading: viewBoardIsLoading } = useViewboards(useViewBoardSearchParams());
  const { isLoading: taskIsLoading } = useTasks(useTasksSearchParams())
  const isLoading = taskIsLoading || viewBoardIsLoading
  return (
    <ViewContainer>
      <h1>{currentProject?.name}看板</h1>
      <SearchPanel />
      {
        isLoading ? <Spin/> : <ColumnsContainer>
        ...
        </ColumnsContainer>
      }
    </ViewContainer>
  );
};

const ColumnsContainer = styled.div`
  display: flex;
  overflow-x: scroll;
  flex: 1;
`;

编辑 src\screens\ProjectDetail\index.tsx(引入 Menu 并调整整个组件样式,Menu 高亮状态从路由中获取):

import { Link, Navigate } from "react-router-dom";
import { Route, Routes, useLocation } from "react-router";
import { TaskGroup } from "screens/TaskGroup";
import { ViewBoard } from "screens/ViewBoard";
import styled from "@emotion/styled";
import { Menu } from "antd";

const useRouteType = () => {
  const pathEnd = useLocation().pathname.split('/')
  return pathEnd[pathEnd.length - 1]
}

export const ProjectDetail = () => {
  const routeType = useRouteType()

  return (
    <Container>
      <Aside>
        <Menu mode="inline" selectedKeys={[routeType]}>
          <Menu.Item key='viewboard'>
            <Link to="viewboard">看板</Link>
          </Menu.Item>
          <Menu.Item key='taskgroup'>
            <Link to="taskgroup">任务组</Link>
          </Menu.Item>
        </Menu>
      </Aside>
      <Main>
        <Routes>
          <Route path="/viewboard" element={<ViewBoard />} />
          <Route path="/taskgroup" element={<TaskGroup />} />
          <Route index element={<Navigate to="viewboard" replace />} />
        </Routes>
      </Main>
    </Container>
  );
};

const Aside = styled.aside`
  background-color: rgb(244, 245, 247);
  display: flex;
`

const Main = styled.div`
  display: flex;
  box-shadow: -5px 0 5px -5px rgbs(0, 0, 0, 0.1);
  overflow: hidden;
`

const Container = styled.div`
  display: grid;
  grid-template-columns: 16rem 1fr;
  width: 100%;
`

查看功能和效果:
效果图

6.创建看板与任务

接下来新建创建看板的组件:

先准备好调用新增看板接口的 Hook,编辑 src\utils\viewboard.ts

...
export const useAddViewboard = (queryKey: QueryKey) => {
  const client = useHttp();
  return useMutation(
    (params: Partial<Viewboard>) =>
      client(`kanbans`, {
        method: "POST",
        data: params,
      }),
    useAddConfig(queryKey)
  );
};

新建组件:src\screens\ViewBoard\components\CreateViewboard.tsx

import { useState } from "react"
import { useProjectIdInUrl, useViewBoardQueryKey } from "../utils"
import { useAddViewboard } from "utils/viewboard"
import { Input } from "antd"
import { Container } from "./ViewboardCloumn"

export const CreateViewBoard = () => {
  const [name, setName] = useState('')
  const projectId = useProjectIdInUrl()
  const { mutateAsync: addViewBoard } = useAddViewboard(useViewBoardQueryKey())

  const submit = async () => {
    await addViewBoard({name, projectId})
    setName('')
  }

  return <Container>
    <Input
      size="large"
      placeholder="新建看板名称"
      onPressEnter={submit}
      value={name}
      onChange={evt => setName(evt.target.value)}
    />
  </Container>
}

编辑:src\screens\ViewBoard\index.tsx(引入 CreateViewBoard):

...
import { CreateViewBoard } from "./components/CreateViewboard";

export const ViewBoard = () => {
  ...
  return (
    <ViewContainer>
      ...
      {
        isLoading ? <Spin/> : <ColumnsContainer>
          {viewboards?.map((vbd) => (
            <ViewboardColumn viewboard={vbd} key={vbd.id} />
          ))}
          <CreateViewBoard/>
        </ColumnsContainer>
      }
    </ViewContainer>
  );
};
...

查看功能和效果,输入新增看板名后回车,即可看到新看板:
效果图

接下来新建创建任务的组件:

先准备好调用新增任务接口的 Hook,编辑 src\utils\task.ts

...
import { QueryKey, useMutation, useQuery } from "react-query";
import { useAddConfig } from "./use-optimistic-options";

...
export const useAddTask = (queryKey: QueryKey) => {
  const client = useHttp();
  return useMutation(
    (params: Partial<Task>) =>
      client(`tasks`, {
        method: "POST",
        data: params,
      }),
    useAddConfig(queryKey)
  );
};

新建组件:src\screens\ViewBoard\components\CreateTask.tsx

import { useEffect, useState } from "react";
import { useProjectIdInUrl, useTasksQueryKey } from "../utils";
import { Card, Input } from "antd";
import { useAddTask } from "utils/task";

export const CreateTask = ({kanbanId}: {kanbanId: number}) => {
  const [name, setName] = useState("");
  const { mutateAsync: addTask } = useAddTask(useTasksQueryKey());
  const projectId = useProjectIdInUrl();
  const [inputMode, setInputMode] = useState(false)

  const submit = async () => {
    await addTask({ name, projectId, kanbanId });
    setName("");
    setInputMode(false)
  };

  const toggle = () => setInputMode(mode => !mode)

  useEffect(() => {
    if (!inputMode) {
      setName('')
    }
  }, [inputMode])
  
  if (!inputMode) {
    return <div onClick={toggle}>+创建任务</div>
  }

  return (
    <Card>
      <Input
        onBlur={toggle}
        placeholder="需要做些什么"
        autoFocus={true}
        onPressEnter={submit}
        value={name}
        onChange={(evt) => setName(evt.target.value)}
      />
    </Card>
  );
};

编辑:src\screens\ViewBoard\components\ViewboardCloumn.tsx(引入 CreateTask):

...
import { CreateTask } from "./CreateTask";

...
export const ViewboardColumn = ({ viewboard }: { viewboard: Viewboard }) => {
  ...
  return (
    <Container>
      <h3>{viewboard.name}</h3>
      <TasksContainer>
        ...
        <CreateTask kanbanId={viewboard.id}/>
      </TasksContainer>
    </Container>
  );
};
...

查看功能和效果,点击 +创建任务 输入框出现,点击输入框以外的地方输入框隐藏,输入新增任务名后回车,即可看到新任务:
效果图


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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Scratch 游戏 之 随机大地图生成教程

在很多生存 / 沙盒类游戏中&#xff0c;地图往往是随机生成的&#xff0c;例如&#xff1a;饥荒、我的世界等。那我们该如何在scratch中实现这一点呢&#xff1f; 在scratch中有两种办法可以实现——画笔和克隆体。我们这次先聊克隆体。 我们可以先将克隆体设置为方形的&#x…

快解析内网穿透便捷访问内网私有云

快解析内网穿透软件的首要优势在于其不改变企业现有IT架构的特点。传统的内网穿透解决方案常常需要对企业网络进行重构&#xff0c;这不仅增加了工作量&#xff0c;还可能带来不稳定的因素。而快解析则巧妙地绕过了这一问题&#xff0c;让您能够在保持原有网络设备和配置的前提…

【Unity每日一记】Physics.Raycast 相关_Unity中的“X光射线”

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…

spss---如何使用信度分析以及案例分析

信度分析 问卷调查法是教育研究中广泛采用的一种调查方法&#xff0c;根据调查目的设计的调查问卷是问卷调查法获取信息的工具&#xff0c;其质量高低对调查结果的真实性、适用性等具有决定性的作用。 为了保证问卷具有较高的可靠性和有效性&#xff0c;在形成正式问卷之 前&…

Python基础知识:类的属性查找教程

目录标题 前言正文尾语 前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 正文 有需要python源码/安装包/教程/电子书/资料等 点击此处跳转文末名片免费获取 先从对象自己的名称空间找&#xff0c;没有则取类里找&#xff0c;如果类里也没有则程序报错 class Student1:# …

JS中对象数组深拷贝方法

structuredClone() JavaScript 中提供了一个原生 API 来执行对象的深拷贝&#xff1a;structuredClone。它可以通过结构化克隆算法创建一个给定值的深拷贝&#xff0c;并且还可以传输原始值的可转移对象。 当对象中存在循环引用时&#xff0c;仍然可以通过 structuredClone()…

【Hibench 】完成 HDP-Spark 性能测试

&#x1f341; 博主 "开着拖拉机回家"带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——&#x1f390;开着拖拉机回家_Linux,Java基础学习,大数据运维-CSDN博客 &#x1f390;✨&#x1f341; &#x1fa81;&#x1f341; 希望本文能够给您带来一定的…

0基础学习VR全景平台篇 第87篇:智慧眼-公告有什么作用?

一、功能说明 公告&#xff0c;即政府、团体对有关事件或者行动发布的通告。公告内容由管理员在后台添加&#xff0c;智慧眼成员在场景中添加热点时可以选择引用此公告&#xff0c;引用后会在热点详情页中展示。 二、后台编辑界面 点击【新增】&#xff0c;填写公告的标题和…

[Raspberry Pi]如何用VNC遠端控制樹莓派(Ubuntu desktop 23.04)?

之前曾利用VMware探索CentOS&#xff0c;熟悉Linux操作系統的指令和配置運作方式&#xff0c;後來在樹莓派價格飛漲的時期&#xff0c;遇到貴人贈送Raspberry Pi 4 model B / 8GB&#xff0c;這下工具到位了&#xff0c;索性跳過樹莓派官方系統(Raspberry Pi OS)&#xff0c;直…

牛客OJ题 打印日期

⭐️ 题目描述 &#x1f31f; OJ链接&#xff1a;https://www.nowcoder.com/practice/b1f7a77416194fd3abd63737cdfcf82b?tpId69&&tqId29669&rp1&ru/activity/oj&qru/ta/hust-kaoyan/question-ranking 思路&#xff1a; 默认从一月的天数开始&#xff0c…

一键批量修改文件夹名称,中文瞬间变日语,轻松搞定重命名

大家好&#xff01;现在为了更好地适应全球化发展&#xff0c;许多人都有了海外交流、旅行、学习的需求。但是难免遇到一个问题&#xff1a;在电脑中的中文文件夹名称如何快速翻译成日语&#xff1f; 首先&#xff0c;第一步&#xff0c;我们需要打开文件批量改名&#xff0c;…

编译老版本c++程序 报错 msvcrt.dll 以及 0x000000 内存 不能为 “read“ 问题 已解决

一般 win10 编译 xp对应老版本软件 调试采用 虚拟机形式进行测试&#xff0c;但是虚拟机中&#xff0c;无独立显卡&#xff0c;运行程序提示有&#xff0c;无法调用动态库&#xff0c;或者 内存无法读取&#xff0c;炸一看以为 winxp32位 内存识别只能3.7G.其实是显存无法使用…

【C++】STL---list

STL---list 一、list 的介绍二、list 的模拟实现1. list 节点类2. list 迭代器类&#xff08;1&#xff09;前置&#xff08;2&#xff09;后置&#xff08;3&#xff09;前置- -、后置- -&#xff08;4&#xff09;! 和 运算符重载&#xff08;5&#xff09;* 解引用重载 和 …

七夕特辑——3D爱心(可监听鼠标移动)

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 「推荐专栏」&#xff1a; ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄&#xff0c;vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄&#xff…

第13章——FreeRTOS队列

1.队列简介 队列是任务到任务、任务到中断、中断到任务数据交流的一种机制&#xff08;消息传递&#xff09; FreeRTOS基于队列&#xff0c; 实现了多种功能&#xff0c;其中包括队列集、互斥信号量、计数型信号量、二值信号量、 递归互斥信号量&#xff0c;因此很有必要深入了…

软考A计划-系统集成项目管理工程师-标准规范

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列 &#x1f449;关于作者 专注于Android/Unity和各种游…

python多线程真是让人受够了

一、有8种不同的方法 三、参考文献 https://superfastpython.com/multiprocessing-pool-issue-tasks

linux-进程

1.先谈硬件 冯诺依曼体系结构 一个计算机能够正常运行&#xff0c;就必须遵守冯诺依曼体系 数据流向 为什么不把Cpu直接怼到输入设备和输出设备中间&#xff0c;非要加个内存呢&#xff1f; 答&#xff1a;因为根据木桶原理&#xff0c;如果这样设计&#xff0c;导致最终效…

赴印设厂获得的份额减少,富士康后悔莫及,中国制造获苹果认可

随着iPhone的量产在推进&#xff0c;产业链人士指出iPhone15的分配份额已基本确定&#xff0c;富士康获得了58%的份额&#xff0c;中国大陆的纬创获得28%的份额&#xff0c;而纬创只获得了1%的份额&#xff0c;显示出富士康和纬创这两家企业听从苹果的要求赴印设厂反而被抛弃。…

如何优雅地处理Java多线程编程中的共享资源问题,以确保线程安全和高性能?

文章目录 &#x1f389;欢迎来到Java面试技巧专栏~探索Java中的静态变量与实例变量 ☆* o(≧▽≦)o *☆嗨~我是IT陈寒&#x1f379;✨博客主页&#xff1a;IT陈寒的博客&#x1f388;该系列文章专栏&#xff1a;Java面试技巧文章作者技术和水平有限&#xff0c;如果文中出现错误…