React路由学习笔记---小黑是小白

1 路由组件

1 前言

1 React路由模式分为两种:

hashHistory:

 http://localhost:8080/#/login

browserHistory

http://localhost:8080/login

browserHistory的好处大于hashHistory, 但是麻烦的地方就是,browserHistory路由模式,需要服务器的配置:

请求 http://localhost:8080/login 上的资源的时候,服务器会默认搜索当前目录下的login文件夹里的资源。但是logIn这个目录其实是不存在的,往往在刷新浏览器的时候,会404Not fund;

所以需要 nginx 里面conf文件的nginx.conf配置文件,try_files 去指定一个 fall back 资源;

2 nginx 配置

location / {
    # browserHistory模式 404问题
    #访问任何URL地址,都转发到 /index.html;
​    try_files $uri  /index.html;
​    index index.html;
​    autoindex on;
​    gzip on;
​    add_header Access-Control-Allow-Origin '*';
​    add_header Access-Control-Allow-Methods 'GET, POST, PUT, OPTIONS';
​    add_header Access-Control-Expose-Headers 'Accept-Ranges, Content-Encoding, Content-Length, Content-Range';
  }

autoindex on; 开启这个,输入到/ 会直接定向到index.html;

try_files 主要解决的是,如果在一些目录下找不到 index.html, 会最终有一个保底资源的路径就是 /index.html;

2 示例

.page-header{
    height: 50px;
    line-height: 50px;
    text-indent: 20px;
    border: 1px solid gray;
    font-size:24px ;
}

.list-group-item{
    display: block;
    height: 50px;
    line-height: 50px;
    margin:0 auto;
    border:1px solid gray;
    font-size: 20px;
    font-weight: bold;
}
.action{
    background-color: orange;
}
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import{BrowserRouter}from 'react-router-dom'

const root=ReactDOM.createRoot(document.getElementById('root'))
root.render(
  // <React.StrictMode>
  <BrowserRouter>
   <App />
  </BrowserRouter>
   
//   </React.StrictMode>,
 )
export default root
import React from "react";
import { Col,Row } from "antd";
import'./App.css'
import { NavLink, Navigate, Route, Routes } from "react-router-dom";
import About from "./page/About";
import Home from "./page/Home";
const style = {
  background: '#f0f5ff',
  border:'1px solid gray'
};
function App() {
  //isActive是一个对象,用于判断链接是否被点击,用来绑定已激活链接的样式
  function computedClassName(isActive) {
    console.log(isActive);
    return isActive.isActive?'list-group-item  action':'list-group-item'
  }
  return (
    <>
      <Row gutter={16}>
        <Col className="gutter-row" span={10} offset={2}>
          <div style={style} className="page-header">React-Router-Dom</div>
        </Col>
      </Row>

      <Row gutter={16}>
        <Col className="gutter-row" span={3} offset={2} >
          <div style={style} className="list-group">
            {/* 路由链接 */}
            <NavLink href="" className={computedClassName} to='/about'>About</NavLink>
            <NavLink href="" className={computedClassName} to='/home'>Home</NavLink>
          </div>
        </Col>

        <Col className="gutter-row" span={7}>
          <div style={style} className="panel">
           {/* 注册路由,路由链接点击时,进行路由匹配,匹配成功就停止 */}
           <Routes>
            <Route path="/about" element={<About/>}/>
            {/* 如果不加caseSensitive={true},是不区分大小写的,path="/HOME"也能匹配上 */}
            <Route path="/home" element={<Home/>} caseSensitive={true}/>
            {/*Navigate 组件是一个特殊的路由链接组件,只要一渲染,就会跳到指定路由  */}
            <Route path="/" element={<Navigate to="/about" />}/>
           </Routes>
          </div>
        </Col>
      </Row>
    </>
  );
}
export default App;

import React, { useState } from 'react';
import { Navigate } from 'react-router-dom';
function Home() {
    const [count,setCount]=useState(0)
    function add(params) {
        setCount( count=>count+1)
    }
    return ( 
        <>
         <h3>我是Home的内容</h3>
         {/*Navigate 组件是一个特殊的路由链接组件,只要一渲染,就会跳到指定路由  */}
         {/* replace={true}时跳转后不留回退,直接替换原页面 */}
         {count==2?<Navigate to='/about' replace={true}/>:<h2>{count}</h2>}
         <button onClick={add}>点击计数变成2时跳转</button>
        </>
     );
}

export default Home;
import React, { useState } from 'react';
function About() {
    return ( 
    <>
     <h3>我是About的内容</h3>
    </>
     );
}

export default About;

2 路由表及嵌套

1 路由表的使用

.page-header{
    height: 50px;
    line-height: 50px;
    text-indent: 20px;
    border: 1px solid gray;
    font-size:24px ;
}

.list-group-item{
    display: block;
    height: 50px;
    line-height: 50px;
    margin:0 auto;
    border:1px solid gray;
    font-size: 20px;
    font-weight: bold;
}
.action{
    background-color: orange;
}
li{
    text-decoration: none;
    list-style: none;
    display: inline-block;
}
ul>li>.list-group-item{
    display: block;
}
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import{BrowserRouter}from 'react-router-dom'

const root=ReactDOM.createRoot(document.getElementById('root'))
root.render(
  // <React.StrictMode>
  <BrowserRouter>
   <App />
  </BrowserRouter>
   
//   </React.StrictMode>,
 )
export default root

1 app

import React from "react";
import { Col,Row } from "antd";
import'./App.css'
import { NavLink, useRoutes } from "react-router-dom";
import router from'./routes/routes'


const style = {
  background: '#f0f5ff',
  border:'1px solid gray'
};

