四、案例:学习日志

郁子大约 22 分钟约 6646 字笔记React18李立超

(一)创建项目

1. public 目录

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>学习日志</title>
  </head>

  <body>
    <div id="root"></div>
  </body>
</html>

2. src/index.js

// 引入ReactDOM
import ReactDOM from "react-dom/client";
// 引入样式表
import "./index.css";

// 创建一个JSX
const App = (
  <div className="logs">
    {/* 日志项容器 */}
    <div className="item">
      {/* 日期的容器 */}
      <div className="date">
        <div className="month">四月</div>
        <div className="day">19</div>
      </div>
      {/* 日志内容的容器 */}
      <div className="content">
        <h2 className="desc">学习React</h2>
        <div className="time">40分钟</div>
      </div>
    </div>
  </div>
);

// 获取根容器
const root = ReactDOM.createRoot(document.getElementById("root"));
// 将App渲染进根容器
root.render(App);

3. src/index.css

* {
  box-sizing: border-box;
}
body {
  background-color: #dfdfdf;
  margin: 0;
}
.logs {
  width: 800px;
  margin: 50px auto;
  padding: 20px;
  background-color: #eae2b7;
  border-radius: 10px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.item {
  display: flex;
  margin: 16px 0;
  padding: 6px;
  background-color: #fcbf49;
  border-radius: 10px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.date {
  width: 90px;
  background-color: #ffffff;
  border-radius: 10px;
  font-weight: bold;
  text-align: center;
  overflow: hidden;
}
.month {
  height: 30px;
  line-height: 30px;
  font-size: 20px;
  color: #ffffff;
  background-color: #d62828;
}
.day {
  height: 60px;
  line-height: 60px;
  font-size: 50px;
  font-weight: bold;
}
.content {
  flex: auto;
  text-align: center;
}
.desc {
  font-size: 16px;
  color: #194b49;
}
.time {
  color: #d62828;
}

(二)函数式组件

  • React 中组件有两种创建方式

1)函数式组件

  • 函数组件就是一个返回 JSX 的普通函数
  • 组件的首字母必须大写

2)类式组件

1. src/index.js

// 引入ReactDOM
import ReactDOM from "react-dom/client";
// 引入样式表
import "./index.css";
// 引入App组件
import App from "./App";

// 获取根容器
const root = ReactDOM.createRoot(document.getElementById("root"));
// 将App渲染进根容器
root.render(<App />);

2. src/App.js

export default () => {
  return (
    <div className="logs">
      {/* 日志项容器 */}
      <div className="item">
        {/* 日期的容器 */}
        <div className="date">
          <div className="month">四月</div>
          <div className="day">19</div>
        </div>
        {/* 日志内容的容器 */}
        <div className="content">
          <h2 className="desc">学习React</h2>
          <div className="time">40分钟</div>
        </div>
      </div>
    </div>
  );
};

(三)类式组件

  • React 中组件有两种创建方式

1)函数式组件

2)类式组件(麻烦)

  • 类式组件必须继承自 React.Component
  • 必须添加一个 render() ,且返回值是一个 JSX

1. src/index.js

// 引入ReactDOM
import ReactDOM from "react-dom/client";
// 引入样式表
import "./index.css";
// 引入App组件
import App from "./App";

// 获取根容器
const root = ReactDOM.createRoot(document.getElementById("root"));
// 将App渲染进根容器
root.render(<App />);

2. src/App.js

import React from "react";
export default class App extends React.Component {
  render() {
    return (
      <div className="logs">
        {/* 日志项容器 */}
        <div className="item">
          {/* 日期的容器 */}
          <div className="date">
            <div className="month">四月</div>
            <div className="day">19</div>
          </div>
          {/* 日志内容的容器 */}
          <div className="content">
            <h2 className="desc">学习React</h2>
            <div className="time">40分钟</div>
          </div>
        </div>
      </div>
    );
  }
}

(四)使用自定义组件

1. src/App.js

import Logs from "./components/Logs/Logs";

function App() {
  return (
    <div>
      <Logs />
    </div>
  );
}

export default App;

2. src/components/Logs/Logs.jsx

// 日志的容器
import React from "react";
import LogItem from "./LogItem/LogItem";
import "./Logs.css";

export default function Logs() {
  return (
    <div className="logs">
      <LogItem />
    </div>
  );
}

3. src/components/Logs/Logs.css

.logs {
  width: 800px;
  margin: 50px auto;
  padding: 20px;
  background-color: #eae2b7;
  border-radius: 10px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}

4. src/components/Logs/LogItem/LogItem.jsx

import React from "react";
import MyDate from "./MyDate/MyDate";
import "./LogItem.css";

export default function LogItem() {
  return (
    <div>
      {/* 日志项容器 */}
      <div className="item">
        {/* 日期的容器 */}
        <div className="date">
          <div className="month">四月</div>
          <div className="day">19</div>
        </div>
        {/* 日志内容的容器 */}
        <MyDate />
      </div>
    </div>
  );
}

5. src/components/Logs/LogItem/LogItem.css

