四、案例:学习日志
大约 22 分钟约 6646 字
(一)创建项目
public
目录
1. <!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>
src/index.js
2. // 引入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);
src/index.css
3. * {
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)类式组件
src/index.js
1. // 引入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 />);
src/App.js
2. 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
src/index.js
1. // 引入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 />);
src/App.js
2. 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>
);
}
}
(四)使用自定义组件
src/App.js
1. import Logs from "./components/Logs/Logs";
function App() {
return (
<div>
<Logs />
</div>
);
}
export default App;
src/components/Logs/Logs.jsx
2. // 日志的容器
import React from "react";
import LogItem from "./LogItem/LogItem";
import "./Logs.css";
export default function Logs() {
return (
<div className="logs">
<LogItem />
</div>
);
}
src/components/Logs/Logs.css
3. .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);
}
src/components/Logs/LogItem/LogItem.jsx
4. 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>
);
}
src/components/Logs/LogItem/LogItem.css
5. .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;
}
src/components/Logs/LogItem/MyDate/MyDate.jsx
6. 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>
);
}
src/components/Logs/LogItem/MyDate/MyDate.css
7. .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;
}
src/index.css
8. * {
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
包装后的事件对象 - 由于对象进行过包装,所以使用过程中无需再考虑兼容性问题
src/App.js
1. 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
src/App.js
1. import Logs from "./components/Logs/Logs";
function App() {
return (
<div>
<Logs />
</div>
);
}
export default App;
src/components/Logs/Logs.jsx
2. - 在父组件中可以直接在子组件标签中设置属性
// 日志的容器
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>
);
}
src/components/Logs/LogItem/LogItem.jsx
3. - 在组件间,父组件可以通过
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>
);
}
src/components/Logs/LogItem/MyDate/MyDate.jsx
4. 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>
);
}
(七)渲染数据列表
src/components/Logs/Logs.jsx
1. // 日志的容器
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
,调用后会触发组件重新渲染,并且使用函数中的值作为新的值
- 需要一个值作为参数,即
src/App.js
1. 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;
src/App.css
2. .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
- 所以当调用
src/App.js
1. 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
属性
- 创建一个存储
useRef()
返回的是一个普通的 JS
对象 {current: undefined}
3.- 直接创建该对象也能生效
- 但是自己创建的对象每次组件重新渲染时都会创建一个新对象
useRef()
创建的对象可以确保每次获取到的都是同一个对象
- 当需要一个对象不会因为组件的重新渲染而改变时,就可以使用
useRef()
src/App.js
4. 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
- 创建一个属性用于存储
src/App.js
1. 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>
);
}
}
src/components/User.jsx
2. 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
表示组件的类名
src/App.js
1. import Logs from "./components/Logs/Logs";
const App = () => {
return (
<div>
<Logs />
</div>
);
};
export default App;
src/components/UI/Card.jsx
2. import React from "react";
import "./Card.css";
export default function Card(props) {
return <div className={`card ${props.className}`}>{props.children}</div>;
}
src/components/UI/Card.css
3. .card {
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
src/components/Logs/Logs.jsx
4. // 日志的容器
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>;
}
src/components/Logs/LogItem/LogItem.jsx
5. 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>
);
}
(十三)创建表单
src/App.js
1. 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;
src/components/LogsForm/LogsForm.jsx
2. 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>
);
}
src/components/LogsForm/LogsForm.css
3. .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;
}
(十四)处理表单数据
src/components/LogsForm/LogsForm.jsx
1. 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>
);
}
(十五)双向绑定
src/components/LogsForm/LogsForm.jsx
1. 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 变量中
src/components/LogsForm/LogsForm.jsx
1. 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>
);
}
(十七)添加新日志
src/App.js
1. 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;
src/components/Logs/Logs.jsx
2. // 日志的容器
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>;
}
src/components/LogsForm/LogsForm.jsx
3. 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>
);
}
(十八)删除旧日志
src/App.js
1. 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;
src/components/Logs/Logs.jsx
2. // 日志的容器
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>;
}
src/components/Logs/LogItem/LogItem.jsx
3. 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>
);
}
src/components/Logs/LogItem/LogItem.css
4. .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);
}
(十九)空列表提示
src/components/Logs/Logs.js
1. // 日志的容器
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>;
}
src/components/Logs/Logs.css
2. .logs {
padding: 20px;
background-color: #eae2b7;
}
.no-logs {
text-align: center;
font-size: 30px;
font-weight: bold;
}
(二十)添加 ConfirmModal 确认框
src/components/UI/ConfirmModal/ConfirmModal.jsx
1. 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>
);
}
src/components/UI/ConfirmModal/ConfirmModal.css
2. .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
src/components/UI/ConfirmModal/ConfirmModal.jsx
1. 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>
);
}
src/components/UI/BackDrop/BackDrop.jsx
2. import React from "react";
import "./BackDrop.css";
export default function BackDrop(props) {
return <div className="back-drop">{props.children}</div>;
}
src/components/UI/BackDrop/BackDrop.css
3. .back-drop {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
background-color: rgba(0, 0, 0, 0.3);
}
src/components/Logs/LogItem/LogItem.jsx
4. 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
元素)
- 通过
- 在
public/index.html
1. <!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>
src/components/UI/BackDrop/BackDrop.jsx
2.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);
}
src/components/UI/BackDrop/BackDrop.css
3. .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);
}
src/components/Logs/LogItem/LogItem.jsx
4. 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>
);
}
(二十三)过滤日志
src/App.js
1. 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;
src/components/Logs/Logs.jsx
2. // 日志的容器
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>
);
}
src/components/LogFilter/LogFilter.jsx
3. 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>
);
}