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