如何使用 React 制作日历应用

2025-06-04

如何使用 React 制作日历应用

立即在http://jauyeung.net/subscribe/订阅我的电子邮件列表

在 Twitter 上关注我:https://twitter.com/AuMayeung

更多文章请访问https://medium.com/@hohanga

对于许多应用程序来说,记录日期是一项重要功能。拥有日历通常是一项非常实用的功能。幸运的是,许多开发者已经开发了日历组件,其他开发者可以轻松地将其添加到他们的应用中。

React 有许多日历小部件可以添加到我们的应用中。其中之一就是 React Big Calendar。它功能丰富,包含月历、周历和日历。此外,你还可以使用“后退”和“下一步”按钮轻松导航到今天或任何其他日期。你还可以在日历中拖动日期范围来选择日期范围。这样,你就可以对日期进行任何操作。

在本文中,我们将创建一个简单的日历应用,用户可以拖动日期范围并添加日历条目。用户还可以点击现有日历条目并进行编辑。现有条目也可以删除。用于添加和编辑日历条目的表单将包含日期和时间选择器,用于选择日期和时间。

我们将把后端的数据保存在 JSON 文件中。

我们将使用 React 来构建我们的应用。首先,我们运行:

npx create-react-app calendar-app
Enter fullscreen mode Exit fullscreen mode

创建项目。

接下来我们需要安装一些软件包。我们将使用 Axios 向后端发送 HTTP 请求,使用 Bootstrap 进行样式设置,使用 MobX 进行简单的状态管理,使用 React Big Calendar 作为日历组件,使用 React Datepicker 作为表单中的日期和时间选择器,并使用 React Router 进行路由。

要安装它们,我们运行:

npm i axios bootstrap mobx mobx-react moment react-big-calendar react-bootstrap react-datepicker react-router-dom
Enter fullscreen mode Exit fullscreen mode

安装完所有软件包后,我们就可以开始编写代码了。首先,我们将现有代码替换为App.js