function App() {
  //isActive是一个对象,用于判断链接是否被点击,用来绑定已激活链接的样式
  function computedClassName(isActive) {
    console.log(isActive);
    return isActive.isActive?'list-group-item  action':'list-group-item'
  }

    //!!!!重要
  const element=useRoutes(router)


  return (
    <>
      <Row gutter={16}>
        <Col className="gutter-row" span={10} offset={2}>
          <div style={style} className="page-header">React-Router-Dom</div>
        </Col>
      </Row>

      <Row gutter={16}>
        <Col className="gutter-row" span={3} offset={2} >
          <div style={style} className="list-group">
            {/* 路由链接 */}
            <NavLink href="" className={computedClassName} to='/about'>About</NavLink>
            <NavLink href="" className={computedClassName} to='/home'>Home</NavLink>
          </div>
        </Col>

        <Col className="gutter-row" span={7}>
          <div style={style} className="panel">
           {/* 注册路由,路由链接点击时,进行路由匹配,匹配成功就停止 */}
          
            {element}
           
          </div>
        </Col>
      </Row>
    </>
  );
}
export default App;


2 路由表

import About from "../page/About";
import Home from "../page/Home";
import News from "../page/news";
import Message from "../page/message";
import { Navigate } from "react-router-dom";
export default  [
    {
        path:'/about',
        element:<About/>,
        children:[
            {
                path:'new',
                element:<News/>
            },
            {
                path:'message',
                element:<Message/>
            }
        ]
    },
    {
        path:'/home',
        element:<Home/>
    },
    {
        path:'/',
        element:<Navigate to="/about" />
    }
]

子路由

import React, { useState } from 'react';
import { Navigate } from 'react-router-dom';
function Home() {
    const [count,setCount]=useState(0)
    function add(params) {
        setCount( count=>count+1)
    }
    return ( 
        <>
         <h3>我是Home的内容</h3>
         {/*Navigate 组件是一个特殊的路由链接组件,只要一渲染,就会跳到指定路由  */}
         {/* replace={true}时跳转后不留回退,直接替换原页面 */}
         {count==2?<Navigate to='/about' replace={true}/>:<h2>{count}</h2>}
         <button onClick={add}>点击计数变成2时跳转</button>
        </>
     );
}

export default Home;

4 子路由和嵌套路由

import React, { useState } from 'react';
import { NavLink, Outlet } from 'react-router-dom';
function About() {
 //isActive是一个对象,用于判断链接是否被点击,用来绑定已激活链接的样式
 function computedClassName(isActive) {
    console.log(isActive);
    return isActive.isActive?'list-group-item  action':'list-group-item'
  }

    return ( 
    <>
     <ul>
        {/* to="new"等于to="./new"等于to="/about/new" */}
        <li><NavLink to="new" className={computedClassName}>News</NavLink></li>
        {/* end当子路由组件激活时,父路由组件失去高亮 */}
        <li><NavLink to="message"className={computedClassName } end>message</NavLink></li>
     </ul>
     <hr />
     <div>
           {/* 嵌套路由的插槽*/} 
       <Outlet/>
     </div>
    </>
     );
}

export default About;
import React, { useState } from 'react';
function News() {
    return ( 
    <>
    <h3>111</h3>
    </> );
}

export default News;
import React, { useState } from 'react';
function Message() {
    return ( 
    <>
    <h3>222</h3>
    </> );
}

export default Message;

3 路由传值

1.路由的三个传参方式

1.params,参数靠 / 号分隔,路由表中要提前设置号占位
2.search
3.state

2 公共部分

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import{BrowserRouter}from 'react-router-dom'

const root=ReactDOM.createRoot(document.getElementById('root'))
root.render(
  // <React.StrictMode>
  <BrowserRouter>
   <App />
  </BrowserRouter>
   
//   </React.StrictMode>,
 )
export default root

一级路由

import React from "react";
import { Col,Row } from "antd";
import'./App.css'
import { NavLink, useRoutes } from "react-router-dom";
import router from'./routes/routes'


const style = {
  background: '#f0f5ff',
  border:'1px solid gray'
};

function App() {
  //isActive是一个对象,用于判断链接是否被点击,用来绑定已激活链接的样式
  function computedClassName(isActive) {
    console.log(isActive);
    return isActive.isActive?'list-group-item  action':'list-group-item'
  }

  const element=useRoutes(router)


  return (
    <>
      <Row gutter={16}>
        <Col className="gutter-row" span={10} offset={2}>
          <div style={style} className="page-header">React-Router-Dom</div>
        </Col>
      </Row>

      <Row gutter={16}>
        <Col className="gutter-row" span={3} offset={2} >
          <div style={style} className="list-group">
            {/* 路由链接 */}
            <NavLink href="" className={computedClassName} to='/about'>About</NavLink>
            <NavLink href="" className={computedClassName} to='/home'>Home</NavLink>
          </div>
        </Col>

        <Col className="gutter-row" span={7}>
          <div style={style} className="panel">
           {/* 注册路由,路由链接点击时,进行路由匹配,匹配成功就停止 */}
          
            {element}
           
          </div>
        </Col>
      </Row>
    </>
  );
}
export default App;

二级路由

import React, { useState } from 'react';
import { Navigate } from 'react-router-dom';
function Home() {
    const [count,setCount]=useState(0)
    function add(params) {
        setCount( count=>count+1)
    }
    return ( 
        <>
         <h3>我是Home的内容</h3>
         {/*Navigate 组件是一个特殊的路由链接组件,只要一渲染,就会跳到指定路由  */}
         {/* replace={true}时跳转后不留回退,直接替换原页面 */}
         {count==2?<Navigate to='/about' replace={true}/>:<h2>{count}</h2>}
         <button onClick={add}>点击计数变成2时跳转</button>
        </>
     );
}

export default Home;
import React, { useState } from 'react';
import { NavLink, Outlet } from 'react-router-dom';
function About() {
 //isActive是一个对象,用于判断链接是否被点击,用来绑定已激活链接的样式
 function computedClassName(isActive) {
    console.log(isActive);
    return isActive.isActive?'list-group-item  action':'list-group-item'
  }

    return ( 
    <>
     <ul>
        {/* to="new"等于to="./new"等于to="/about/new" */}
        <li><NavLink to="new" className={computedClassName}>News</NavLink></li>
        {/* end当子路由组件激活时,父路由组件失去高亮 */}
        <li><NavLink to="message"className={computedClassName } end>message</NavLink></li>
     </ul>
     <hr />
     <div>
       <Outlet/>
     </div>
    </>
     );
}

