Skip to content

开发思路

jsx
:new: 组件化的开发思路:

6-1. 静态布局:组件拆分

6-2. 首屏数据渲染:

- 数据结构分析:
- 分析数据定义在谁身上

6-3. 用户交互功能实现

1. 描述:干了什么-》什么结果
2. 拆分步骤:
3. 给步骤匹配技术点

导包规范

shell
把第三方的包放在自定义的上边

轮播图练习

🆕 主页面文件:src->App.js

jsx
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

jsx
1.jpeg
2.jpeg
3.jpeg

子组件:src->components->Class->Child.jsx

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) }}>&lt;</span>
                    <span className="rightBtn" onClick={this.changePicAdd.bind(this,index,imgArr)}>&gt;</span>
                </div>
            </>
        )
    }
}

子组件样式:src->components->Class->Child.css

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样式处理

jsx
第三方的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'
jsx
重置样式:
位置:src/index.css
引入:在入口文件 src/index.js 通过 import 导入
jsx
全局通用样式:
位置:src/App.css
引入:在主App页面中 src/App.jsx 通过 import 导入
jsx
组件内样式:
位置:组件目录的css文件中
导入:组件中使用import 导入

BootStrap警告

再导入bootstrap.css后会在控制台提示bootstrap.map这个警告,可以在bootstrap.css文件的末尾删除最后一行注释即可。

Css模块化

shell
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>
            </>
        )
    }
}

图片处理

jsx
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中配置

js
...
"eslintConfig": {
        "extends": [
            "react-app",
            "react-app/jest"
        ],
        "rules": {
            "xxx":0
        }
    },
...

然后重启项目

TodoList 类组件

静态页面拆分

jsx
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 的样式拆分到各个组件中

数据结构分析

jsx
let todos = [
    {
        id:xxx,
        title:'吃饭',
        isDone:true | false
    }
]

分析数据应该定义在哪个组件身上

shell
原则:
1. 如果数据只在一个组件身上使用,那么就当以在该组件自己身上
2. 如果数据在多个组件都需要使用,那么定义在他们共同的父级身上[也叫做状态提升]

实现思路

jsx
实现用户交互功能

思路生成步骤:描述你要做什么,产生了什么结果

1. 文本框输入内容,按下回车,将内容添加到列表中
2. 拆分步骤:
	2.1 获取文本框输入内容
    2.2 按下回车
    2.3 将内容添加到列表中
3. 给步骤匹配你能想到的技术点
	3.1 获取文本框输入内容
    	ref
        受控组件
        e.target.value
    3.2 按下回车
    	绑定事件-键盘事件 keyup keydown  onKeyUp  onKeyDown
    3.3 将内容添加到列表中
    	子传父

首屏渲染

shell
-- 使用nanoid包生成一个不重复的id
# 安装
npm i nanoid
# 导入
import {nanoid} from 'nanoid'
# 调用
nanoid()

-- 使用random方法生成一个不重复的id 36=10位数字加26位字母
Math.random().toString(36).slice(2)
js
首屏数据渲染实现步骤及技术点
1. 将todos 数据定义成 TodoList 组件的状态
2. 将 todos数据 通过标签属性的方式传递给 Main组件和 Footer组件
3. 在Main组件中接收todos,并使用map进行列表渲染,定义key值,并将遍历的每一项 todo传递给 Item组件
4. 在Footer组件中接收todos,计算total 和 doneNum后进行渲染

src->components->TodoList->TodoList.jsx

shell
1. 将todos 数据定义成 TodoList 组件的状态
2. todos数据 通过标签属性的方式传递给 Main组件和 Footer组件
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
            }
        ]
    }
    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

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

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

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>
        )
    }
}

添加任务

jsx
思路:
文本框输入内容,按下回车,将内容添加到列表中
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

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

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>
        )
    }
}

删除任务

jsx
描述:点击删除按钮,删除列表中的当前行
2. 步骤:
   1. 点击删除按钮
   2. 删除该行
3. 技术点:
   1. 点击删除按钮: onClick   传递 id
   2. 删除该行: 子传父
      数据在谁身上,方法就要定义在谁身上

src->components->Item->Item.jsx

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

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

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>
            </>
        )
    }
}

勾选完成

jsx
1. 点击复选框,改变完成状态
2. 步骤:
   1. 点击复选框[复选框发生变化,触发一个事件]
   2. 改变该条的完成状态
3. 技术:
   1. 点击复选框[复选框发生变化,触发一个事件]   onChange 不是onClick事件
   2. 改变该条的完成状态, 子传父

src->components->Item->Item.jsx

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

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

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

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

shell
添加一个方法:把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

shell
子组件接收,然后调用方法
shell
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>
        )
    }
}

本地缓存(完结)

jsx
1. 存储:localStorage.setItem(key,value)
2. 读数据: localStorage.getItem(key)
3. 删除: localStorage.removeItem(key)
4. 全部删除: localStorage.clear()

src->components->todoList->todoList.jsx

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

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

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

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

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

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

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

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

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

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

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

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

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>
    )
}