import React from "react";
import { Router, Route } from "react-router-dom";
import HomePage from "./HomePage";
import { createBrowserHistory as createHistory } from "history";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import "./App.css";
import "react-big-calendar/lib/css/react-big-calendar.css";
import "react-datepicker/dist/react-datepicker.css";
const history = createHistory();
function App({ calendarStore }) {
  return (
    <div>
      <Router history={history}>
        <Navbar bg="primary" expand="lg" variant="dark">
          <Navbar.Brand href="#home">Calendar App</Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="mr-auto">
              <Nav.Link href="/">Home</Nav.Link>
            </Nav>
          </Navbar.Collapse>
        </Navbar>
        <Route
          path="/"
          exact
          component={props => (
            <HomePage {...props} calendarStore={calendarStore} />
          )}
        />
      </Router>
    </div>
  );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

我们在这里添加了 React Bootstrap 顶部栏,并添加了指向主页的链接。此外,我们还在这里添加了主页的路由,并calendarStore传入了 MobX。

此外,我们在这里导入日期选择器和日历的样式,以便在整个应用程序中使用它们。

接下来App.css,将现有代码替换为:

.page {
  padding: 20px;
}
.form-control.react-datepicker-ignore-onclickoutside,
.react-datepicker-wrapper {
  width: 465px !important;
}
.react-datepicker__current-month,
.react-datepicker-time__header,
.react-datepicker-year-header,
.react-datepicker__day-name,
.react-datepicker__day,
[class^="react-datepicker__day--*"],
.react-datepicker__time-list-item {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
    "Droid Sans", "Helvetica Neue", sans-serif;
}
Enter fullscreen mode Exit fullscreen mode

在我们的页面上添加一些填充,更改日期选择器输入的宽度并更改日期选择器的字体。

CalendarForm.js接下来在文件夹中创建一个名为的文件src并添加:

import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import DatePicker from "react-datepicker";
import Button from "react-bootstrap/Button";
import {
  addCalendar,
  editCalendar,
  getCalendar,
  deleteCalendar
} from "./requests";
import { observer } from "mobx-react";
const buttonStyle = { marginRight: 10 };
function CalendarForm({ calendarStore, calendarEvent, onCancel, edit }) {
  const [start, setStart] = React.useState(null);
  const [end, setEnd] = React.useState(null);
  const [title, setTitle] = React.useState("");
  const [id, setId] = React.useState(null);
React.useEffect(() => {
    setTitle(calendarEvent.title);
    setStart(calendarEvent.start);
    setEnd(calendarEvent.end);
    setId(calendarEvent.id);
  }, [
    calendarEvent.title,
    calendarEvent.start,
    calendarEvent.end,
    calendarEvent.id
  ]);
const handleSubmit = async ev => {
    ev.preventDefault();
    if (!title || !start || !end) {
      return;
    }
if (+start > +end) {
      alert("Start date must be earlier than end date");
      return;
    }
    const data = { id, title, start, end };
    if (!edit) {
      await addCalendar(data);
    } else {
      await editCalendar(data);
    }
    const response = await getCalendar();
    const evs = response.data.map(d => {
      return {
        ...d,
        start: new Date(d.start),
        end: new Date(d.end)
      };
    });
    calendarStore.setCalendarEvents(evs);
    onCancel();
  };
  const handleStartChange = date => setStart(date);
  const handleEndChange = date => setEnd(date);
  const handleTitleChange = ev => setTitle(ev.target.value);
const deleteCalendarEvent = async () => {
    await deleteCalendar(calendarEvent.id);
    const response = await getCalendar();
    const evs = response.data.map(d => {
      return {
        ...d,
        start: new Date(d.start),
        end: new Date(d.end)
      };
    });
    calendarStore.setCalendarEvents(evs);
    onCancel();
  };
return (
    <Form noValidate onSubmit={handleSubmit}>
      <Form.Row>
        <Form.Group as={Col} md="12" controlId="title">
          <Form.Label>Title</Form.Label>
          <Form.Control
            type="text"
            name="title"
            placeholder="Title"
            value={title || ""}
            onChange={handleTitleChange}
            isInvalid={!title}
          />
          <Form.Control.Feedback type="invalid">{!title}</Form.Control.Feedback>
        </Form.Group>
      </Form.Row>
<Form.Row>
        <Form.Group as={Col} md="12" controlId="start">
          <Form.Label>Start</Form.Label>
          <br />
          <DatePicker
            showTimeSelect
            className="form-control"
            selected={start}
            onChange={handleStartChange}
          />
        </Form.Group>
      </Form.Row>
<Form.Row>
        <Form.Group as={Col} md="12" controlId="end">
          <Form.Label>End</Form.Label>
          <br />
          <DatePicker
            showTimeSelect
            className="form-control"
            selected={end}
            onChange={handleEndChange}
          />
        </Form.Group>
      </Form.Row>
      <Button type="submit" style={buttonStyle}>
        Save
      </Button>
      <Button type="button" style={buttonStyle} onClick={deleteCalendarEvent}>
        Delete
      </Button>
      <Button type="button" onClick={onCancel}>
        Cancel
      </Button>
    </Form>
  );
}
export default observer(CalendarForm);
Enter fullscreen mode Exit fullscreen mode

这是用于添加和编辑日历条目的表单。我们在这里通过添加Form组件来添加 React Bootstrap 表单。它Form.Control也来自同一个库。我们用它来title输入文本。

另外两个字段是开始日期和结束日期。我们在这里使用 React Datepicker 来选择日历条目的开始日期和结束日期。此外,我们还启用了时间选择器,方便用户选择时间。

每个字段都有变更处理程序,用于更新状态中的值,以便用户能够看到自己输入的内容,并允许他们稍后提交数据。变更处理程序包括handleStartChangehandleEndChangehandleTitleChange。我们使用钩子生成的 setter 函数来设置状态useState

我们使用useEffect回调将 prop 中的字段设置calendarEvent为状态。我们将所有要设置的字段作为函数的第二个参数传入数组,这样每当prop 的最新值传入useEffect时,状态就会更新。calendarEvent

handleSubmit在单击表单“保存”按钮时调用的函数中,我们必须调用该函数ev.preventDefault,以便我们可以使用 Ajax 提交表单数据。

如果数据验证通过,那么我们提交数据并获取最新数据并将其存储在我们的calendarStoreMobX 存储中。

我们将其包装在组件observer外面CalendarForm,以便始终从中获取最新值calendarStore

接下来我们创建主页。在文件夹HomePage.js中创建一个文件src并添加:

import React from "react";
import { Calendar, momentLocalizer } from "react-big-calendar";
import moment from "moment";
import Modal from "react-bootstrap/Modal";
import CalendarForm from "./CalendarForm";
import { observer } from "mobx-react";
import { getCalendar } from "./requests";
const localizer = momentLocalizer(moment);
function HomePage({ calendarStore }) {
  const [showAddModal, setShowAddModal] = React.useState(false);
  const [showEditModal, setShowEditModal] = React.useState(false);
  const [calendarEvent, setCalendarEvent] = React.useState({});
  const [initialized, setInitialized] = React.useState(false);
  const hideModals = () => {
    setShowAddModal(false);
    setShowEditModal(false);
  };
  const getCalendarEvents = async () => {
    const response = await getCalendar();
    const evs = response.data.map(d => {
      return {
        ...d,
        start: new Date(d.start),
        end: new Date(d.end)
      };
    });
    calendarStore.setCalendarEvents(evs);
    setInitialized(true);
  };
  const handleSelect = (event, e) => {
    const { start, end } = event;
    const data = { title: "", start, end, allDay: false };
    setShowAddModal(true);
    setShowEditModal(false);
    setCalendarEvent(data);
  };
  const handleSelectEvent = (event, e) => {
    setShowAddModal(false);
    setShowEditModal(true);
    let { id, title, start, end, allDay } = event;
    start = new Date(start);
    end = new Date(end);
    const data = { id, title, start, end, allDay };
    setCalendarEvent(data);
  };
  React.useEffect(() => {
    if (!initialized) {
      getCalendarEvents();
    }
  });
  return (
    <div className="page">
      <Modal show={showAddModal} onHide={hideModals}>
        <Modal.Header closeButton>
          <Modal.Title>Add Calendar Event</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <CalendarForm
            calendarStore={calendarStore}
            calendarEvent={calendarEvent}
            onCancel={hideModals.bind(this)}
            edit={false}
          />
        </Modal.Body>
      </Modal>
      <Modal show={showEditModal} onHide={hideModals}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Calendar Event</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <CalendarForm
            calendarStore={calendarStore}
            calendarEvent={calendarEvent}
            onCancel={hideModals.bind(this)}
            edit={true}
          />
        </Modal.Body>
      </Modal>
      <Calendar
        localizer={localizer}
        events={calendarStore.calendarEvents}
        startAccessor="start"
        endAccessor="end"
        selectable={true}
        style={{ height: "70vh" }}
        onSelectSlot={handleSelect}
        onSelectEvent={handleSelectEvent}
      />
    </div>
  );
}
export default observer(HomePage);
Enter fullscreen mode Exit fullscreen mode

我们获取日历条目并将其填充到此处的日历中。这些条目从后端检索,然后保存到 store 中。在useEffect回调中,我们设置了页面加载时获取条目。我们仅在为initializedfalse 时执行此操作,这样就不会在每次页面渲染时重新加载数据。

为了打开添加日历条目的模式,我们onSelectSlot使用处理程序设置道具,以便我们可以调用setShowAddModalsetCalendarEvent设置打开模式并在打开添加日历事件模式之前设置日期。

类似地,我们onSelectEvent使用handleSelectEvent处理函数设置模式,以便我们打开编辑模式并设置现有条目的日历事件数据。

每个模态框都包含一个CalendarForm组件。我们将关闭模态框的函数传入表单,以便我们可以从表单中关闭它们。此外,我们还传入了calendarStorecalendarEvent,以便在 中进行操作CalendarForm

我们将其包装在组件observer外面CalendarForm,以便始终从中获取最新值calendarStore

接下来index.js,我们用以下内容替换现有代码:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { CalendarStore } from "./store";
const calendarStore = new CalendarStore();
ReactDOM.render(
  <App calendarStore={calendarStore} />,
  document.getElementById("root")
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
Enter fullscreen mode Exit fullscreen mode

这样我们就可以将 MobX 传递calendarStore到根App组件中。

接下来在文件夹中创建一个requests.js文件src并添加:

const APIURL = "http://localhost:3000";
const axios = require("axios");
export const getCalendar = () => axios.get(`${APIURL}/calendar`);
export const addCalendar = data => axios.post(`${APIURL}/calendar`, data);
export const editCalendar = data =>
  axios.put(`${APIURL}/calendar/${data.id}`, data);
export const deleteCalendar = id => axios.delete(`${APIURL}/calendar/${id}`);
Enter fullscreen mode Exit fullscreen mode

这些是用于进行 HTTP 调用来操作日历条目的函数。

接下来在文件夹store.js中创建src并添加:

import { observable, action, decorate } from "mobx";
class CalendarStore {
  calendarEvents = [];
setCalendarEvents(calendarEvents) {
    this.calendarEvents = calendarEvents;
  }
}
CalendarStore = decorate(CalendarStore, {
  calendarEvents: observable,
  setCalendarEvents: action
});
export { CalendarStore };
Enter fullscreen mode Exit fullscreen mode

保存商店中的物品以供我们所有的组件访问。

接下来index.html,将现有代码替换为:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Calendar App</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

添加 Bootstrap CSS 并重命名标题。

现在所有困难的工作都完成了。我们要做的就是使用位于https://github.com/typicode/json-server 的JSON Server NPM 包作为我们的后端。

通过运行以下命令进行安装:

npm i -g json-server
Enter fullscreen mode Exit fullscreen mode

然后通过运行以下命令来运行它:

json-server --watch db.json
Enter fullscreen mode Exit fullscreen mode

在 中db.json,将现有内容替换为:

{  
  "calendar": []  
}
Enter fullscreen mode Exit fullscreen mode

接下来,我们通过在应用程序的项目文件夹中运行我们的应用程序来运行它npm start,当程序要求您在不同的端口中运行时,选择“是”。

文章来源:https://dev.to/aumayeung/how-to-make-a-calendar-app-with-react-2alp
PREV
JavaScript Promises 和异步编程简介 Promises 方法 Async 和 Await 结论
NEXT
JavaScript 生成器简介 同步生成器 异步生成器