export default About;

1.useParams()

import React, { useState } from 'react';
import {Link, Outlet}from 'react-router-dom'
function Message() {
    const [message]=useState([
        {id:'001',title:'消息1',content:'12345'},
        {id:'002',title:'消息2',content:'67890'}
    ])
    return ( 
    <>
   {message.map((m) => {
    return (
        <li key={m.id}>
               {/*参数靠 / 号分隔*/}
            <Link to={`detail/${m.id}/${m.title}/${m.content}`}>{m.title}</Link>
        </li>
    )
   })}
   <hr />
   <Outlet/>
    </> );
}

export default Message;

最终显示

import React, { useState } from 'react';
import { useMatch, useParams } from 'react-router-dom';
function Detail() {
    //调用useParams获得从前端路由传来的Params参数
    const {id,title,content}=useParams()

//useMatch中有详细的信息,不过一般不用,要加地址
    const a=useMatch('/about/message/detail/:id/:title/:content')
    console.log(a);

    return ( 
    <>
    <ul>
    <li>消息编号:{id}</li><br />
    <li>消息标题:{title}</li><br />
    <li>消息内容: {content}</li>
    </ul>
    </> );

}

export default Detail;

路由表

import About from "../page/About";
import Home from "../page/Home";
import News from "../page/news";
import Message from "../page/message";
import { Navigate } from "react-router-dom";
import Detail from "../page/Detail";
export default  [
    {
        path:'/about',
        element:<About/>,
        children:[
            {
                path:'new',
                element:<News/>
            },
            {
                path:'message',
                element:<Message/>, 
                children:[
                    {
                        //路由参数占位
                        path:'detail/:id/:title/:content',
                        element:<Detail/>,
                    }
                ]
            }
        ]
    },
    {
        path:'/home',
        element:<Home/>
    },
    {
        path:'/',
        element:<Navigate to="/about" />
    }
]

2.useSearch()

import React, { useState } from 'react';
import {Link, Outlet}from 'react-router-dom'
function Message() {
    const [message]=useState([
        {id:'001',title:'消息1',content:'12345'},
        {id:'002',title:'消息2',content:'67890'}
    ])
    return ( 
    <>
   {message.map((m) => {
    return (
        <li key={m.id}>
            <!--虽然useSearch路由表中不用路由参数占位,但是传值参数要用键值对的方式放在?后面,用$连接-->
            <Link to={`detail?id=${m.id}&title=${m.title}&content=${m.content}`}>{m.title}</Link>
        </li>
    )
   })}
   <hr />
   <Outlet/>
    </> );
}

export default Message;
import React, { useState } from 'react';
import { useMatch, useSearchParams } from 'react-router-dom';
function Detail() {
    //调用useSearchParams获得从前端路由传来的search参数
    //useSearchParams的使用类似useState
    const [search,setSearch]=useSearchParams()

    //useuseLocation中有详细的信息,不过一般不用,不用加地址
    const a=useLocation()
    console.log(a);
    
    //通过search.get('id')获得search里的值
    const id= search.get('id')
    const title= search.get('title')
    const content= search.get('content')
    
    return ( 
    <>
    <ul>
    <li>消息编号:{id}</li><br />
    <li>消息标题:{title}</li><br />
    <li>消息内容: {content}</li><br />
        {/*可以通过setSearch修改Search的值*/}     
    <li><button onClick={() => {setSearch('id=999&title=消息9&content=99999')}}>点击更新</button></li>
    </ul>
    </> );

}

export default Detail;
import About from "../page/About";
import Home from "../page/Home";
import News from "../page/news";
import Message from "../page/message";
import { Navigate } from "react-router-dom";
import Detail from "../page/Detail";
export default  [
    {
        path:'/about',
        element:<About/>,
        children:[
            {
                path:'new',
                element:<News/>
            },
            {
                path:'message',
                element:<Message/>, 
                children:[
                    {
                        path:'detail',
                        //路由表中不用路由参数占位
                        element:<Detail/>,
                    }
                ]
            }
        ]
    },
    {
        path:'/home',
        element:<Home/>
    },
    {
        path:'/',
        element:<Navigate to="/about" />
    }
]

2.state和useLocation()

import React, { useState } from 'react';
import {Link, Outlet}from 'react-router-dom'
function Message() {
    const [message]=useState([
        {id:'001',title:'消息1',content:'12345'},
        {id:'002',title:'消息2',content:'67890'}
    ])
    return ( 
    <>
   {message.map((m) => {
    return (
        <li key={m.id}>
              {/*可以通过state对象传值*/}     
            <Link to='detail' state={{id:m.id,title:m.title,content:m.content}}>{m.title}</Link>
        </li>
    )
   })}
   <hr />
   <Outlet/>
    </> );
}

export default Message;
import React from 'react';
import { useLocation, } from 'react-router-dom';
function Detail() {
     //state只能用useuseLocation接收值,对useuseLocation进行双重结构赋值
   const {state:{id,title,content}}=useLocation()
    console.log(id);
    return ( 
    <>
    <ul>
    <li>消息编号:{id}</li><br />
    <li>消息标题:{title}</li><br />
    <li>消息内容: {content}</li><br />
    <li><button>点击更新</button></li>
    </ul>
    </> );

}

export default Detail;
import About from "../page/About";
import Home from "../page/Home";
import News from "../page/news";
import Message from "../page/message";
import { Navigate } from "react-router-dom";
import Detail from "../page/Detail";
export default  [
    {
        path:'/about',
        element:<About/>,
        children:[
            {
                path:'new',
                element:<News/>
            },
            {
                path:'message',
                element:<Message/>, 
                children:[
                    {
                        path:'detail',
                        element:<Detail/>,
                    }
                ]
            }
        ]
    },
    {
        path:'/home',
        element:<Home/>
    },
    {
        path:'/',
        element:<Navigate to="/about" />
    }
]

4 函数式组件的useNavigate

1.编程式路由导航

1 借助useNavigate钩子实现编程式路由跳转

import { useNavigate } from "react-router-dom";
const navigate = useNavigate();
// 跳转
function showDetail(message) {
    navigate("detail", {
      replace: false, // 本身false就是默认值
      state: {
        id: message.id,
        title: message.id,
        content: message.content,
      }
    });
  }

2 前进后退

import React from "react";
import { useNavigate } from "react-router-dom";
export default function Header() {
  const navigate = useNavigate();
  function back(){
    navigate(-1)
  }
  function forward(){
    navigate(1)
  }
  return (
    <div>
      <h2>React Router Demo</h2>
      <button onClick={back}>后退</button>
      <button onClick={forward}>前进</button>
    </div>
  );

5 一些不常用的路由Hook

1 useInRouterContext

判断当前路由是否处于路由上下文中,即当前组件是否包含在BrowserRouter或者HashRouter的标签中,一般来讲都是直接在App标签外包一层Router类标签,所以没啥用,除非引入了第三方组件库等情况。

2 useNavigationType

返回当前的导航类型,即用户是如何来到当前页面的,会返回POP、PUSH、REPLACE中的一个,POP指的是直接在浏览器中直接打开了这个路由组件(或刷新页面)。

3 useOutlet

用来呈现当前组件中渲染的嵌套路由,如果嵌套路由没有挂载,则result为null,如果嵌套路由已经挂载则展示嵌套的路由对象。

4 useResolvedPath

useResolvedPath是React Router v6中的一个hook函数,它的作用是获取当前路由的解析路径。在React Router v6中,路由的路径不再是字符串,而是一个路径对象(Path Object)。该对象包含了路由路径信息,包括路由路径字符串、路由参数、查询参数等。

在某些情况下,你可能需要获取当前路由的解析路径,例如需要在代码中读取路由参数等。使用useResolvedPath可以方便地获取当前路由的解析路径对象,并且可以通过解析路径对象访问路由参数、查询参数等信息。

例如,假设你有一个路由路径为 /user/:userId,当用户访问 /user/123 时,你需要获取路由参数 userId 的值,可以使用以下代码:

import { useResolvedPath } from 'react-router-dom';

function MyComponent() {
  const { params } = useResolvedPath('/user/123');
  const userId = params.userId;
  // do something with userId
}

在这个例子中,我们使用useResolvedPath获取了路径为/user/123的路由路径对象,并通过路由路径对象的params属性获取了路由参数userId的值。

需要注意的是,useResolvedPath需要传入一个路径字符串作为参数,而不是路由组件本身。因此,在使用useResolvedPath时需要注意路径字符串的格式,以确保获取到正确的路由路径对象。


6 学习记录器的例子

HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React</title>
  </head>
  <body>
    <div id="root"></div>
    <div id="backdrop"></div>

    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

UI组件

盒子卡片UI

import React, { useState } from "react";

function Card(props) {
  return (
    <div
      style={{
        borderRadius: "10px",
        boxShadow: "0 0 10px rgb(145, 145, 145)",
      }}
      className={`${props.className}`}
    >
      {props.children}
    </div>
  );
}

export default Card;

提示框遮罩UI

import React, { useState } from "react";
import  ReactDOM  from "react-dom";

const backdropRoot=document.getElementById('backdrop')

function Backdrop(props) {
  return ReactDOM.createPortal(
         (
        <div className="backdrop">
          {props.children}
        </div>
        ),backdropRoot

  )
}

export default Backdrop;

APP根组件

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import {Provider}from 'react-redux'
import store from "./redux/formStore.js"


const root=ReactDOM.createRoot(document.getElementById('root'))
root.render(
  // <React.StrictMode>
  <Provider store={store}>
    <App />
  </Provider>
  
   
//   </React.StrictMode>,
 )
export default root
import React from "react";
import Logs from "./component/logs";
import "./App.css";
import Logsform from "./component/logsform";

function App() {
  return (
    <div className="app">
      <Logsform />
      <Logs />
    </div>
  );
}
export default App;

组件

输入表单组件

import React, { useRef, useState } from "react";
import { connect } from "react-redux";
import Card from "./card";
import { addLog } from "../redux/formSlice";

function Logsform(props) {
  const dateRef = useRef();
  const descRef = useRef();
  const timeRef = useRef();

  function addLog(event) {
    event.preventDefault();
    const form = {
      date: dateRef.current.value,
      desc: descRef.current.value,
      time: timeRef.current.value,
    };
    props.add(form);
  }

  return (
    <>
      <Card className="logs-form">
        <form onSubmit={addLog}>
          <div className="form-item">
            <label htmlFor="date">日期</label>
            <input type="date" name="" id="date" ref={dateRef} />
          </div>
          <div className="form-item">
            <label htmlFor="desc">内容</label>
            <input type="text" name="" id="desc" ref={descRef} />
          </div>
          <div className="form-item">
            <label htmlFor="time">时长</label>
            <input type="number" name="" id="time" ref={timeRef} />
          </div>
          <div className="form-button">
            <button>添加</button>
          </div>
        </form>
      </Card>
    </>
  );
}

export default connect(
  (state) => ({ form: state.formData.form }),
  (dispatch) => ({
    add: (form) => dispatch(addLog(form)),
  })
)(Logsform);

记录日志展示组件

import React, { useState } from "react";
import { connect } from "react-redux";
import Item from "./item";
import Card from "./card";

function Logs(props) {
  const { form } = props;
  return (
    <Card className="logs">
      {form.length !== 0 ? (
        form.map((item) => {
          return (
            <Item
              key={Math.random()}
              date={new Date(item.date)}
              desc={item.desc}
              time={item.time}
            />
          );
        })
      ) : (
        <h2>暂无学习记录</h2>
      )}
    </Card>
  );
}

export default connect((state) => ({ form: state.formData.form }))(Logs);

各项日志组件

import React, { useRef, useState } from "react";
import Card from "./card";
import ConfirmModel from "./Confirm";
import { connect } from "react-redux";
import { show } from "../redux/confirmSlice";
import { delForm } from "../redux/formSlice";
function Item(props) {
  const myDesc = useRef();
  const del = () => {
    props.show(true);
    props.delform(myDesc.current.innerHTML);
  };
  return (
    <Card className="item">
      {props.isShow && <ConfirmModel />}
      <Card className="data">
        {/* toLocaleString('zh-CN',{month:'long'})本地日期格式 */}
        <div className="month">
          {props.date.toLocaleString("zh-CN", { month: "long" })}
        </div>
        <div className="day">{props.date.getDate()}</div>
      </Card>
      <div className="content">
        <h2 className="desc" ref={myDesc}>
          {props.desc}
        </h2>
        <div className="time">{props.time}</div>
      </div>
      {/* 删除按钮 */}
      <div>
        <div className="delete" onClick={del}>
          x
        </div>
      </div>
    </Card>
  );
}
export default connect(
  (state) => ({
    form: state.formData.form,
    isShow: state.confirmData.showConfirm,
    isDel: state.confirmData.isDel,
  }),
  //向UI组件的props中传入方法,使UI组件能读取方法
  (dispatch) => ({
    delform: (isShow) => dispatch(delForm(isShow)),
    show: (isShow) => dispatch(show(isShow)),
  })
)(Item);

自定义提示框组件

import React, { useState, useEffect } from "react";
import { connect } from "react-redux";
import Card from "./card";
import Backdrop from "./backdrop";
import { noDel } from "../redux/confirmSlice";
import { delLog } from "../redux/formSlice";

function ConfirmModel(props) {
  const del = () => {
    props.delItem(props.willdel);
  };
  const nodel = () => {
    props.noDel();
  };
  return (
    <Backdrop>
      <Card className="confirmModel">
        <div>
          <p>确定删除?</p>
        </div>
        <div>
          <button onClick={del}>确定</button>
          <button onClick={nodel}>取消</button>
        </div>
      </Card>
    </Backdrop>
  );
}

export default connect(
  (state) => ({
    isDel: state.confirmData.isDel,
    willdel: state.formData.willdelForm,
  }),
  (dispatch) => ({
    delItem: (desc) => dispatch(delLog(desc)),
    noDel: (isShow) => dispatch(noDel(isShow)),
  })
)(ConfirmModel);

redux

Store

import { combineReducers, configureStore } from "@reduxjs/toolkit";
import formSlice from "./formSlice";
import logsSlice from "./logsSlice";
import confirmSlice from "./confirmSlice";

const rootReducer = combineReducers({
  formData: formSlice.reducer,
  logsData: logsSlice.reducer,
  confirmData: confirmSlice.reducer,
});
const store = configureStore({
  reducer: rootReducer,

  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false,
    }),
});
export default store;

import { createSlice } from "@reduxjs/toolkit";

const formSlice = createSlice({
  name: "form",
  //存储状态,使UI组件状态初始化
  initialState: {
    form: [],
    willdelForm: "",
  },

  //存储方法,使UI组件能够调用方法
  reducers: {
    // 将新记录添加到状态对象中
    addLog(State, action) {
      State.form = [...State.form, action.payload];
    },

    // 将记录从状态对象中删除
    delLog(State, action) {
      const newState = State.form.filter((todoObj) => {
        console.log(action.payload);
        return todoObj.desc !== action.payload;
      });
      State.form = newState;
      console.log(State.form);
    },

    delForm(State, action) {
      State.willdelForm = action.payload;
    },
  },
});
export const { addLog, delLog, delForm } = formSlice.actions;
export default formSlice;

import { createSlice } from "@reduxjs/toolkit";

const logsSlice = createSlice({
  name: "logs",
  //存储状态,使UI组件状态初始化
  initialState: {
    logs: [{ date: new Date(2021, 3, 1), desc: "123", time: 20 }],
  },
});
export default logsSlice;

import { createSlice } from "@reduxjs/toolkit";

const confirmSlice = createSlice({
  name: "confirm",
  //存储状态,使UI组件状态初始化
  initialState: {
    showConfirm: false,
    isDel: false,
  },

  //存储方法,使UI组件能够调用方法
  reducers: {
    show(State, action) {
      State.showConfirm = action.payload;
    },

    noDel(State, action) {
      State.showConfirm = false;
    },
  },
});
export const { show, isDel, noDel } = confirmSlice.actions;
export default confirmSlice;


7 点餐界面的例子

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React</title>
  </head>
  <body>
    <div id="root"></div>
    <div id="backdropRoot"></div>
    <div id="checkoutRoot"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'

document.documentElement.style.fontSize=100/750+'vw';

const root=ReactDOM.createRoot(document.getElementById('root'))
root.render(
  // <React.StrictMode>
    <App />
//   </React.StrictMode>,
 )
export default root

app.jsx

import React, {useState} from "react";
import "./App.css";
import Meals from "./component/meals/meals";
import CartContext from "./store/CartUsecontext";
import FilterMeals from "./component/filterMeals/filterMeals";
import Cart from "./component/cart/cart.jsx";