.item {
  display: flex;
  margin: 16px 0;
  padding: 6px;
  background-color: #fcbf49;
  border-radius: 10px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.desc {
  font-size: 16px;
  color: #194b49;
}
.time {
  color: #d62828;
}

6. src/components/Logs/LogItem/MyDate/MyDate.jsx

import React from "react";
import "./MyDate.css";

export default function MyDate() {
  return (
    <div>
      <div className="content">
        <h2 className="desc">学习React</h2>
        <div className="time">40分钟</div>
      </div>
    </div>
  );
}

7. src/components/Logs/LogItem/MyDate/MyDate.css

.date {
  width: 90px;
  background-color: #ffffff;
  border-radius: 10px;
  font-weight: bold;
  text-align: center;
  overflow: hidden;
}
.month {
  height: 30px;
  line-height: 30px;
  font-size: 20px;
  color: #ffffff;
  background-color: #d62828;
}
.day {
  height: 60px;
  line-height: 60px;
  font-size: 50px;
  font-weight: bold;
}
.content {
  flex: auto;
  text-align: center;
}

8. src/index.css

* {
  box-sizing: border-box;
}
body {
  background-color: #dfdfdf;
  margin: 0;
}

(五)事件

  • React 中事件需要通过元素的属性来设置
  • 和原生 JS 不同,在 React 中事件的属性需要使用 半驼峰命名法
    • onclick => onClick
    • onchange => onChange
  • 属性值不能直接执行代码,需要一个回调函数
    • onclick="alert(123)" => onClick={() => { alert(123) }}
  • React 中,无法通过 return false 取消默认行为
  • React 事件同样会传递事件对象,可以在响应函数中定义参数来接收事件对象
  • 该对象不是原生中的事件对象,是经过 React 包装后的事件对象
  • 由于对象进行过包装,所以使用过程中无需再考虑兼容性问题

1. src/App.js

const App = () => {
  const clickHandler = (event) => {
    console.log(event);
    event.preventDefault(); // 取消默认行为
    event.stopPropagation(); // 取消事件冒泡
    alert("我是App中的clickHandler");
  };

  return (
    <div
      onClick={() => {
        alert("div");
      }}
      style={{
        width: 200,
        height: 200,
        margin: "100px auto",
        backgroundColor: "#bfa",
      }}
    >
      <button
        onClick={() => {
          alert(123);
        }}
      >
        点我一下
      </button>
      <button onClick={clickHandler}>哈哈</button>
      <hr />
      <a href="https://www.baidu.com" onClick={clickHandler}>
        超链接
      </a>
    </div>
  );
};

export default App;

(六)props

1. src/App.js

import Logs from "./components/Logs/Logs";

function App() {
  return (
    <div>
      <Logs />
    </div>
  );
}

export default App;

2. src/components/Logs/Logs.jsx

  • 在父组件中可以直接在子组件标签中设置属性
// 日志的容器
import React from "react";
import LogItem from "./LogItem/LogItem";
import "./Logs.css";

export default function Logs() {
  return (
    <div className="logs">
      <LogItem date={new Date()} desc={"学习前端"} time="50" />
      <LogItem date={new Date()} desc={"哈哈"} time="30" />
    </div>
  );
}

3. src/components/Logs/LogItem/LogItem.jsx

  • 在组件间,父组件可以通过 props (属性)向子组件传递数据
  • 在函数组件中,属性就相当于是函数的参数,可以通过参数来访问
  • 可以在函数组件的形参中定义一个 props ,指向的是一个对象,包含父组件传递的所有参数
  • props 是只读的,不能修改,只能父组件传给子组件(自顶向下流
import React from "react";
import MyDate from "./MyDate/MyDate";
import "./LogItem.css";

export default function LogItem(props) {
  console.log(props);

  return (
    <div>
      <div className="item">
        <MyDate date={props.date} />
        <div className="content">
          <h2 className="desc">{props.desc}</h2>
          <div className="time">{props.time}分钟</div>
        </div>
      </div>
    </div>
  );
}

4. src/components/Logs/LogItem/MyDate/MyDate.jsx

import React from "react";
import "./MyDate.css";

export default function MyDate(props) {
  // console.log(props.date);
  const month = props.date.toLocaleString("zh-CN", { month: "2-digit" });
  const date = props.date.getDate();

  return (
    <div className="date">
      <div className="month">{month}</div>
      <div className="day">{date}</div>
    </div>
  );
}

(七)渲染数据列表

1. src/components/Logs/Logs.jsx

// 日志的容器
import React from "react";
import LogItem from "./LogItem/LogItem";
import "./Logs.css";

export default function Logs() {
  const logsData = [
    {
      id: "001",
      date: new Date(2022, 1, 20, 18, 30),
      desc: "学习前端",
      time: 60,
    },
    {
      id: "002",
      date: new Date(2022, 2, 15, 18, 30),
      desc: "学习Angular",
      time: 60,
    },
    {
      id: "003",
      date: new Date(2022, 3, 10, 18, 30),
      desc: "学习Vue",
      time: 30,
    },
    {
      id: "004",
      date: new Date(2022, 4, 5, 18, 30),
      desc: "学习React",
      time: 30,
    },
  ];

  const logItemData = logsData.map((log) => <LogItem key={log.id} {...log} />);

  return (
    <div className="logs">
      {/* 在父组件中可以直接在子组件中设置属性 */}
      {/* <LogItem date={new Date()} desc="学习前端" time="50" /> */}

      {logItemData}
    </div>
  );
}

(八)state 简介

  • React 中,当组件渲染完毕后,再修改组件中的变量,不会使组件重新渲染
  • 要使得组件可以受到变量的影响,必须在变量修改后对组件重新渲染
  • 需要一个特殊变量,当这个变量被修改时,组件会自动重新渲染( state
  • state 相当于一个变量,只是该变量在 React 中进行了注册, React 会监控这个变量的变化,自动触发组件重新渲染
  • 在函数组件中,需要通过钩子函数获取 state
  • 使用钩子 useState() 来创建 state
    • 需要一个值作为参数,即 state 的初始值
    • 返回一个数组
      • 第一个元素是初始值,只用于显示数据,修改时不触发组件重新渲染
      • 第二个元素是操作变量的方法,通常命名为 setXxx ,调用后会触发组件重新渲染,并且使用函数中的值作为新的值

1. src/App.js

import { useState } from "react";
import "./App.css";

function App() {
  // let counter = 1;
  let [counter, setCounter] = useState(1);

  const addHandler = () => {
    // counter++;
    setCounter(++counter);
  };

  const lessHandler = () => {
    // counter--;
    setCounter(counter - 1);
  };

  return (
    <div className="app">
      <h1>{counter}</h1>
      <button onClick={lessHandler}>-</button>
      <button onClick={addHandler}>+</button>
    </div>
  );
}

export default App;

2. src/App.css

.app {
  width: 300px;
  height: 300px;
  margin: 50px auto;
  background-color: #bfa;
  text-align: center;
}
.app button {
  width: 100px;
  font-size: 50px;
  margin: 0 20px;
}

(九)state 的注意事项

  • state 就是一个被 React 管理的变量
    • 通过 setState() 修改变量值时,会触发组件的重新渲染
  • 只有 state 值发生变化时,组件才会被重新渲染
  • state 的值是一个对象时,修改时是使用新对象替换旧对象,要确保新对象属性完整
  • 当通过 setState 去修改一个 state 时,当前 state 并没有被修改,而是下一次渲染时的 state
  • setState 会触发组件的重新渲染,它是异步的
    • 所以当调用 setState() 需要用旧 state 值时,有可能出现计算错误的情况
    • 所以需要通过 setState() 传递回调函数的形式来修改 state

1. src/App.js

import { useState } from "react";
import "./App.css";

function App() {
  console.log("函数执行了 ===> 组件创建完毕!");

  const [counter, setCounter] = useState(1);
  const [user, setUser] = useState({ name: "aaa", age: 18 });

  const addHandler = () => {
    // setCounter(counter + 1);
    // setCounter(2); // 不重新渲染

    setTimeout(() => {
      // setCounter(counter + 1);

      /**
       * setState中回调函数的返回值将会成为新的state值
       * 回调函数执行时,React会将最新的state值作为传递
       */
      setCounter((preCounter) => preCounter + 1);
    }, 1000);
  };

  const updateUserHandler = () => {
    // 属性不完整
    // setUser({
    //   name: "bbb",
    // });

    // const newUser = Object.assign({}, user);
    // newUser.name='bbb';
    // setUser(newUser);

    setUser({ ...user, name: "bbb" });
  };

  return (
    <div className="app">
      <h1>
        {counter}---{user.name}---{user.age}
      </h1>
      <button onClick={addHandler}>+</button>
      <button onClick={updateUserHandler}>换人</button>
    </div>
  );
}

export default App;

(十)DOM 对象和 useRef()

1.钩子函数

  • 只能用于函数组件或自定义钩子函数中
  • 钩子函数只能直接在函数组件中调用,不能包裹在自定义函数中

2.获取原生的 DOM 对象

  • 使用传统的 document 操作
  • 直接从 React 处获取 DOM 对象
    • 创建一个存储 DOM 对象的容器【 useRef()
    • 将容器设置为想要获取的 DOM 对象元素的 ref 属性
    • React 会自动将当前元素的 DOM 对象设置为容器的 current 属性

3.useRef() 返回的是一个普通的 JS 对象 {current: undefined}

  • 直接创建该对象也能生效
    • 但是自己创建的对象每次组件重新渲染时都会创建一个新对象
    • useRef() 创建的对象可以确保每次获取到的都是同一个对象
  • 当需要一个对象不会因为组件的重新渲染而改变时,就可以使用 useRef()

4. src/App.js

import { useRef, useState } from "react";
import "./App.css";

// let temp;
function App() {
  const [count, setCount] = useState(1);

  const h1Ref = useRef();

  // const h1Ref = { current: null };
  // console.log(temp === h1Ref);
  // temp = h1Ref;

  const clickHandler = () => {
    const header = document.getElementById("header");
    header.innerHTML = "哈哈";

    alert(h1Ref.current === header);
  };
  const addHandler = () => {
    setCount((prev) => prev + 1);
  };

  return (
    <div className="app">
      <h1 id="header" ref={h1Ref}>
        我是标题{count}
      </h1>
      <button onClick={clickHandler}>1</button>
      <button onClick={addHandler}>2</button>
    </div>
  );
}

export default App;

(十一)类组件

  • 类组件的 props 是存储到类的实例对象中,可以直接访问: this.props
  • 类组件中 state 统一存储到了实例对象的 state 属性中: this.state / this.setState
  • 函数组件中响应函数直接以函数形式定义在组件中,类组件中响应函数以类的方法定义
  • this.setState 只会修改设置的属性,对旧 state合并 处理,仅限于直接存储 state 中的属性(浅合并)
  • 获取 DOM 对象
    • 创建一个属性用于存储 DOM 对象
    • 将属性设置为指定元素的 ref

1. src/App.js

import React from "react";
import "./App.css";
import User from "./components/User";

export default class App extends React.Component {
  render() {
    return (
      <div className="app">
        <User name="bbb" age={22} gender="男" />
      </div>
    );
  }
}

2. src/components/User.jsx

import React, { Component } from "react";

class User extends Component {
  // divRef = React.createRef();
  divRef = {
    current: null,
  };

  state = {
    count: 0,
    test: "哈哈",
    obj: {
      name: "111",
    },
  };

  clickHandler = () => {
    // this.setState({
    //   count: this.state.count + 1,
    // });

    // this.setState((prevState) => ({ count: prevState.count + 1 }));

    // 属性不完整也不会丢失属性
    this.setState({
      count: this.state.count + 1,
      obj: {
        name: "222",
      },
    });

    console.log(this.divRef.current);
  };

  render() {
    const { name, age, gender } = this.props;

    return (
      <div ref={this.divRef}>
        <h1>{this.state.count}</h1>
        <h1>{this.state.obj.name}</h1>
        <button onClick={this.clickHandler}></button>
        <ul>
          <li>姓名:{name}</li>
          <li>年龄:{age}</li>
          <li>性别:{gender}</li>
        </ul>
      </div>
    );
  }
}

export default User;

(十二)添加 Card 组件

  • props.children 表示组件的标签体
  • props.className 表示组件的类名

1. src/App.js

import Logs from "./components/Logs/Logs";

const App = () => {
  return (
    <div>
      <Logs />
    </div>
  );
};

export default App;

2. src/components/UI/Card.jsx

import React from "react";
import "./Card.css";

export default function Card(props) {
  return <div className={`card ${props.className}`}>{props.children}</div>;
}

3. src/components/UI/Card.css

.card {
  border-radius: 10px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}

4. src/components/Logs/Logs.jsx

// 日志的容器
import React from "react";
import Card from "../UI/Card/Card";
import LogItem from "./LogItem/LogItem";
import "./Logs.css";

export default function Logs() {
  const logsData = [
    {
      id: "001",
      date: new Date(2022, 1, 20, 18, 30),
      desc: "学习前端",
      time: 60,
    },
    {
      id: "002",
      date: new Date(2022, 2, 15, 18, 30),
      desc: "学习Angular",
      time: 60,
    },
    {
      id: "003",
      date: new Date(2022, 3, 10, 18, 30),
      desc: "学习Vue",
      time: 30,
    },
    {
      id: "004",
      date: new Date(2022, 4, 5, 18, 30),
      desc: "学习React",
      time: 30,
    },
  ];

  const logItemData = logsData.map((log) => <LogItem key={log.id} {...log} />);

  return <Card className="logs">{logItemData}</Card>;
}

5. src/components/Logs/LogItem/LogItem.jsx

import React from "react";
import MyDate from "./MyDate/MyDate";
import "./LogItem.css";
import Card from "../../UI/Card/Card";

export default function LogItem(props) {
  return (
    <Card className="item">
      <MyDate date={props.date} />
      <div className="content">
        <h2 className="desc">{props.desc}</h2>
        <div className="time">{props.time}分钟</div>
      </div>
    </Card>
  );
}

(十三)创建表单

1. src/App.js

import Logs from "./components/Logs/Logs";
import LogsForm from "./components/LogsForm/LogsForm";
import "./App.css";

const App = () => {
  return (
    <div className="app">
      <LogsForm />
      <Logs />
    </div>
  );
};

export default App;

2. src/components/LogsForm/LogsForm.jsx

import React from "react";
import Card from "../UI/Card/Card";
import "./LogsForm.css";

export default function LogsForm() {
  return (
    <Card>
      <form className="logs-form">
        <div className="form-item">
          <label htmlFor="date">日期</label>
          <input id="date" type="date" />
        </div>
        <div className="form-item">
          <label htmlFor="desc">内容</label>
          <input id="desc" type="text" />
        </div>
        <div className="form-item">
          <label htmlFor="time">时长</label>
          <input id="time" type="number" />
        </div>
        <div className="form-btn">
          <button>添加</button>
        </div>
      </form>
    </Card>
  );
}

3. src/components/LogsForm/LogsForm.css

.logs-form {
  background-color: #eae2b7;
  margin-bottom: 16px;
  padding: 10px;
  border-radius: 10px;
}
.form-item {
  height: 40px;
  line-height: 40px;
  margin: 16px 0;
  display: flex;
  flex-flow: row;
}
.form-item label {
  text-align: center;
  padding: 0 10px;
}
.form-item input {
  flex: auto;
}
.form-btn {
  text-align: center;
}
.form-btn button {
  width: 100px;
  height: 50px;
  border: none;
  background-color: #194b49;
  color: #fff;
  font-weight: bold;
  font-size: 30px;
  transition: 0.3s;
}
.form-btn button:hover {
  background-color: #d62828;
}

(十四)处理表单数据

1. src/components/LogsForm/LogsForm.jsx

import React from "react";
import Card from "../UI/Card/Card";
import "./LogsForm.css";

export default function LogsForm() {
  let inputDate = "",
    inputDesc = "",
    inputTime = "";

  // 创建一个响应函数,监听表单项的变化
  // 监听日期的变化
  const dateChangeHandler = (e) => {
    // 获取当前触发事件的对象
    inputDate = e.target.value;
  };
  // 监听内容的变化
  const descChangeHandler = (e) => {
    inputDesc = e.target.value;
  };
  // 监听时长的变化
  const timeChangeHandler = (e) => {
    inputTime = e.target.value;
  };

  // 表单提交时汇总表单数据
  // React中通常表单不需要自行提交,而是通过React提交
  const formSubmitHandler = (e) => {
    // 取消表单默认行为
    e.preventDefault();
    // 获取表单数据
    const newLog = {
      date: new Date(inputDate),
      desc: inputDesc,
      time: +inputTime,
    };
    console.log(newLog);
  };

  return (
    <Card>
      <form className="logs-form" onSubmit={formSubmitHandler}>
        <div className="form-item">
          <label htmlFor="date">日期</label>
          <input id="date" type="date" onChange={dateChangeHandler} />
        </div>
        <div className="form-item">
          <label htmlFor="desc">内容</label>
          <input id="desc" type="text" onChange={descChangeHandler} />
        </div>
        <div className="form-item">
          <label htmlFor="time">时长</label>
          <input id="time" type="number" onChange={timeChangeHandler} />
        </div>
        <div className="form-btn">
          <button>添加</button>
        </div>
      </form>
    </Card>
  );
}

(十五)双向绑定

1. src/components/LogsForm/LogsForm.jsx

import React, { useState } from "react";
import Card from "../UI/Card/Card";
import "./LogsForm.css";

export default function LogsForm() {
  // 普通变量 ==> 非受控组件
  // let inputDate = "",
  //   inputDesc = "",
  //   inputTime = "";

  // state变量 ==> 受控组件
  const [inputDate, setInputDate] = useState("");
  const [inputDesc, setInputDesc] = useState("");
  const [inputTime, setInputTime] = useState("");

  // 创建一个响应函数,监听表单项的变化
  // 监听日期的变化
  const dateChangeHandler = (e) => {
    // 获取当前触发事件的对象
    // inputDate = e.target.value;
    setInputDate(e.target.value);
  };
  // 监听内容的变化
  const descChangeHandler = (e) => {
    // inputDesc = e.target.value;
    setInputDesc(e.target.value);
  };
  // 监听时长的变化
  const timeChangeHandler = (e) => {
    // inputTime = e.target.value;
    setInputTime(e.target.value);
  };

  // 表单提交时汇总表单数据
  // React中通常表单不需要自行提交,而是通过React提交
  const formSubmitHandler = (e) => {
    // 取消表单默认行为
    e.preventDefault();
    // 获取表单数据
    const newLog = {
      date: new Date(inputDate),
      desc: inputDesc,
      time: +inputTime,
    };
    console.log(newLog);
    // 清空表单旧数据(当前表单在React中称为非受控组件,不建议使用)
    // 将表单中的数据存储到state中,然后将state设置为表单项的value属性,则表单项值和state同步变化——双向绑定
    setInputDate("");
    setInputDesc("");
    setInputTime("");
  };

  return (
    <Card>
      <form className="logs-form" onSubmit={formSubmitHandler}>
        <div className="form-item">
          <label htmlFor="date">日期</label>
          <input id="date" type="date" value={inputDate} onChange={dateChangeHandler} />
        </div>
        <div className="form-item">
          <label htmlFor="desc">内容</label>
          <input id="desc" type="text" value={inputDesc} onChange={descChangeHandler} />
        </div>
        <div className="form-item">
          <label htmlFor="time">时长</label>
          <input id="time" type="number" value={inputTime} onChange={timeChangeHandler} />
        </div>
        <div className="form-btn">
          <button>添加</button>
        </div>
      </form>
    </Card>
  );
}

(十六)存储到一个 state 变量中

1. src/components/LogsForm/LogsForm.jsx

import React, { useState } from "react";
import Card from "../UI/Card/Card";
import "./LogsForm.css";

export default function LogsForm() {
  // state变量 ==> 受控组件
  // const [inputDate, setInputDate] = useState("");
  // const [inputDesc, setInputDesc] = useState("");
  // const [inputTime, setInputTime] = useState("");

  const [formData, setFormData] = useState({
    inputDate: "",
    inputDesc: "",
    inputTime: "",
  });

  // 创建一个响应函数,监听表单项的变化
  // 监听日期的变化
  const dateChangeHandler = (e) => {
    // 获取当前触发事件的对象
    // setInputDate(e.target.value);
    setFormData({
      ...formData,
      inputDate: e.target.value,
    });
  };
  // 监听内容的变化
  const descChangeHandler = (e) => {
    // setInputDesc(e.target.value);
    setFormData({
      ...formData,
      inputDesc: e.target.value,
    });
  };
  // 监听时长的变化
  const timeChangeHandler = (e) => {
    // setInputTime(e.target.value);
    setFormData({
      ...formData,
      inputTime: e.target.value,
    });
  };

  // 表单提交时汇总表单数据
  // React中通常表单不需要自行提交,而是通过React提交
  const formSubmitHandler = (e) => {
    // 取消表单默认行为
    e.preventDefault();
    // 获取表单数据
    const newLog = {
      // date: new Date(inputDate),
      // desc: inputDesc,
      // time: +inputTime,
      date: new Date(formData.inputDate),
      desc: formData.inputDesc,
      time: +formData.inputTime,
    };
    console.log(newLog);
    // 清空表单旧数据
    // setInputDate("");
    // setInputDesc("");
    // setInputTime("");
    setFormData({
      inputDate: "",
      inputDesc: "",
      inputTime: "",
    });
  };

  return (
    <Card>
      <form className="logs-form" onSubmit={formSubmitHandler}>
        <div className="form-item">
          <label htmlFor="date">日期</label>
          <input id="date" type="date" value={formData.inputDate} onChange={dateChangeHandler} />
        </div>
        <div className="form-item">
          <label htmlFor="desc">内容</label>
          <input id="desc" type="text" value={formData.inputDesc} onChange={descChangeHandler} />
        </div>
        <div className="form-item">
          <label htmlFor="time">时长</label>
          <input id="time" type="number" value={formData.inputTime} onChange={timeChangeHandler} />
        </div>
        <div className="form-btn">
          <button>添加</button>
        </div>
      </form>
    </Card>
  );
}

(十七)添加新日志

1. src/App.js

import Logs from "./components/Logs/Logs";
import LogsForm from "./components/LogsForm/LogsForm";
import "./App.css";
import { useState } from "react";

const App = () => {
  const [logsData, setLogsData] = useState([
    {
      id: "001",
      date: new Date(2022, 1, 20, 18, 30),
      desc: "学习前端",
      time: 60,
    },
    {
      id: "002",
      date: new Date(2022, 2, 15, 18, 30),
      desc: "学习Angular",
      time: 60,
    },
    {
      id: "003",
      date: new Date(2022, 3, 10, 18, 30),
      desc: "学习Vue",
      time: 30,
    },
    {
      id: "004",
      date: new Date(2022, 4, 5, 18, 30),
      desc: "学习React",
      time: 30,
    },
  ]);

  // 定义一个函数,接收子组件传递的数据
  const saveLogHandler = (newLog) => {
    // 向新的日志中添加id
    newLog.id = Date.now() + "";
    // 将新的日志添加到数组中
    setLogsData([newLog, ...logsData]);
  };

  return (
    <div className="app">
      <LogsForm onSaveLog={saveLogHandler} />
      <Logs logsData={logsData} />
    </div>
  );
};

export default App;

2. src/components/Logs/Logs.jsx

// 日志的容器
import React from "react";
import Card from "../UI/Card/Card";
import LogItem from "./LogItem/LogItem";
import "./Logs.css";

export default function Logs(props) {
  /* 
    logsData用来存储学习的日志
    该数据除了当前组件Logs需要使用,LogsForm也需要使用
    此时(一个数据需要被多个组件使用时),可以将数据放入这些组件共同的祖先组件中【state的提升】
  */
  // const logsData = [
  //   {
  //     id: "001",
  //     date: new Date(2022, 1, 20, 18, 30),
  //     desc: "学习前端",
  //     time: 60,
  //   },
  //   {
  //     id: "002",
  //     date: new Date(2022, 2, 15, 18, 30),
  //     desc: "学习Angular",
  //     time: 60,
  //   },
  //   {
  //     id: "003",
  //     date: new Date(2022, 3, 10, 18, 30),
  //     desc: "学习Vue",
  //     time: 30,
  //   },
  //   {
  //     id: "004",
  //     date: new Date(2022, 4, 5, 18, 30),
  //     desc: "学习React",
  //     time: 30,
  //   },
  // ];

  const logItemData = props.logsData.map((log) => <LogItem key={log.id} {...log} />);

  return <Card className="logs">{logItemData}</Card>;
}

3. src/components/LogsForm/LogsForm.jsx

import React, { useState } from "react";
import Card from "../UI/Card/Card";
import "./LogsForm.css";

export default function LogsForm(props) {
  // state变量 ==> 受控组件
  const [inputDate, setInputDate] = useState("");
  const [inputDesc, setInputDesc] = useState("");
  const [inputTime, setInputTime] = useState("");

  // 创建一个响应函数,监听表单项的变化
  // 监听日期的变化
  const dateChangeHandler = (e) => {
    // 获取当前触发事件的对象
    setInputDate(e.target.value);
  };
  // 监听内容的变化
  const descChangeHandler = (e) => {
    setInputDesc(e.target.value);
  };
  // 监听时长的变化
  const timeChangeHandler = (e) => {
    setInputTime(e.target.value);
  };

  // 表单提交时汇总表单数据
  // React中通常表单不需要自行提交,而是通过React提交
  const formSubmitHandler = (e) => {
    // 取消表单默认行为
    e.preventDefault();
    // 获取表单数据
    const newLog = {
      date: new Date(inputDate),
      desc: inputDesc,
      time: +inputTime,
    };
    // 调用父组件传递的函数,把数据递上去
    props.onSaveLog(newLog);
    // 清空表单旧数据
    setInputDate("");
    setInputDesc("");
    setInputTime("");
  };

  return (
    <Card>
      <form className="logs-form" onSubmit={formSubmitHandler}>
        <div className="form-item">
          <label htmlFor="date">日期</label>
          <input id="date" type="date" value={inputDate} onChange={dateChangeHandler} />
        </div>
        <div className="form-item">
          <label htmlFor="desc">内容</label>
          <input id="desc" type="text" value={inputDesc} onChange={descChangeHandler} />
        </div>
        <div className="form-item">
          <label htmlFor="time">时长</label>
          <input id="time" type="number" value={inputTime} onChange={timeChangeHandler} />
        </div>
        <div className="form-btn">
          <button>添加</button>
        </div>
      </form>
    </Card>
  );
}

(十八)删除旧日志

1. src/App.js

import Logs from "./components/Logs/Logs";
import LogsForm from "./components/LogsForm/LogsForm";
import "./App.css";
import { useState } from "react";

const App = () => {
  const [logsData, setLogsData] = useState([
    {
      id: "001",
      date: new Date(2022, 1, 20, 18, 30),
      desc: "学习前端",
      time: 60,
    },
    {
      id: "002",
      date: new Date(2022, 2, 15, 18, 30),
      desc: "学习Angular",
      time: 60,
    },
    {
      id: "003",
      date: new Date(2022, 3, 10, 18, 30),
      desc: "学习Vue",
      time: 30,
    },
    {
      id: "004",
      date: new Date(2022, 4, 5, 18, 30),
      desc: "学习React",
      time: 30,
    },
  ]);

  // 定义一个函数,接收子组件传递的数据
  const saveLogHandler = (newLog) => {
    // 向新的日志中添加id
    newLog.id = Date.now() + "";
    // 将新的日志添加到数组中
    setLogsData([newLog, ...logsData]);
  };

  // 定义一个函数,从数据中删除一条日志
  const delLogByIndex = (index) => {
    setLogsData((prevState) => {
      const newLogs = [...prevState];
      // slice不影响原数组,splice影响原数组
      newLogs.splice(index, 1);
      return newLogs;
    });
  };

  return (
    <div className="app">
      <LogsForm onSaveLog={saveLogHandler} />
      <Logs logsData={logsData} onDelLog={delLogByIndex} />
    </div>
  );
};

export default App;

2. src/components/Logs/Logs.jsx

// 日志的容器
import React from "react";
import Card from "../UI/Card/Card";
import LogItem from "./LogItem/LogItem";
import "./Logs.css";

export default function Logs(props) {
  const logItemData = props.logsData.map((log, index) => (
    // 方法一
    // <LogItem key={log.id} logIndex={index} onDelLog={props.onDelLog} {...log} />
    // 方法二
    <LogItem key={log.id} onDelLog={() => props.onDelLog(index)} {...log} />
  ));

  return <Card className="logs">{logItemData}</Card>;
}

3. src/components/Logs/LogItem/LogItem.jsx

import React from "react";
import MyDate from "./MyDate/MyDate";
import "./LogItem.css";
import Card from "../../UI/Card/Card";

export default function LogItem(props) {
  const deleteHandler = () => {
    if (window.confirm("该操作不可恢复,确认吗?")) {
      // 删除当前日志
      // 方法一
      // props.onDelLog(props.logIndex);
      // 方法二
      props.onDelLog();
    }
  };

  return (
    <Card className="item">
      <MyDate date={props.date} />
      <div className="content">
        <h2 className="desc">{props.desc}</h2>
        <div className="time">{props.time}分钟</div>
      </div>
      <div>
        <div className="delete" onClick={deleteHandler}>
          x
        </div>
      </div>
    </Card>
  );
}

4. src/components/Logs/LogItem/LogItem.css

.item {
  display: flex;
  margin: 16px 0;
  padding: 6px;
  background-color: #fcbf49;
}
.desc {
  font-size: 16px;
  color: #194b49;
}
.time {
  color: #d62828;
}
.delete {
  font-size: 20px;
  font-weight: bold;
  color: #fff;
  margin-right: 10px;
  cursor: pointer;
  transition: 0.3s;
}
.delete:hover {
  color: #d62828;
  transform: rotate(1turn);
}

(十九)空列表提示

1. src/components/Logs/Logs.js

// 日志的容器
import React from "react";
import Card from "../UI/Card/Card";
import LogItem from "./LogItem/LogItem";
import "./Logs.css";

export default function Logs(props) {
  const logItemData = props.logsData.map((log, index) => <LogItem key={log.id} onDelLog={() => props.onDelLog(index)} {...log} />);

  return <Card className="logs">{logItemData.length !== 0 ? logItemData : <p className="no-logs">没有找到日志!</p>}</Card>;
}

2. src/components/Logs/Logs.css

.logs {
  padding: 20px;
  background-color: #eae2b7;
}

.no-logs {
  text-align: center;
  font-size: 30px;
  font-weight: bold;
}

(二十)添加 ConfirmModal 确认框

1. src/components/UI/ConfirmModal/ConfirmModal.jsx

import React from "react";
import Card from "../Card/Card";
import "./ConfirmModal.css";

export default function ConfirmModal() {
  return (
    <Card className="confirm-modal">
      <div className="confirm-text">
        <p>该操作不可恢复!确认吗?</p>
      </div>
      <div className="confirm-button">
        <button className="ok-btn">确认</button>
        <button>取消</button>
      </div>
    </Card>
  );
}

2. src/components/UI/ConfirmModal/ConfirmModal.css

.confirm-modal {
  display: flex;
  flex-direction: column;
  width: 400px;
  height: 200px;
  padding: 10px;
  background-color: #fff;
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  margin: auto;
}
.confirm-text {
  height: 150px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  color: #d62828;
}
.confirm-button {
  flex: auto;
  display: flex;
  justify-content: flex-end;
}
.confirm-button button {
  width: 80px;
  margin: 0 10px;
  border: none;
  background-color: #eae2b7;
  font-weight: bold;
  font-size: 18px;
  cursor: pointer;
}
.confirm-button .ok-btn {
  background-color: #d62828;
  color: #fff;
}

(二十一)完成确认窗口 BackDrop

1. src/components/UI/ConfirmModal/ConfirmModal.jsx

import React from "react";
import BackDrop from "../BackDrop/BackDrop";
import Card from "../Card/Card";
import "./ConfirmModal.css";

export default function ConfirmModal(props) {
  return (
    <BackDrop>
      <Card className="confirm-modal">
        <div className="confirm-text">
          <p>{props.confirmText}</p>
        </div>
        <div className="confirm-button">
          <button onClick={props.onOk} className="ok-btn">
            确认
          </button>
          <button onClick={props.onCancel}>取消</button>
        </div>
      </Card>
    </BackDrop>
  );
}

2. src/components/UI/BackDrop/BackDrop.jsx

import React from "react";
import "./BackDrop.css";

export default function BackDrop(props) {
  return <div className="back-drop">{props.children}</div>;
}

3. src/components/UI/BackDrop/BackDrop.css

.back-drop {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  margin: auto;
  background-color: rgba(0, 0, 0, 0.3);
}

4. src/components/Logs/LogItem/LogItem.jsx

import React, { useState } from "react";
import MyDate from "./MyDate/MyDate";
import "./LogItem.css";
import Card from "../../UI/Card/Card";
import ConfirmModal from "../../UI/ConfirmModal/ConfirmModal";

export default function LogItem(props) {
  // 添加一个state,记录是否显示确认窗口
  const [showConfirm, setShowConfirm] = useState(false);

  const deleteHandler = () => {
    // if (window.confirm("该操作不可恢复,确认吗?")) {
    //   // 删除当前日志
    //   props.onDelLog();
    // }

    // 显示确认窗口
    setShowConfirm(true);
  };

  // 确认函数
  const okHandler = () => {
    props.onDelLog();
  };

  // 取消函数
  const cancelHandler = () => {
    setShowConfirm(false);
  };

  return (
    <Card className="item">
      {showConfirm && <ConfirmModal confirmText="该操作不可恢复!确认吗?" onOk={okHandler} onCancel={cancelHandler} />}

      <MyDate date={props.date} />
      <div className="content">
        <h2 className="desc">{props.desc}</h2>
        <div className="time">{props.time}分钟</div>
      </div>
      <div>
        <div className="delete" onClick={deleteHandler}>
          x
        </div>
      </div>
    </Card>
  );
}

(二十二)使用 Portal 修改项目

  • ConfirmModal 默认以父组件后代渲染时,一旦父组件开启了定位,样式就会混乱,且有层级等问题出现
  • 使用 Portal 可以解决(将组件渲染到页面中的指定位置)
    • index.html 中添加一个新的容器
    • 修改组件的渲染方式
      • 通过 ReactDOM.createPortal() 作为返回值创建元素
      • 参数:
        • JSX (修改前 return 后的代码)
        • 目标位置(第 1 步中创建的 DOM 元素)

1. public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>学习日志</title>
  </head>

  <body>
    <div id="root"></div>
    <!-- 专门用于渲染遮罩层组件 -->
    <div id="backdrop_root"></div>
  </body>
</html>

2.src/components/UI/BackDrop/BackDrop.jsx

import React from "react";
import ReactDOM from "react-dom";
import "./BackDrop.css";

// 获取BackDrop的根元素
const backDropRoot = document.getElementById("backdrop_root");

export default function BackDrop(props) {
  // return <div className="back-drop">{props.children}</div>;
  return ReactDOM.createPortal(<div className="back-drop">{props.children}</div>, backDropRoot);
}

3. src/components/UI/BackDrop/BackDrop.css

.back-drop {
  /* 
    绝对定位会有隐患——离BackDrop组件最近的父元素一旦开启相对定位,该组件位置就会变化
  */
  /* position: absolute; */
  position: fixed;
  z-index: 9999;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  margin: auto;
  background-color: rgba(0, 0, 0, 0.3);
}

4. src/components/Logs/LogItem/LogItem.jsx

import React, { useState } from "react";
import MyDate from "./MyDate/MyDate";
import "./LogItem.css";
import Card from "../../UI/Card/Card";
import ConfirmModal from "../../UI/ConfirmModal/ConfirmModal";

export default function LogItem(props) {
  // 添加一个state,记录是否显示确认窗口
  const [showConfirm, setShowConfirm] = useState(false);

  const deleteHandler = () => {
    // 显示确认窗口
    setShowConfirm(true);
  };

  // 确认函数
  const okHandler = () => {
    props.onDelLog();
  };

  // 取消函数
  const cancelHandler = () => {
    setShowConfirm(false);
  };

  return (
    <Card className="item">
      {showConfirm && <ConfirmModal confirmText="该操作不可恢复!确认吗?" onOk={okHandler} onCancel={cancelHandler} />}

      <MyDate date={props.date} />
      <div className="content">
        <h2 className="desc">{props.desc}</h2>
        <div className="time">{props.time}分钟</div>
      </div>
      <div>
        <div className="delete" onClick={deleteHandler}>
          x
        </div>
      </div>
    </Card>
  );
}

(二十三)过滤日志

1. src/App.js

import Logs from "./components/Logs/Logs";
import LogsForm from "./components/LogsForm/LogsForm";
import "./App.css";
import { useState } from "react";

const App = () => {
  const [logsData, setLogsData] = useState([
    {
      id: "001",
      date: new Date(2022, 1, 20, 18, 30),
      desc: "学习前端",
      time: 60,
    },
    {
      id: "002",
      date: new Date(2021, 2, 15, 18, 30),
      desc: "学习Angular",
      time: 60,
    },
    {
      id: "003",
      date: new Date(2022, 3, 10, 18, 30),
      desc: "学习Vue",
      time: 30,
    },
    {
      id: "004",
      date: new Date(2022, 4, 5, 18, 30),
      desc: "学习React",
      time: 30,
    },
  ]);

  // 定义一个函数,接收子组件传递的数据
  const saveLogHandler = (newLog) => {
    // 向新的日志中添加id
    newLog.id = Date.now() + "";
    // 将新的日志添加到数组中
    setLogsData([newLog, ...logsData]);
  };

  // 定义一个函数,从数据中删除一条日志
  // const delLogByIndex = (index) => {
  //   setLogsData((prevState) => {
  //     const newLogs = [...prevState];
  //     // slice不影响原数组,splice影响原数组
  //     newLogs.splice(index, 1);
  //     return newLogs;
  //   });
  // };

  // 定义一个函数,从数据中删除一条日志
  const delLogById = (id) => {
    setLogsData((prevState) => {
      return prevState.filter((item) => item.id !== id);
    });
  };

  return (
    <div className="app">
      <LogsForm onSaveLog={saveLogHandler} />
      <Logs logsData={logsData} onDelLog={delLogById} />
    </div>
  );
};

export default App;

2. src/components/Logs/Logs.jsx

// 日志的容器
import React, { useState } from "react";
import LogFilter from "../LogFilter/LogFilter";
import Card from "../UI/Card/Card";
import LogItem from "./LogItem/LogItem";
import "./Logs.css";

export default function Logs(props) {
  // 创建存储年份的state
  const [year, setYear] = useState(2022);
  const onSelectYear = (year) => {
    setYear(year);
  };

  // 过滤数据,只显示某一年的数据
  let filterData = props.logsData.filter((item) => item.date.getFullYear() === year);

  const logItemData = filterData.map((log) => <LogItem key={log.id} onDelLog={() => props.onDelLog(log.id)} {...log} />);

  return (
    <Card className="logs">
      <LogFilter year={year} onSelectYear={onSelectYear} />
      {logItemData.length !== 0 ? logItemData : <p className="no-logs">没有找到日志!</p>}
    </Card>
  );
}

3. src/components/LogFilter/LogFilter.jsx

import React from "react";

export default function LogFilter(props) {
  // 监听下拉框change
  const changeHandler = (e) => {
    props.onSelectYear(+e.target.value);
  };

  return (
    <div>
      年份:
      <select value={props.year} onChange={changeHandler}>
        <option value="2022">2022</option>
        <option value="2021">2021</option>
        <option value="2020">2020</option>
      </select>
    </div>
  );
}
上次编辑于: