开发思路
:new: 组件化的开发思路:
6-1. 静态布局:组件拆分
6-2. 首屏数据渲染:
- 数据结构分析:
- 分析数据定义在谁身上
6-3. 用户交互功能实现
1. 描述:干了什么-》什么结果
2. 拆分步骤:
3. 给步骤匹配技术点
导包规范
把第三方的包放在自定义的上边
轮播图练习
🆕 主页面文件:src->App.js
import React, { Component } from 'react'
import Child from './components/Class/Child'
export default class App extends Component {
render() {
return (
<div>
<h3>App</h3>
<hr />
<Child></Child>
</div>
)
}
}
导入图片:src->assets->images
1.jpeg
2.jpeg
3.jpeg
子组件:src->components->Class->Child.jsx
import React, { Component } from 'react'
// 导入样式
import './Child.css'
export default class Child extends Component {
// 定义数据状态
state = {
imgArr: ['1.jpeg', '2.jpeg', '3.jpeg'],
index: 0
}
// 定义方法
changePicAdd(index,imgArr){
index++;
if (index > imgArr.length -1) {
index = 0;
}
this.setState({
index
})
}
changePicSub(index, imgArr){
index--;
if (index < 0) {
index = imgArr.length - 1;
}
this.setState({
index
})
}
render() {
let { index,imgArr } = this.state;
return (
<>
<div className='wrapper'>
<img src={require(`../../assets/images/${imgArr[index]}`)} alt="" />
<span className="" onClick={() => { this.changePicSub(index,imgArr) }}><</span>
<span className="rightBtn" onClick={this.changePicAdd.bind(this,index,imgArr)}>></span>
</div>
</>
)
}
}
子组件样式:src->components->Class->Child.css
.wrapper {
position: relative;
width: 600px;
height: 350px;
border: 1px solid aqua;
margin: 50px auto;
}
.wrapper img {
width: 100%;
height: 100%;
}
.wrapper span {
position: absolute;
top: 145px;
display: block;
width: 30px;
height: 60px;
background-color: #ccc;
line-height: 60px;
text-align: center;
cursor: pointer;
user-select: none;
}
.rightBtn {
right: 0;
}
Css样式处理
第三方的css样式库: bootstrap.css
- 方式一:
位置:public/css/bootstrap.css
引入方式:public/index.html 通过 link标签引入
<link rel="stylesheet" herf="%PUBLIC_URL%/css/bootstrap.css">或
<!-- /前边没有./ /标识一个绝对目录,表示的是网站的根目录-->
<link rel="stylesheet" herf="/css/bootstrap.css">
- 方式二:
1. 安装样式库: `npm i bootstrap@3`
2. 在入口文件 src->index.js 使用import 语法导入样式:
import 'bootstrap/dist/css/bootstrap.min.css'
重置样式:
位置:src/index.css
引入:在入口文件 src/index.js 通过 import 导入
全局通用样式:
位置:src/App.css
引入:在主App页面中 src/App.jsx 通过 import 导入
组件内样式:
位置:组件目录的css文件中
导入:组件中使用import 导入
BootStrap警告
再导入bootstrap.css后会在控制台提示bootstrap.map这个警告,可以在bootstrap.css文件的末尾删除最后一行注释即可。
Css模块化
css模块化:将css文件变为 js模块进行处理
作用:在样式后使用一个随机的数字后缀,解决组件同类名的样式冲突问题,原生Css默认后边导入的样式会覆盖前边导入的。
步骤:
#1. css文件名:必须以 文件名.module.css 后缀结尾
#2. 使用import 语法导入css模块并存储成 js变量
例如:
import styles from './index.module.css'
console.log('styles: ', styles); // {box: 'B_box__-Qmig'}
#3. 使用插值表达式给className赋值,之为 styles中的类名
import React, { Component } from 'react'
// 2 导入css模块并存储成js变量
import styles from './index.module.css'
console.log('styles: ', styles); // styles被处理成了一个对象
export default class B extends Component {
render() {
return (
<>
<div className={styles.box}>B</div>
<div className={[styles.box, styles.app, 'container'].join(' ')}>B多个类名</div>
</>
)
}
}
图片处理
1. 网络图片:
直接填入网络地址即可
2. 本地图片:
前提:存放位置必须在src目录中
2-1. 使用 import 导入
import imgSrc from './assets/images/1.jpeg'
<img src={imgSrc} alt="" />
2-2. 使用 require 导入
// 定义数据状态
state = {
index:2
}
// 使用模版字符串把对应位置替换为变量
<img src={require(`./assets/images/${index}.jpeg`)} alt="" />
import 导入和 require导入图片的差别:
require导入可以看到图片的路径,可以通过状态数据控制导入的图片目录
import导入的图片路径是写死的,不能控制
关闭eslint语法检查
关闭eslint语法检查,在package.js中配置
...
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"rules": {
"xxx":0
}
},
...
然后重启项目
TodoList 类组件
静态页面拆分
1. 创建components目录
2. 创建 TodoList、Header、Main、Item、Footer目录
3. 创建组件[jsx]及css文件
4. 在各个组件中导入 css样式 import './index.css'
5. 将todolist静态页面的整体结构拷贝到 TodoList/Todolist.jsx的结构中
6. 将样式拷贝到 TodoList/index.css中
7. 将TodoList.jsx中的 class ===> className
8. 将TodoList.jsx中的 结构 拆分到各个组件中
9. 将TodoList/index.css 的样式,拆分到各个组件中
数据结构分析
let todos = [
{
id:xxx,
title:'吃饭',
isDone:true | false
}
]
分析数据应该定义在哪个组件身上
原则:
1. 如果数据只在一个组件身上使用,那么就当以在该组件自己身上
2. 如果数据在多个组件都需要使用,那么定义在他们共同的父级身上[也叫做状态提升]
实现思路
实现用户交互功能
思路生成步骤:描述你要做什么,产生了什么结果
1. 文本框输入内容,按下回车,将内容添加到列表中
2. 拆分步骤:
2.1 获取文本框输入内容
2.2 按下回车
2.3 将内容添加到列表中
3. 给步骤匹配你能想到的技术点
3.1 获取文本框输入内容
ref
受控组件
e.target.value
3.2 按下回车
绑定事件-键盘事件 keyup keydown onKeyUp onKeyDown
3.3 将内容添加到列表中
子传父
首屏渲染
-- 使用nanoid包生成一个不重复的id
# 安装
npm i nanoid
# 导入
import {nanoid} from 'nanoid'
# 调用
nanoid()
-- 使用random方法生成一个不重复的id 36=10位数字加26位字母
Math.random().toString(36).slice(2)
首屏数据渲染实现步骤及技术点
1. 将todos 数据定义成 TodoList 组件的状态
2. 将 todos数据 通过标签属性的方式传递给 Main组件和 Footer组件
3. 在Main组件中接收todos,并使用map进行列表渲染,定义key值,并将遍历的每一项 todo传递给 Item组件
4. 在Footer组件中接收todos,计算total 和 doneNum后进行渲染
src->components->TodoList->TodoList.jsx
1. 将todos 数据定义成 TodoList 组件的状态
2. 将 todos数据 通过标签属性的方式传递给 Main组件和 Footer组件
import React, { Component } from 'react'
import {nanoid} from 'nanoid'
import Footer from '../Footer/Footer'
import Header from '../Header/Header'
import Main from '../Main/Main'
import './index.css'
export default class TodoList extends Component {
state={
todos:[
{
id:nanoid(),
title:'吸烟',
isDone:true
},
{
id: nanoid(),
title: '喝酒',
isDone: true
},
{
id: nanoid(),
title: '烫头',
isDone: false
}
]
}
render() {
let { todos } = this.state
return (
<>
<div className="todo-container">
<div className="todo-wrap">
<Header />
<Main todos={todos}/>
<Footer todos={todos} />
</div>
</div>
</>
)
}
}
src->components->Main->Main.jsx
在Main组件中接收todos,并使用map进行列表渲染,定义key值,并将遍历的每一项 todo传递给 Item组件
import React, { Component } from 'react'
import Item from '../Item/Item'
import './index.css'
export default class Main extends Component {
render() {
let {todos} = this.props
return (
<ul className="todo-main">
{todos.map(todo => (<Item key={todo.id} todo={todo} />))}
</ul>
)
}
}
src->components->Item->Item.jsx
import React, { Component } from 'react'
import './index.css'
export default class Item extends Component {
render() {
let {todo} = this.props
return (
<li>
<label>
<input type="checkbox" checked={todo.isDone} />
<span>{todo.title}</span>
</label>
<button className="btn btn-danger">删除</button>
</li>
)
}
}
src->components->Footer->Footer.jsx
import React, { Component } from 'react'
import './index.css'
export default class Footer extends Component {
render(){
// 接收todos
let {todos} = this.props
// 总条数
let total = todos.length;
// 已完成数量
let doneNum = todos.reduce((pre,item)=>{
// 使用强制类型转换
return pre + item.isDone
},0)
return(
<div className="todo-footer">
<label>
<input type="checkbox" />
</label>
<span>
<span>已完成 {doneNum}</span> / 全部 {total}
</span>
<button className="btn btn-danger">清除已完成任务</button>
</div>
)
}
}
添加任务
思路:
文本框输入内容,按下回车,将内容添加到列表中
1. 拆分步骤:
1. 获取文本框输入内容
2. 按下回车
3. 将内容添加到列表中
2. 给步骤匹配你能想到的技术点
1. 获取文本框输入内容
1. ref
2. 受控组件
3. e.target.value
2. 按下回车
1. 绑定事件-键盘事件 keyup keydown onKeyUp onKeyDown
3. 将内容添加到列表中
子传父(通过调用父亲自身的方法[this.props],传递参数从而修改父亲中的数据状态)
src->components->TodoList->TodoList.jsx
import React, { Component } from 'react'
import {nanoid} from 'nanoid'
import Footer from '../Footer/Footer'
import Header from '../Header/Header'
import Main from '../Main/Main'
import './index.css'
export default class TodoList extends Component {
state={
todos:[
{
id:nanoid(),
title:'吸烟',
isDone:true
},
{
id: nanoid(),
title: '喝酒',
isDone: true
},
{
id: nanoid(),
title: '烫头',
isDone: false
}
]
}
addTodo(title){
// 拼接一个新的todo
let todo = {
id:nanoid(),
title,
isDone:false
}
// 将todo加入到todos状态中
this.setState({
todos:[todo,...this.state.todos]
})
}
render() {
let { todos } = this.state
return (
<>
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo.bind(this)}/>
<Main todos={todos}/>
<Footer todos={todos} />
</div>
</div>
</>
)
}
}
src->components->Header->Header.jsx
import React, { Component } from 'react'
import './index.css'
export default class Header extends Component {
// 添加方法
sendTitle(e){
// 判断是否是回车键
if(e.key !== 'Enter') return;
const title = e.target.value.trim();
this.props.addTodo(title)
// 清空文本框
e.target.value = ''
}
render() {
return (
<div className="todo-header">
<input type="text" onKeyUp={(event) => this.sendTitle(event)} placeholder="请输入你的任务名称,按回车键确认" />
</div>
)
}
}
删除任务
描述:点击删除按钮,删除列表中的当前行
2. 步骤:
1. 点击删除按钮
2. 删除该行
3. 技术点:
1. 点击删除按钮: onClick 传递 id
2. 删除该行: 子传父
数据在谁身上,方法就要定义在谁身上
src->components->Item->Item.jsx
import React, { Component } from 'react'
import './index.css'
export default class Item extends Component {
render() {
let {todo,deleteById} = this.props
return (
<li>
<label>
<input type="checkbox" checked={todo.isDone} />
<span>{todo.title}</span>
</label>
<button className="btn btn-danger" onClick={()=>deleteById(todo.id)}>删除</button>
</li>
)
}
}
src->components->Main->Main.jsx
import React, { Component } from 'react'
import Item from '../Item/Item'
import './index.css'
export default class Main extends Component {
render() {
let {todos,deleteById} = this.props
return (
<ul className="todo-main">
{todos.map(todo => (<Item key={todo.id} todo={todo} deleteById={deleteById} />))}
</ul>
)
}
}
src->components->TodoList->TodoList.jsx
import React, { Component } from 'react'
import {nanoid} from 'nanoid'
import Footer from '../Footer/Footer'
import Header from '../Header/Header'
import Main from '../Main/Main'
import './index.css'
export default class TodoList extends Component {
state={
todos:[
{
id:nanoid(),
title:'吸烟',
isDone:true
},
{
id: nanoid(),
title: '喝酒',
isDone: true
},
{
id: nanoid(),
title: '烫头',
isDone: false
}
]
}
addTodo(title){
// 拼接一个新的todo
let todo = {
id:nanoid(),
title,
isDone:false
}
// 将todo加入到todos状态中
this.setState({
todos:[todo,...this.state.todos]
})
}
deleteById(id) {
if (!window.confirm('确定删除么?')) return;
console.log('father id: ', id);
//
this.setState({
todos: this.state.todos.filter(todo => todo.id !== id)
})
}
render() {
let { todos } = this.state
return (
<>
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo.bind(this)}/>
<Main todos={todos} deleteById={this.deleteById.bind(this)} />
<Footer todos={todos} />
</div>
</div>
</>
)
}
}
勾选完成
1. 点击复选框,改变完成状态
2. 步骤:
1. 点击复选框[复选框发生变化,触发一个事件]
2. 改变该条的完成状态
3. 技术:
1. 点击复选框[复选框发生变化,触发一个事件] onChange 不是onClick事件
2. 改变该条的完成状态, 子传父
src->components->Item->Item.jsx
import React, { Component } from 'react'
import './index.css'
export default class Item extends Component {
render() {
let { todo, deleteById, checkOne } = this.props
return (
<li>
<label>
<input type="checkbox" checked={todo.isDone} onChange={() => checkOne(todo.id)}/>
<span>{todo.title}</span>
</label>
<button className="btn btn-danger" onClick={()=>deleteById(todo.id)}>删除</button>
</li>
)
}
}
src->components->Main->Item.jsx
import React, { Component } from 'react'
import Item from '../Item/Item'
import './index.css'
export default class Main extends Component {
render() {
let { todos, deleteById, checkOne } = this.props
return (
<ul className="todo-main">
{todos.map(todo => (<Item key={todo.id} todo={todo} deleteById={deleteById} checkOne={checkOne} />))}
</ul>
)
}
}
src->components->todoList->Item.jsx
import React, { Component } from 'react'
import {nanoid} from 'nanoid'
import Footer from '../Footer/Footer'
import Header from '../Header/Header'
import Main from '../Main/Main'
import './index.css'
export default class TodoList extends Component {
state={
todos:[
{
id:nanoid(),
title:'吸烟',
isDone:true
},
{
id: nanoid(),
title: '喝酒',
isDone: true
},
{
id: nanoid(),
title: '烫头',
isDone: false
}
]
}
addTodo(title){
// 拼接一个新的todo
let todo = {
id:nanoid(),
title,
isDone:false
}
// 将todo加入到todos状态中
this.setState({
todos:[todo,...this.state.todos]
})
}
deleteById(id) {
if (!window.confirm('确定删除么?')) return;
console.log('father id: ', id);
//
this.setState({
todos: this.state.todos.filter(todo => todo.id !== id)
})
}
checkOne(id){
this.setState({
todos: this.state.todos.map(todo => {
if (todo.id === id) {
todo.isDone = !todo.isDone;
}
return todo;
})
})
}
checkAll(isDone){
this.setState({
todos:this.state.todos.map(todo=>{
todo.isDone = isDone
return todo
})
})
}
render() {
let { todos } = this.state
return (
<>
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo.bind(this)}/>
<Main todos={todos} deleteById={this.deleteById.bind(this)} checkOne={(id)=>this.checkOne(id)} />
<Footer todos={todos} checkAll={this.checkAll.bind(this)}/>
</div>
</div>
</>
)
}
}
src->components->Footer->Footer.jsx
import React, { Component } from 'react'
import './index.css'
export default class Footer extends Component {
// changeToAll
changeToAll(e) {
const isChecked = e.target.checked
this.props.checkAll(isChecked)
}
render(){
// 接收todos
let {todos} = this.props
// 总条数
let total = todos.length;
// 已完成数量
let doneNum = todos.reduce((pre,item)=>{
// 使用强制类型转换
return pre + item.isDone
},0)
return(
<div className="todo-footer">
<label>
<input type="checkbox" checked={doneNum === total && total > 0} onChange={this.changeToAll.bind(this)}/>
</label>
<span>
<span>已完成 {doneNum}</span> / 全部 {total}
</span>
<button className="btn btn-danger">清除已完成任务</button>
</div>
)
}
}
删除已完成
src->components->todoList->todoList.jsx
添加一个方法:把isDone的todo过滤调
然后把这个方法传给子组件
deleteDone(){
if (!window.confirm('确定删除已完成么?')) return;
this.setState({
todos: this.state.todos.filter(todo => !todo.isDone)
})
}
render() {
let { todos } = this.state
return (
<>
<Footer deleteDone={this.deleteDone.bind(this)} />
</>
)
}
src->components->Footer->Footer.jsx
子组件接收,然后调用方法
import React, { Component } from 'react'
import './index.css'
export default class Footer extends Component {
// changeToAll
changeToAll(e) {
const isChecked = e.target.checked
this.props.checkAll(isChecked)
}
render(){
// 接收todos
let { todos, deleteDone } = this.props
// 总条数
let total = todos.length;
// 已完成数量
let doneNum = todos.reduce((pre,item)=>{
// 使用强制类型转换
return pre + item.isDone
},0)
return(
<div className="todo-footer">
<label>
<input type="checkbox" checked={doneNum === total && total > 0} onChange={this.changeToAll.bind(this)}/>
</label>
<span>
<span>已完成 {doneNum}</span> / 全部 {total}
</span>
<button className="btn btn-danger" onClick={deleteDone}>清除已完成任务</button>
</div>
)
}
}
本地缓存(完结)
1. 存储:localStorage.setItem(key,value)
2. 读数据: localStorage.getItem(key)
3. 删除: localStorage.removeItem(key)
4. 全部删除: localStorage.clear()
src->components->todoList->todoList.jsx
import React, { Component } from 'react'
import {nanoid} from 'nanoid'
import Footer from '../Footer/Footer'
import Header from '../Header/Header'
import Main from '../Main/Main'
import './index.css'
export default class TodoList extends Component {
state={
// 初始数据为一个空数组
todos:[]
}
// 组件挂载完成后
componentDidMount(){
// 从本地存储读取数据
const stringTodos = localStorage.getItem('todos')
// json字符串转为对象
const objToDos = JSON.parse(stringTodos) || []
// 设置数据状态
this.setState({
todos: objToDos
})
}
// 当组件更新时
componentDidUpdate(){
// 把todos对象转为json字符串
const stringTodos = JSON.stringify(this.state.todos)
// 存储数据到localstorage
localStorage.setItem('todos', stringTodos)
}
addTodo(title){
// 拼接一个新的todo
let todo = {
id:nanoid(),
title,
isDone:false
}
// 将todo加入到todos状态中
this.setState({
todos:[todo,...this.state.todos]
})
}
deleteById(id) {
if (!window.confirm('确定删除么?')) return;
console.log('father id: ', id);
//
this.setState({
todos: this.state.todos.filter(todo => todo.id !== id)
})
}
checkOne(id){
this.setState({
todos: this.state.todos.map(todo => {
if (todo.id === id) {
todo.isDone = !todo.isDone;
}
return todo;
})
})
}
checkAll(isDone){
this.setState({
todos:this.state.todos.map(todo=>{
todo.isDone = isDone
return todo
})
})
}
deleteDone(){
if (!window.confirm('确定删除已完成么?')) return;
this.setState({
todos: this.state.todos.filter(todo => !todo.isDone)
})
}
render() {
let { todos } = this.state
return (
<>
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo.bind(this)}/>
<Main todos={todos} deleteById={this.deleteById.bind(this)} checkOne={(id)=>this.checkOne(id)} />
<Footer todos={todos} checkAll={this.checkAll.bind(this)} deleteDone={this.deleteDone.bind(this)} />
</div>
</div>
</>
)
}
}
src->components->Main->Main.jsx
import React, { Component } from 'react'
import Item from '../Item/Item'
import './index.css'
export default class Main extends Component {
render() {
let { todos, deleteById, checkOne } = this.props
return (
<ul className="todo-main">
{todos.map(todo => (<Item key={todo.id} todo={todo} deleteById={deleteById} checkOne={checkOne} />))}
</ul>
)
}
}
src->components->Header->Header.jsx
import React, { Component } from 'react'
import './index.css'
export default class Header extends Component {
// 添加方法
sendTitle(e){
// 判断是否是回车键
if(e.key !== 'Enter') return;
const title = e.target.value.trim();
this.props.addTodo(title)
// 清空文本框
e.target.value = ''
}
render() {
return (
<div className="todo-header">
<input type="text" onKeyUp={(event) => this.sendTitle(event)} placeholder="请输入你的任务名称,按回车键确认" />
</div>
)
}
}
src->components->Footer->Footer.jsx
import React, { Component } from 'react'
import './index.css'
export default class Footer extends Component {
// changeToAll
changeToAll(e) {
const isChecked = e.target.checked
this.props.checkAll(isChecked)
}
render(){
// 接收todos
let { todos, deleteDone } = this.props
// 总条数
let total = todos.length;
// 已完成数量
let doneNum = todos.reduce((pre,item)=>{
// 使用强制类型转换
return pre + item.isDone
},0)
return(
<div className="todo-footer">
<label>
<input type="checkbox" checked={doneNum === total && total > 0} onChange={this.changeToAll.bind(this)}/>
</label>
<span>
<span>已完成 {doneNum}</span> / 全部 {total}
</span>
<button className="btn btn-danger" onClick={deleteDone}>清除已完成任务</button>
</div>
)
}
}
src->components->Item->Item.jsx
import React, { Component } from 'react'
import './index.css'
export default class Item extends Component {
render() {
let { todo, deleteById, checkOne } = this.props
return (
<li>
<label>
<input type="checkbox" checked={todo.isDone} onChange={() => checkOne(todo.id)}/>
<span>{todo.title}</span>
</label>
<button className="btn btn-danger" onClick={()=>deleteById(todo.id)}>删除</button>
</li>
)
}
}
TodoList函数组件实现
入口文件:src->index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
// 导入重置样式表
import './index.css'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
样式重置文件:src->index.css
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
App主组件:src->App.jsx
import React from 'react'
import TodoList from './components/TodoList/TodoList'
export default function App(){
return(
<div>
<TodoList/>
</div>
)
}
TodoList子组件:src->components->TodoList->TodoList.jsx
import React, { useEffect, useRef, useState } from 'react'
import { nanoid } from 'nanoid'
import Footer from '../Footer/Footer'
import Header from '../Header/Header'
import Main from '../Main/Main'
import './index.css'
export default function TodoList(){
// 定义标记Ref,设置初始值为true
const flagRef = useRef(true)
// 状态数据定义
let [todos,setTodos] = useState([])
// 添加todo
function addTodo(mission){
// 创建一个对象
let todo = {
id: nanoid(),
mission,
isDone: false
}
// 把这对象添加到数组中:使用setTodos,把新值赋值给旧值替换
setTodos([todo,...todos])
}
// 删除todo
function delTodo(id){
if (!window.confirm('确定要删除么?')) return;
// 使用filter方法返回一个新数组,把剩下来的 赋值给Todos
setTodos(todos.filter(todo=>id !== todo.id))
}
// 修改checked状态
function checkOne(id){
setTodos(todos.map(todo=>{
if (todo.id === id){
todo.isDone = !todo.isDone
}
return todo
} ))
}
// 全选状态
function checkAll(isDone){
setTodos(todos.map(todo=>{
todo.isDone = isDone
return todo
}))
}
// 清除已完成
// 将isDone是true删掉 == > 将isDone是false的保留
function delDoneMission(){
if (!window.confirm('确定要删除完成的项目么?')) return
setTodos(todos.filter(todo=>!todo.isDone))
}
// 组件挂载完成后执行
useEffect(()=>{
// componentDisMount,挂载完成后从localStorage中获取数据
let todosJson = localStorage.getItem('todos')
let todos = JSON.parse(todosJson) || []
// 设置todos
setTodos(todos)
},[])
// 组件更新完成后执行
useEffect(()=>{
if (flagRef.current){
flagRef.current = false
return
}
// 更新本地存储数据
localStorage.setItem('todos',JSON.stringify(todos))
},[todos])
return(
<>
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={addTodo}/>
<Main todos={todos} delTodo={delTodo} checkOne={checkOne} />
<Footer todos={todos} delDoneMission={delDoneMission} checkAll={checkAll}/>
</div>
</div>
</>
)
}
Main子组件:src->components->Main->Main.jsx
import React from 'react'
import Item from '../Item/Item'
import './index.css'
export default function Main({ todos, delTodo,checkOne }){
return(
<ul className="todo-main">
{todos.map(todo => (
<Item key={todo.id} todo={todo} delTodo={delTodo} checkOne={checkOne} />
))}
</ul>
)
}
Item子组件:src->components->Item->Item.jsx
import React from 'react'
import './index.css'
export default function Item({ todo, delTodo,checkOne }){
return (
<li>
<label>
<input type="checkbox" checked={todo.isDone} onChange={()=>checkOne(todo.id)}/>
<span>{todo.mission}</span>
</label>
<button className="btn btn-danger" onClick={()=>{delTodo(todo.id)}}>删除</button>
</li>
)
}
Header子组件:src->components->Header->Header.jsx
import React from 'react'
import './index.css'
export default function Header ({addTodo}){
function addMission(e){
// 1. 判断是否按的是回车
// 2. 获取用户输入内容
// 3. 调用方法,传递数据给父组件
if(e.key !== 'Enter') return;
let mission = e.target.value.trim()
addTodo(mission)
// 清空文本框
e.target.value = ''
}
return (
<div className="todo-header">
<input type="text" onKeyUp={addMission} placeholder="请输入你的任务名称,按回车键确认" />
</div>
)
}
Footer子组件:src->components->Footer->Footer.jsx
import React, { Component } from 'react'
import './index.css'
export default function Footer({todos,delDoneMission,checkAll}){
let total = todos.length;
let doneNum = todos.reduce((pre,item)=>{return pre + item.isDone},0)
return (
<div className="todo-footer">
<label>
<input type="checkbox" checked={doneNum === total && total > 0} onChange={(e)=>checkAll(e.target.checked)} />
</label>
<span>
<span>已完成{doneNum}</span> / 全部{total}
</span>
<button className="btn btn-danger" onClick={delDoneMission}>清除已完成任务</button>
</div>
)
}