const MEALS_DATA = [
    {
        id: 1,
        title: "汉堡包",
        desc: "100%纯牛肉!以少许盐和胡椒调味,搭配爽脆酸黄瓜和洋葱粒,添加美味番茄酱,打造出经典好滋味。",
        price: 12,
        img: "/img/0.jpg",
        amount: 0,
    },
    {
        id: 2,
        title: "鸡肉汉堡包",
        desc: "100%纯鸡肉!以少许盐和胡椒调味,搭配爽脆酸黄瓜和洋葱粒,添加美味番茄酱,打造出经典好滋味。",
        price: 12,
        img: "/img/0.jpg",
        amount: 0,
    },
    {
        id: 3,
        title: "汉堡包",
        desc: "100%纯牛肉!以少许盐和胡椒调味,搭配爽脆酸黄瓜和洋葱粒,添加美味番茄酱,打造出经典好滋味。",
        price: 12,
        img: "/img/0.jpg",
        amount: 0,
    },
    {
        id: 4,
        title: "鸡肉汉堡包",
        desc: "100%纯鸡肉!以少许盐和胡椒调味,搭配爽脆酸黄瓜和洋葱粒,添加美味番茄酱,打造出经典好滋味。",
        price: 12,
        img: "/img/0.jpg",
        amount: 0,
    },
    {
        id: 5,
        title: "汉堡包",
        desc: "100%纯牛肉!以少许盐和胡椒调味,搭配爽脆酸黄瓜和洋葱粒,添加美味番茄酱,打造出经典好滋味。",
        price: 12,
        img: "/img/0.jpg",
        amount: 0,
    },
    {
        id: 6,
        title: "鸡肉汉堡包",
        desc: "100%纯鸡肉!以少许盐和胡椒调味,搭配爽脆酸黄瓜和洋葱粒,添加美味番茄酱,打造出经典好滋味。",
        price: 12,
        img: "/img/0.jpg",
        amount: 0,
    },
    {
        id: 7,
        title: "汉堡包",
        desc: "100%纯牛肉!以少许盐和胡椒调味,搭配爽脆酸黄瓜和洋葱粒,添加美味番茄酱,打造出经典好滋味。",
        price: 12,
        img: "/img/0.jpg",
        amount: 0,
    },
    {
        id: 8,
        title: "汉堡包",
        desc: "100%纯牛肉!以少许盐和胡椒调味,搭配爽脆酸黄瓜和洋葱粒,添加美味番茄酱,打造出经典好滋味。",
        price: 12,
        img: "/img/0.jpg",
        amount: 0,
    },
    {
        id: 9,
        title: "汉堡包",
        desc: "100%纯牛肉!以少许盐和胡椒调味,搭配爽脆酸黄瓜和洋葱粒,添加美味番茄酱,打造出经典好滋味。",
        price: 12,
        img: "/img/0.jpg",
        amount: 0,
    },
    {
        id: 10,
        title: "汉堡包",
        desc: "100%纯牛肉!以少许盐和胡椒调味,搭配爽脆酸黄瓜和洋葱粒,添加美味番茄酱,打造出经典好滋味。",
        price: 12,
        img: "/img/0.jpg",
        amount: 0,
    },
];

function App() {
    const [mealsData, setMealsData] = useState(MEALS_DATA);

    const [cartData, setCartData] = useState({
        item: [],
        totalAmount: 0,
        totalPrice: 0,
    });

    // 增加购物商品的方法
    const addMealHandle = (meal) => {
        const newCart = {...cartData};

        // 判断购物车是否有这件物品,如果有则加一显示
        if (newCart.item.indexOf(meal) == -1) {
            newCart.item.push(meal);
            meal.amount = 1;
        } else {
            meal.amount += 1;
        }
        // 购物车总件数加一
        newCart.totalAmount += 1;
        // 总价格增加
        newCart.totalPrice += meal.price;
        setCartData(newCart);
    };

    // 减少商品的方法
    const SubMealHandle = (meal) => {
        const newCart = {...cartData};
        // 商品单件数减一
        meal.amount -= 1;
        if (meal.amount === 0) {
            newCart.item.splice(newCart.item.indexOf(meal), 1);
        }
        // 购物车总件数减一
        newCart.totalAmount -= 1;
        // 总价格减少
        newCart.totalPrice -= meal.price;
        setCartData(newCart);
    };

    // 清除购物车的方法
    const clearCart = () => {
        const newCart = {...cartData};
        newCart.item.forEach(item=>delete item.amount)
        newCart.item = [];
        newCart.totalAmount = 0;
        newCart.totalPrice = 0;
        setCartData(newCart)
    }
    const filterHandle = (keyword) => {
        const newMealsData = MEALS_DATA.filter(
            (item) => item.title.indexOf(keyword) !== -1
        );
        setMealsData(newMealsData);
    };

    return (
        <CartContext.Provider value={{...cartData, addMealHandle, SubMealHandle,clearCart}}>
            <div className="app" style={{width: "750rem"}}>
                <FilterMeals onFilter={filterHandle}/>
                <Meals mealsData={mealsData}/>
                <Cart/>
            </div>
        </CartContext.Provider>
    );
}

export default App;

*{
    box-sizing: border-box;
}
body{
    margin: 0;
}
img{
    vertical-align:bottom;
}

filterMeals.jsx

import React, {useEffect, useState} from "react";
import {SearchOutlined} from "@ant-design/icons";
import classes from "./filterMeals.module.css";

function FilterMeals(props) {
    const [keyword, setkeyword] = useState('')

    
    // 监听输入框,每打一个字开启延时器,同时关闭上一个延时器
    // 上个延时器的关闭,其中的回调也随之销毁,最后只会返回最终的输出
    useEffect(() => {
        const timer=setTimeout(()=>{
        props.onFilter(keyword);
        },1000);
        return ()=>{
        clearTimeout(timer)
        }
    }, [keyword])



    const inputChangeHandle = (event) => {
        // trim过滤空格
        setkeyword(event.target.value.trim())
    };


    return (
        <div className={classes.filterMeals}>
            <div className={classes.InputOuter}>
                <input
                    type="text"
                    className={classes.SearchInput}
                    placeholder="请输入关键字"
                    onChange={inputChangeHandle}
                />
                <SearchOutlined className={classes.SearchIcon}/>
            </div>
        </div>
    );
}

export default FilterMeals;

