实战项目案例:编写任务管理系统的前端界面

下面编写任务管理系统的前端界面,如图23-2所示。它拥有添加任务、查看任务、设置任务为完成状态以及删除任务的功能。

image 2024 02 20 13 59 26 914
Figure 1. 图23-2 任务管理系统的前端界面

在上一章里已经编写好了对应的 API,因此现在直接使用这些 API 即可。通过以下命令安装 Axios 库(Axios 是一个基于 promise 对象的网络请求库,可以用于浏览器和 Node.js),以便在前端项目中用 Axios 调用前面编写的后端 API。由于 Axios 已经内置 TypeScript 支持,因此无须再安装声明文件库。

$ npm install axios

使用上一节中的 React 项目结构,其中粗体字标出的文件表示相对于上一节新增或修改的文件。

D:\TSProject\client-side
│  ...
│
├─node_modules
│      ...
│
├─public
│      ...
│
└─src
    │  apis.ts
    │  App.css
    │  App.test.tsx
    │  App.tsx
    │  index.css
    │  index.tsx
    │  logo.svg
    │  react-app-env.d.ts
    │  reportWebVitals.ts
    │  setupTests.ts
    │  type.d.ts
    │
    └─components
            TaskCreator.tsx
            TaskItem.tsx

编写任务类型声明及任务管理后端API

src/type.d.ts 文件的内容如下。

interface Task {
    id: number,
    name: string,
    description: string,
    isDone:boolean
}

该文件定义了表示任务的 Task 接口,供其他 TypeScript 文件引用,它包含 idname(名称)、description(描述)、isDone(是否完成)等字段。

src/apis.ts 文件的内容如下。

import axios, { AxiosResponse } from "axios"

const baseUrl: string = "http://localhost:8000"

export const getTaskList = async (): Promise<AxiosResponse<Task[]>> => {
    const tasks: AxiosResponse<Task[]> = await axios.get(
        baseUrl + "/tasks"
    )
    return tasks;
}

export const addTask = async (task: Task): Promise<AxiosResponse<Task>> => {
    const newTask: AxiosResponse<Task> = await axios.post(
        baseUrl + "/task", task
    );
    return newTask;
}

export const deleteTask = async (taskId: number): Promise<AxiosResponse<boolean>> => {
    const res: AxiosResponse<boolean> = await axios.delete(
        baseUrl + "/task/" + taskId
    );
    return res;
}

export const setTaskDone = async (taskId: number): Promise<AxiosResponse<boolean>> => {
    const res: AxiosResponse<boolean> = await axios.put(
        baseUrl + "/task/" + taskId
    );
    return res;
}

该文件提供了访问任务管理后端 API 的功能,使用 Axios 调用前面编写的任务管理后端 API。

下面介绍 src/apis.ts 文件中部分方法的作用。

编写添加任务UI组件及任务列表项UI组件

下面编写添加任务 UI 组件,如图23-3所示。

image 2024 02 20 14 07 47 225
Figure 2. 图23-3 添加任务UI组件

添加任务 UI 组件的文件为 src/components/TaskCreator.tsx,内容如下。

import React, { useState } from 'react'

type Props = {
    addTask: (e: React.FormEvent, formData: Task | any) => void
}

const TaskCreator: React.FC<Props> = ({ addTask }) => {
    const [formData, setFormData] = useState<Task | {}>()

    const handleForm = (e: React.FormEvent<HTMLInputElement>): void => {
        setFormData({
            ...formData,
            [e.currentTarget.id]: e.currentTarget.value,
        })
    }

    return (
        <form className='Form' onSubmit={(e) => addTask(e, formData)}>
            <div>
                <div>
                    <label htmlFor='name'>任务名称</label>
                    <input onChange={handleForm} type='text' id='name' />
                </div>
                <div>
                    <label htmlFor='description'>任务描述</label>
                    <input onChange={handleForm} type='text' id='description' />
                </div>
            </div>
            <button disabled={formData === undefined ? true : false} >添加任务</button>
        </form>
    )
}

export default TaskCreator

该文件声明了一个函数式 UI 组件 TaskCreator,允许通过传入 Props.addTask 接收添加任务的处理函数。

接下来,编写任务列表项 UI 组件,如图23-4所示。

image 2024 02 20 14 09 23 718
Figure 3. 图23-4 任务列表项UI组件

编写任务列表项 UI 组件的文件为 src/components/TaskItem.tsx,内容如下。

import React from 'react'

type Props = {
  task: Task,
  deleteTask: (id: number) => void,
  setTaskDone: (id: number) => void
}

const TaskItem: React.FC<Props> = ({ task, deleteTask, setTaskDone }) => {
  return (
    <div className='Item'>
      <div className='Item--text'>
        <h1 className={task.isDone ? 'done-task' : ""}>{task.name}</h1>
        <span className={task.isDone ? 'done-task' : ""}>{task.description}</span>
      </div>

      <div className='Item--button'>
        <button
          onClick={() => setTaskDone(task.id)}
          className={task.isDone ? `hide-button` : "Item--button__done"}
        >
          完成
        </button>
        <button
          onClick={() => deleteTask(task.id)}
          className='Item--button__delete'
        >
          删除
        </button>
      </div>
    </div>
  )
}

export default TaskItem