.filterMeals{
    display: flex;
    align-items: center;
    justify-content: center;
    position:fixed;
    height: 100rem;
    background: rgb(255, 255, 255);
    left: 0;
    right: 0;
    z-index: 999;
}
.SearchInput{
    background: #e2e2e2;
    outline: none;
    border: none;
    width: 650rem;
    height: 70rem;
    border-radius: 14px;
    text-indent: 2em;
}
.InputOuter{
    position: relative;
    display: flex;
    align-items: center;
}
.SearchIcon{
    color: darkgray;
    font-size: 40rem;
    position: absolute;
    left: 10rem;
   
}

meals.jsx

import React, {useState} from 'react';
import Meal from './meal/meal';
import classes from './meals.module.css'

function Meals(props) {
    const {mealsData} = props
    return (
        <div className={classes.meals}>
            {mealsData.map((item) => {
                return <Meal key={item.id} meal={item}/>
            })}
        </div>
    );
}

export default Meals;
.meals{
    /* 将滚动条设置给了meals */
    position: absolute;
    top: 100rem;
    bottom: 0;
    background: rgb(248, 248, 248);
    overflow: auto;
}

meal.jsx

import React, {useState} from "react";
import classes from "./meal.module.css";
import Counter from "../../UI/counter";

// 食物信息组件
function Meal(props) {
    const {meal} = props;
    return (
        <div className={classes.meal}>
            <div className={classes.ImgBox}>
                <img src={meal.img} alt=""/>
            </div>
            <div>
                <h2 className={classes.title}>{meal.title}</h2>
                {props.noDesc?null:<p className={classes.desc}>{meal.desc}</p>}
                <div className={classes.priceWrap}>
                    <span className={classes.price}>{meal.price}</span>
                    <Counter meal={meal}/>
                </div>
            </div>
        </div>
    );
}

export default Meal;

.meal {
    display: flex;
    padding: 20rem;
    border-bottom: 1px #cbc0c0 solid;
    align-items: center;
}

.ImgBox {
    width: 280rem;
}

img {
    width: 100%;
}

.title {
    font-weight: normal;
    font-size: 36rem;
    margin: 0;
}

.desc {
    color: #9d9d9d;
    font-size: 24rem;
}

.priceWrap {
    padding-right: 40rem;
    display: flex;
    margin-top: 40rem;
    justify-content: space-between;
}

.price {
    font-weight: bold;
    font-size: 40rem;
}

/* 添加货币符号 */
.price::before {
    font-size: 24rem;
    content: '¥';
    font-weight: bold;
}

cart.jsx

import React, {useContext, useEffect, useState} from 'react';
import classes from './cart.module.css'
import iconImg from '../../assets/react.svg'
import CartContext from "../../store/CartUsecontext.js";
import CartDetails from "../cartDetails/cartDetails.jsx";
import Checkout from "../Checkout/Checkout.jsx";

function Cart(props) {
    const ctx=useContext(CartContext)
    const [showDetails,setshowDetails]=useState(false)
    const [showCheckout,setshowCheckout]=useState(false)
    useEffect(()=>{
    if (ctx.totalAmount==0){
        setshowDetails(false)
    }
    },[ctx])
    // 点击购物车详情界面
    const showDetailsHandle=()=> {
        if (ctx.totalAmount===0)return ;
        setshowDetails(prevState => !prevState)
    }
    // 点击结账界面
    const showCheckoutHandle=()=> {
       if (ctx.totalAmount===0)return ;
        setshowCheckout(true)
    }
    // 点击关闭结账界面
    const hiddenCheckoutHandle=()=> {
        setshowCheckout(false)
    }


    return (
        <div className={classes.Cart} onClick={showDetailsHandle}>

            {/*点击触发同时商品数不为零则时显示购物车详情界面*/}
            {(showDetails&&ctx.totalAmount!==0)&&<CartDetails/>}
            {/*点击触发时显示结账界面*/}
            {showCheckout&&<Checkout hiddenCheckoutHandle={hiddenCheckoutHandle}/>}
            <div className={classes.Icon}>
                <img src={iconImg} alt=""/>
                <span className={classes.TotalAmount}>{ctx.totalAmount}</span>
            </div>
            <p className={classes.Price}>{ctx.totalPrice}</p>
            <button className={classes.Button} onClick={showCheckoutHandle}>去结算</button>
        </div>
    );
}

export default Cart;
.Cart {
    display: flex;
    position: fixed;
    justify-content: space-between;
    bottom: 30rem;
    width: 700rem;
    height: 80rem;
    border-radius: 20px;
    background: #4d4d4d;
    left: 0;
    right: 0;
    margin: auto;
    z-index: 999;
}

.Icon {
    width: 80rem;
    position: absolute;
    bottom: 0;
}

.Icon img {
    width: 100%;
}

.TotalAmount {
    position: absolute;
    width: 36rem;
    height: 36rem;
    line-height: 36rem;
    background: #f00;
    border-radius: 50%;
    color: white;
    text-align: center;
    font-weight: bold;
    font-size: 22rem;
}

.Price {
    color: #fff;
    margin-left: 120rem;
    display: flex;
    align-items: center;
    font-weight: bold;
    font-size: 36rem;
}

.Price:before {
    content: '¥';
    font-size: 24rem;
}
.Button{
    border: none;
    background-color: #dc7b1a;
    width: 160rem;
    border-radius: 20px;
    font-size: 36rem;
    color: aliceblue;
}

cartDetails.jsx

import React, {useContext, useState} from 'react';
import BackDrop from "../UI/backDrop.jsx";
import {DeleteOutlined} from "@ant-design/icons";
import classes from './cartDetails.module.css'
import CartContext from "../../store/CartUsecontext.js";
import Meal from "../meals/meal/meal.jsx";
import Confirm from "../UI/confirm.jsx";
function CartDetails(props) {
    const ctx=useContext(CartContext)
    const [showConfirm,setshowConfirm]=useState(false)
    const showConfirmhandle=()=>{
    setshowConfirm(true)
    }
    return (
        <BackDrop>
            {showConfirm&&<Confirm/>}
            <div className={classes.CartDetails} onClick={event => event.stopPropagation()}>
                <header>
                    <h2>餐品详情</h2>
                    <div onClick={showConfirmhandle}><DeleteOutlined />清空购物车</div>
                </header>

                <div className={classes.mealList}>
                    {ctx.item.map(item=><Meal noDesc key={item.id} meal={item}/>)}
                </div>
            </div>
        </BackDrop>
    );
}

export default CartDetails;
.CartDetails {
    background: white;
    position: absolute;
    display: flex;
    flex-flow: column;
    bottom: 0;
    width: 750rem;
    max-height: 1200rem;
    padding-bottom: 120rem;
    border-top-right-radius: 20px;
    border-bottom-left-radius: 20px;
}

header{
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 30rem 20rem;
}
header>div{
    color: #868686;
    font-size: 24rem;
}
.mealList{
    overflow: auto;
    background: #dadada;
}

Checkout.jsx

import React from 'react';
import ReactDOM from "react-dom";
import classes from './Checkout.module.css'
import {CloseOutlined} from "@ant-design/icons";

const checkoutRoot = document.getElementById('checkoutRoot')

function Checkout(props) {
    return ReactDOM.createPortal(
        <div className={classes.Checkout}>
            <div className={classes.close}>
                <CloseOutlined onClick={()=>props.hiddenCheckoutHandle()}/>
            </div>
        </div>, checkoutRoot
    );
}

export default Checkout;
.Checkout {
    position: fixed;
    padding: 20rem;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 9999;
    background: #b7b7b7;
}
.close{
    color: #4d4d4d;
    font-size: 36rem;

}

UI

backDrop

import React from 'react';
import classes from './backDrop.module.css'
import ReactDOM from "react-dom";

const backdropRoot=document.getElementById('backdropRoot')
function BackDrop(props) {
    return ReactDOM.createPortal(
        <div className={`${classes.Backdrop} ${props.className}`}>
            {props.children}
        </div>,backdropRoot
    );
}

export default BackDrop;
.Backdrop {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    background: rgba(147, 147, 147, 0.3);
    z-index: 998;
}

counter

import React, { useContext, useState } from "react";
import classes from "./counter.module.css";
import {PlusOutlined,MinusOutlined }from '@ant-design/icons'
import CartContext from "../../store/CartUsecontext";

function Counter(props) {

const ctx=useContext(CartContext)

const addButtonHandle=() => {
 
ctx.addMealHandle(props.meal)
}

const subButtonHandle=() => {
 
ctx.SubMealHandle(props.meal)
}

  return (
    <div className={classes.Counter}>
        {/* 判断是否有数量,如果没有或则为0,则减少符号和数量就不显示 */}
      {props.meal.amount && props.meal.amount !== 0 ? (
        // 这里也是要有父元素包裹的
        <>
          <button className={classes.sub} onClick={subButtonHandle}><MinusOutlined /></button>
          <span className={classes.count}>{props.meal.amount}</span>
        </>
      ) : null}

      <button className={classes.add} onClick={addButtonHandle} ><PlusOutlined /></button>
    </div>
  );
}

export default Counter;
 
.Counter{
display: flex;
align-items: center;
}
.sub,
.add {
    display: flex;
    justify-content: center;
    align-items: center;
    border: none;
    background: #fcbf49;
    width: 36rem;
    height: 36rem;
    border-radius: 50%;
    padding: 0;
    font-size: 28rem;
}

.count {
    font-size: 36rem;
    margin: 0 5px;
}

confirm

import React, {useContext, useState} from 'react';
import { Button, Modal } from 'antd';
import BackDrop from "./backDrop.jsx";
import classes from'./confirm.module.css'
import CartContext from "../../store/CartUsecontext.js";
const Confirm = () => {
    const ctx=useContext(CartContext)
    const [isModalOpen, setIsModalOpen] = useState(true);

    const handleOk = () => {
        ctx.clearCart()
        console.log(1)
    };
    const handleCancel = () => {
        setIsModalOpen(false);

    };
    return (
            <Modal title="Basic Modal"  open={isModalOpen} onOk={handleOk} onCancel={handleCancel} className={classes.modal}>
                <p>确定清除吗?</p>
            </Modal>
    );
};
export default Confirm;
.modal{
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    margin: auto;
}

Store-createContext

import React from "react";
 const CartContext=React.createContext(
    {
        item:[],
        totalAmount:0,
        totalPrice:0,
        SubMealHandle:() => {},
        addMealHandle:() => {},
    }
 )
 export default CartContext

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
React Router是React的一种路由管理工具,它允许我们在应用程序建立路由,并通过不同的URL路径来加载不同的页面。 而react-router-config是React Router的一个附加库,它提供了一种以配置方式来定义应用程序路由的方法。 路由切入动画是指在切换页面时,为页面添加一些过渡效果和动画,以提升用户体验。 使用react-router-config实现路由切入动画的步骤如下: 1. 首先,在路由配置文件定义各个页面的路由信息,并设置对应的组件。 2. 在路由配置文件,为每个路由定义一个transition属性,用于标识该路由的过渡效果。 3. 在根组件使用React Router提供的Switch组件来包裹所有路由,并使用TransitionGroup组件来包裹Switch组件。 4. 在根组件使用自定义的AnimatedSwitch组件来替换React Router提供的Switch组件,并将路由配置文件传递给AnimatedSwitch组件。 5. 在AnimatedSwitch组件根据当前路由的transition属性,为切换的页面添加不同的过渡效果和动画。 例如,可以定义一个FadeIn动画效果,在路由配置文件为需要应用该动画效果的路由设置transition属性为'fade-in',然后在AnimatedSwitch组件根据该属性为页面添加相应的CSS动画样式。 总而言之,使用react-router-config可以方便地配置应用程序的路由信息,并结合一些CSS动画库,可以实现各种炫酷的路由切入动画。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值