该文件声明了一个函数式 UI 组件 TaskItem,允许传入 Props.task 来接收当前任务数据,允许传入 Props.deleteTask 来接收删除任务的处理函数,允许传入 Props.setTaskDone 来接收设置任务为完成状态的处理函数。

TaskItem 组件中,根据任务的完成状态(task.isDone),决定是否显示 “完成” 按钮,以及是否在任务名称和描述上增加删除线。当 task.isDonetrue 时,完成状态的任务列表项 UI 组件如图23-5所示。

image 2024 02 20 14 11 02 734
Figure 4. 图23-5 完成状态的任务列表项UI组件

编写任务管理页面及样式

下面编写任务管理系统的前端界面,并将添加任务 UI 组件及任务列表项 UI 组件组合起来。任务管理页面的文件为 src/App.tsx,内容如下。

import React, { useEffect, useState } from 'react'
import TaskCreator from './components/TaskCreator'
import TaskItem from './components/TaskItem'
import { addTask, getTaskList, deleteTask, setTaskDone } from './apis'

const App: React.FC = () => {
  const [tasks, setTasks] = useState<Task[]>([])

  useEffect(() => {
    getTaskList().then(p => setTasks(p.data));
  }, [])

  const handleAddTask = (e: React.FormEvent, formData: Task): void => {
    addTask(formData).then(p => setTasks([...tasks, p.data]));
  }

  const handleDeleteTask = (id: number): void => {
    deleteTask(id).then(p => {
      let deletedTaskIndex = tasks.findIndex(y => y.id == id);
      let newTasks = [...tasks]
      newTasks.splice(deletedTaskIndex, 1);
      setTasks(newTasks);
    }
    )
  }

  const handleSetTaskDone = (id: number): void => {
    setTaskDone(id).then(p => {
      let doneTaskIndex = tasks.findIndex(y => y.id == id);
      tasks[doneTaskIndex].isDone = true;
      setTasks([...tasks]);
    }
    )
  }

  return (
    <main className='App'>
      <h1>任务管理</h1>
      <TaskCreator addTask={handleAddTask} />
      <div className='Item'>
        <h1>全部任务</h1>
      </div>
      {tasks.map((task: Task) => (
        <TaskItem
          key={task.id}
          task={task}
          deleteTask={handleDeleteTask}
          setTaskDone={handleSetTaskDone}
        />
      ))}
    </main>
  )
}

export default App

接下来分别介绍 src/App.tsx 文件中部分代码的作用。

  • const [tasks, setTasks]:声明 tasks 变量和 setTasks() 函数,将返回一个名为 task 的 State 变量,以及一个更新 State 的函数 setTasks,参数类型为 Task[]。

  • useEffect(…​) 方法:在浏览器中执行的代码,该方法将会调用后端 API 获取全部任务列表数据,并将任务状态设置到 State 中。

  • handleAddTask(…​) 方法:添加任务的方法,该方法将会调用后端 API 添加任务,并将新任务设置到 State 中。

  • handleDeleteTask(…​) 方法:删除任务的方法,该方法将会调用后端 API 删除任务,并将删除此任务后的任务列表的状态设置到 State 中。

  • handleSetTaskDone(…​) 方法:设置任务完成的方法,该方法将会调用后端 API 将任务设置为已完成状态,并将此状态设置到 State 中。

  • return 语句:返回整个页面的元素结构,其中包含添加任务 UI 组件及遍历全部任务后多次渲染的列表项 UI 组件。

接下来,编写整个页面的样式,样式文件为 src/index.css,内容如下。

* {
  margin: 0;
  padding: 0;
}

body {
  color: #fff;
  background: #333;
}

.App {
  max-width: 600px;
  margin: auto;
}

.App>h1 {
  text-align: center;
  margin: 10px;
}

.Item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: #444;
  padding: 10px;
  border-bottom: 1px solid #333333;
}

.Item--text h1 {
  color: #f59609;
}

.Item--button button {
  background: #ffffff;
  padding: 10px;
  border-radius: 20px;
  cursor: pointer;
}

.Item--button__delete {
  border: 1px solid #ce0404;
  color: #ce0404;
}

.Item--button__done {
  border: 1px solid #05b873;
  color: #05b873;
  margin-right: 10px;
}
.hide-button {
  display: none;
}

.done-task {
  text-decoration: line-through;
  color: #777 !important;
}

.Form {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px;
  background: #444;
  margin-bottom: 15px;
}

.Form>div {
  display: flex;
  justify-content: center;
  align-items: center;
}

.Form input {
  background: #ffffff;
  padding: 10px;
  border: 1px solid #f59609;
  border-radius: 10px;
  display: block;
  margin: 5px;
}

.Form button {
  background: #f59609;
  color: #fff;
  padding: 10px;
  border-radius: 20px;
  cursor: pointer;
  border: none;
}

现在就可以在项目目录下(本例中为 D:\TSProject\client-side)执行 npm start 命令,启动前端 UI 应用程序了。注意,在启动 UI 应用程序之前,需要先启动前面创建的后端服务。UI 应用程序启动后,就可以在浏览器地址栏中输入 http://localhost:3000 ,访问任务管理页面,并在任务管理页面中执行相应操作,如图23-6所示。

image 2024 02 20 14 14 55 612
Figure 5. 图23-6 在任务管理页面中执行相应操作