【React】pro-mobile
1.项目介绍
实现react移动端项目
2.目标:
- 能够应用CRA+React+Mobx+Antd-mobile开发C端项目
- 掌握基于React的C端项目开发流程
- 学会如何应用next优化项目
3.使用技术栈
-
脚手架:cra
- dva-cli
- umi
-
脚本:ts
-
react版本:react v18 2022年更新
- react 17
-
路由:react-router v6 2021年10-11月
- react-router v5
-
状态管理器:mobx v6
- redux
- redux + react-redux
- redux + react-redux + 分模块
- redux + react-redux + 分模块 + redux-thunk
- redux + react-redux + 分模块 + redux-saga
- redux + react-redux + 分模块 + redux-thunk + immutable + redux-immutable
- redux + react-redux + 分模块 + redux-saga + immutable + redux-immutable
- rtk
- mobx v6
-
组件库:antd-mobile v5
- http://ant-design-mobile.antgroup.com/zh
- 更像是vant UI库了
-
hooks
4.构建项目
$ npx create-react-app react-mobile-app --template typescript
4.1 是否抽离配置文件
一般企业级项目,很少会直接抽离配置文件
抽离配置文件目的:对webpack进行二次封装
推荐使用 craco 进行覆盖
4.2 使用craco覆盖webpack配置
https://www.npmjs.com/package/@craco/craco
$ cnpm i @craco/craco -D
为了支持 commonjs 规范,安装如下模块
$ cnpm i @types/node -D
@types/*
这种文件称之为 ts 中的声明文件(ts中的定义的类型的一个整合
)
项目根目录创建 craco.config.js
,代码如下:
const path = require('path')
module.exports = {webpack: {alias: {'@': path.resolve(__dirname, 'src')}}
}
为了使 TS 文件引入时的别名路径能够正常解析,需要配置 tsconifg.json
,在 compilerOptions
选项里添加 path 等属性。为了防止配置被覆盖,需要单独创建一个文件 tsconfig.path.json
,添加以下代码
// tsconfig.path.json
{"compilerOptions": {"baseUrl": ".","paths": {"@/*": ["./src/*"]},"types": ["node"]}
}
在 tsconifg.json
引入配置文件:
// /tsconfig.json
{"compilerOptions": {"target": "es5","lib": ["dom","dom.iterable","esnext"],"allowJs": true,"skipLibCheck": true,"esModuleInterop": true,"allowSyntheticDefaultImports": true,"strict": true,"forceConsistentCasingInFileNames": true,"noFallthroughCasesInSwitch": true,"module": "esnext","moduleResolution": "node","resolveJsonModule": true,"isolatedModules": true,"noEmit": true,"jsx": "react-jsx"},"extends": "./tsconfig.path.json", //+++"include": ["src"]
}
修改 package.json
如下:
"scripts": {"start": "craco start","build": "craco build","test": "craco test"
},
$ npm run start
4.3 确定项目 css 预处理器
https://create-react-app.bootcss.com/docs/adding-a-sass-stylesheet
$ cnpm i node-sass sass -D
cra 默认自带sass支持,只需要安装模块即可自动启动
4.4 改造项目目录结构
- mobile-react-app- src- api- components- router- store- utils- viewsApp.tsxindex.tsxlogo.svgreact-app-env.d.tsreportWebVitals.ts
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';import ErrorBoundary from './ErrorBundary';
import App from './App';
import reportWebVitals from './reportWebVitals';const root = ReactDOM.createRoot(document.getElementById('root') as HTMLDivElement
);
root.render(<React.StrictMode><ErrorBoundary><App /></ErrorBoundary></React.StrictMode>
);reportWebVitals();
// src/App.tsx
import React, { FC } from 'react';interface IAppProps {
}const App: FC<IAppProps> = (props) => {return (<>App</>)
}export default App
// src/ErrorBundary.tsx
import React from 'react'
// 如何给类组件添加类型注解
interface IState {hasError: boolean
}
class ErrorBoundary extends React.Component <any, IState> {
// class ErrorBoundary extends React.Component <{ children: any }, {hasError: boolean}> {constructor(props: any) {super(props);this.state = { hasError: false };}static getDerivedStateFromError(error: any) {// 更新 state 使下一次渲染可以显示降级 UIreturn { hasError: true };}componentDidCatch(error: any, info: { componentStack: any; }) {// "组件堆栈" 例子:// in ComponentThatThrows (created by App)// in ErrorBoundary (created by App)// in div (created by App)// in Appconsole.log(info.componentStack);}render() {if (this.state.hasError) {// 你可以渲染任何自定义的降级 UIreturn <h1>代码出错了,请仔细检查一下</h1>;}return this.props.children; }
}export default ErrorBoundary
5 构建项目基本结构
// src/App.scss
* {padding: 0;margin: 0;list-style: none;text-decoration: none;
}
html, body, #root, .container {height: 100%;
}html {font-size: 26.6666667vw; // 100/375 * 100
}
body {font-size: 12px;
}@media only screen and (orientation: landscape) { // 横屏html {font-size: 100px;}
}.container {display: flex;flex-direction: column;.box {flex: 1;overflow: auto;display: flex;flex-direction: column;.header {height: 0.44rem;background-color: #f66;}.content {flex: 1;overflow: auto;}}.footer {height: 0.5rem;background-color: #efefef;}
}
// src/App.tsx
import React, { FC } from 'react';
import './App.scss';
interface IAppProps {
}const App: FC<IAppProps> = (props) => {return (<div className="container"><div className="box"><header className="header"></header><div className="content"></div></div><footer className="footer"></footer></div>)
}export default App
6.构建项目基本页面
思考每个页面的头部和内容区域是根据用户的选择而一起改变的,那么可以创建以下四个基本页面
6.1 构建首页面
// src/views/home/Index.tsx
import React, { FC } from 'react';interface IHomeProps {
};const Home:FC<IHomeProps> = () => {return (<><header className="header">home header</header><div className="content">home content</div></>)
};export default Home;
6.2 构建分类页面
// src/views/kind/Index.tsx
import React, { FC } from 'react';interface IKindProps {};const Kind:FC<IKindProps> = () => {return (<><header className="header">kind header</header><div className="content">kind content</div></>)
};export default Kind;
6.3 构建购物车页面
// src/views/cart/Index.tsx
import React, { FC } from 'react';interface ICartProps {};const Cart:FC<ICartProps> = () => {return (<><header className="header">cart header</header><div className="content">cart content</div></>)
};export default Cart;
6.4 构建个人中心页面
// src/views/user/Index.tsx
import React, { FC } from 'react';interface IUserProps {};const User:FC<IUserProps> = () => {return (<><header className="header">user header</header><div className="content">user content</div></>)
};export default User;
6.引入路由
https://reactrouter.com/en/main
cnpm i react-router-dom -S
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';import { HashRouter } from 'react-router-dom'import ErrorBoundary from './ErrorBundary';
import App from './App';
import reportWebVitals from './reportWebVitals';const root = ReactDOM.createRoot(document.getElementById('root') as HTMLDivElement
);
root.render(<React.StrictMode><ErrorBoundary><HashRouter><App /></HashRouter></ErrorBoundary></React.StrictMode>
);reportWebVitals();
// src/App.tsx
import React, { FC } from 'react';import { Routes, Route, Navigate } from 'react-router-dom'import Home from '@/views/home/Index'
import Kind from '@/views/kind/Index'
import Cart from '@/views/cart/Index'
import User from '@/views/user/Index'
import NotFound from '@/views/error/404'import './App.scss';
interface IAppProps {
}const App: FC<IAppProps> = (props) => {return (<div className="container"><div className="box"><Routes><Route path="/" element={ <Navigate to="/home" />} /><Route path="/home" element={<Home />} /><Route path="/kind" element={<Kind />} /><Route path="/cart" element={<Cart />} /><Route path="/user" element={<User />} /><Route path="*" element={<NotFound />} /></Routes></div><footer className="footer"></footer></div>)
}export default App
此时地址栏分别输入
http://localhost:3000/home
、http://localhost:3000/kind
、http://localhost:3000/cart
、http://localhost:3000/user
查看项目运行结果,
可以得知已经可以通过路由显示不同的页面
但是用户一般都是通过底部选项卡来切换页面的
7.构建页面底部组件
在src
文件夹下创建components
文件夹,在components
文件夹下创建底部组件
因为底部选项卡需要字体图标,可以选择 iconfont阿里字体图标库,搜索图标,加入购物车,添加至项目mobile-vue-app
,选择font-class
,点击查看在线链接
,拷贝css链接
项目根目录下public/index.html
中引入css链接
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8" /><link rel="icon" href="%PUBLIC_URL%/favicon.ico" /><meta name="viewport" content="width=device-width, initial-scale=1" /><meta name="theme-color" content="#000000" /><metaname="description"content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /><!--manifest.json provides metadata used when your web app is installed on auser's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/--><link rel="manifest" href="%PUBLIC_URL%/manifest.json" /><!--Notice the use of %PUBLIC_URL% in the tags above.It will be replaced with the URL of the `public` folder during the build.Only files inside the `public` folder can be referenced from the HTML.Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" willwork correctly both with client-side routing and a non-root public URL.Learn how to configure a non-root public URL by running `npm run build`.--><title>React App</title><link rel="stylesheet" href="//at.alicdn.com/t/c/font_3665887_h3lsrioddkk.css">
</head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><!--This HTML file is a template.If you open it directly in the browser, you will see an empty page.You can add webfonts, meta tags, or analytics to this file.The build step will place the bundled scripts into the <body> tag.To begin the development, run `npm start` or `yarn start`.To create a production bundle, use `npm run build` or `yarn build`.--></body>
</html>
底部组件展示如下:
// src/components/Footer.tsx
import React, { FC } from 'react';interface IFooterProps {};const Footer:FC<IFooterProps> = () => {return (<ul><li><span className="iconfont icon-shouye"></span><p>首页</p></li><li><span className="iconfont icon-fenlei"></span><p>分类</p></li><li><span className="iconfont icon-gouwuche"></span><p>购物车</p></li><li><span className="iconfont icon-shouye1"></span><p>我的</p></li></ul>)
};export default Footer;
// src/App.scss
* {padding: 0;margin: 0;list-style: none;text-decoration: none;
}
html, body, #root, .container {height: 100%;
}html {font-size: 26.6666667vw; // 100/375 * 100
}
body {font-size: 12px;
}@media only screen and (orientation: landscape) { // 横屏html {font-size: 100px;}
}.container {display: flex;flex-direction: column;.box {flex: 1;overflow: auto;display: flex;flex-direction: column;.header {height: 0.44rem;background-color: #f66;}.content {flex: 1;overflow: auto;}}.footer {height: 0.5rem;background-color: #efefef;ul {width: 100%;height: 100%;display: flex;li {flex: 1;height: 100%;display: flex;flex-direction: column;justify-content: center;align-items: center;span {font-size: 0.24rem;}p {font-size: 0.12rem;}}}}
}
8.点击页面底部跳转路由
此项选择使用声明式导航跳转
react提供了两个可以使用 声明式导航跳转方式 : Link NavLink
如果不需要设置选中的样式,可以使用Link 组件
如果需要设置选中的样式,建议使用NavLink
// src/components/Footer.tsx
import React, { FC } from 'react';import { NavLink } from 'react-router-dom'interface IFooterProps {};const Footer:FC<IFooterProps> = () => {return (<ul><NavLink to="/home" style = {({ isActive }) => isActive ? { color: '#f66' }: undefined }><span className="iconfont icon-shouye"></span><p>首页</p></NavLink><NavLink to="/kind" style = {({ isActive }) => isActive ? { color: '#f66' }: undefined }><span className="iconfont icon-fenlei"></span><p>分类</p></NavLink><NavLink to="/cart" style = {({ isActive }) => isActive ? { color: '#f66' }: undefined }><span className="iconfont icon-gouwuche"></span><p>购物车</p></NavLink><NavLink to="/user" style = {({ isActive }) => isActive ? { color: '#f66' }: undefined }><span className="iconfont icon-shouye1"></span><p>我的</p></NavLink></ul>)
};export default Footer;
// src/App.scss
* {padding: 0;margin: 0;list-style: none;text-decoration: none;
}
html, body, #root, .container {height: 100%;
}html {font-size: 26.6666667vw; // 100/375 * 100
}
body {font-size: 12px;
}@media only screen and (orientation: landscape) { // 横屏html {font-size: 100px;}
}.container {display: flex;flex-direction: column;.box {flex: 1;overflow: auto;display: flex;flex-direction: column;.header {height: 0.44rem;background-color: #f66;}.content {flex: 1;overflow: auto;}}.footer {height: 0.5rem;background-color: #efefef;ul {width: 100%;height: 100%;display: flex;a {flex: 1;height: 100%;display: flex;flex-direction: column;justify-content: center;align-items: center;color: #333;span {font-size: 0.24rem;}p {font-size: 0.12rem;}}}}
}
9.引入UI组件库
http://ant-design-mobile.antgroup.com/zh
react 移动端项目建议使用 Ant Design Mobile
$ cnpm i antd-mobile -S
直接引入组件即可,antd-mobile 会自动为你加载 css 样式文件
10.封装数据请求
在vue/react项目中建议使用 axios
作为数据请求的方案
axios官网:http://www.axios-js.com/
$ cnpm i axios -S
// src/utils/request.ts
// 1.引入axios
import axios from 'axios'// 2.项目环境
// 生产环境 process.env.NODE_ENV === 'production' cnpm run build
// 测试环境 ?
// 开发环境 process.env.NODE_ENV === 'devlopment cnpm run start
const isDev = process.env.NODE_ENV === 'development'// 3.给axios添加默认选项
// axios.defaults.withCredentials = false // 设置跨域是否需要携带凭证
// axios.defaults.timeout = 6000 // 6秒超时时间
// axios.defaults.baseURL = isDev ? 'http://121.89.205.189:3000/api' : 'http://121.89.205.189:3000/api'// 4.自定义axios
const ins = axios.create({baseURL: isDev ? 'http://121.89.205.189:3000/api' : 'http://121.89.205.189:3000/api',timeout: 6000
})// 5.设置拦截器
// 请求的拦截器 所有的请求在开始之前先执行请求拦截器,再执行自己的请求
ins.interceptors.request.use((config) => {// 设置请求的loading显示 --- 使用组件不必要 ---- js模块显示// 设置token,一般token传递给后端通过 请求头传递 config.headers.token = ''return config
}, (err) => {return Promise.reject(err)
})// 响应拦截器 所有的接口返回值先执行响应拦截器,再返回自己的响应的数据
ins.interceptors.response.use((response) => {// 关闭loading动画 --- 使用组件不必要 ---- js模块隐藏// 验证token,如果验证通过,返回数,如果验证不通过,直接跳转到登录页面return response
}, (err) => Promise.reject(err))// 6.暴露自定义axios
export default ins
11.构建首页
11.1 封装首页相关数据请求
// src/api/home.ts
import request from '@/utils/request'interface IPager {count?: numberlimitNum?: number
}
// 轮播图数据
export function getBannerList () {return request.get('/banner/list')
}
// 秒杀列表数据
export function getSeckilllist (params?: IPager) {return request.get('/pro/seckilllist', { params })
}
// 产品列表数据
export function getProList (params?: IPager) {return request.get('/pro/list', { params })
}
11.2 构建首页轮播图组件以及渲染
// src/views/home/components/BannerComponent.tsx
import React, { FC } from 'react';interface IBannerComponentProps {};const BannerComponent:FC<IBannerComponentProps> = () => {return (<><h1>轮播图</h1></>)
};export default BannerComponent;
// src/views/home/Index.tsx
import React, { FC } from 'react';
import BannerComponent from './components/BannerComponent';interface IHomeProps {
};const Home:FC<IHomeProps> = () => {return (<><header className="header">home header</header><div className="content"><BannerComponent /></div></>)
};export default Home;
// src/views/home/Index.tsx
import { getBannerList } from '@/api/home';
import React, { FC, useEffect, useState } from 'react';
import BannerComponent from './components/BannerComponent';interface IHomeProps {
};
export interface IBanner {bannerid: stringimg: stringalt: stringlink: string
}
const Home:FC<IHomeProps> = () => {const [bannerList, setBannerList] = useState<IBanner[]>([])useEffect(() => {getBannerList().then(res => setBannerList(res.data.data))}, [])return (<><header className="header">home header</header><div className="content"><BannerComponent bannerList = { bannerList }/></div></>)
};export default Home;
// src/views/home/components/BannerComponent.tsx
import { Image, Swiper } from 'antd-mobile';
import React, { FC } from 'react';
import { IBanner } from '../Index';interface IBannerComponentProps {bannerList: IBanner[]
};const BannerComponent:FC<IBannerComponentProps> = ({ bannerList }) => {return (<><Swiper style={{ height: '1.5rem', overflow: 'hidden' }}>{bannerList.map((item) => (<Swiper.Item key={ item.bannerid }><Image style={ { width: '100%' } } src={ item.img } fit='cover'/></Swiper.Item>))}</Swiper></>)
};export default BannerComponent;
11.3 构建nav导航组件以及渲染
// src/utils/nav.ts
const navList = [{ navid: 1, title: '嗨购超市', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/125678/35/5947/4868/5efbf28cEbf04a25a/e2bcc411170524f0.png' },{ navid: 2, title: '数码电器', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/178015/31/13828/6862/60ec0c04Ee2fd63ac/ccf74d805a059a44.png' },{ navid: 3, title: '嗨购服饰', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/41867/2/15966/7116/60ec0e0dE9f50d596/758babcb4f911bf4.png' },{ navid: 4, title: '嗨购生鲜', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/177902/16/13776/5658/60ec0e71E801087f2/a0d5a68bf1461e6d.png' },{ navid: 5, title: '嗨购到家', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196472/7/12807/7127/60ec0ea3Efe11835b/37c65625d94cae75.png' },{ navid: 6, title: '充值缴费', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/185733/21/13527/6648/60ec0f31E0fea3e0a/d86d463521140bb6.png' },{ navid: 7, title: '9.9元拼', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/36069/14/16068/6465/60ec0f67E155f9488/595ff3e606a53f02.png' },{ navid: 8, title: '领券', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/186080/16/13681/8175/60ec0fcdE032af6cf/c5acd2f8454c40e1.png' },{ navid: 9, title: '领金贴', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196711/35/12751/6996/60ec1000E21b5bab4/38077313cb9eac4b.png' },{ navid: 10, title: 'plus会员', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/37709/6/15279/6118/60ec1046E4b5592c6/a7d6b66354efb141.png' }
]export default navList
// src/views/home/Index.tsx
import { getBannerList, getSeckilllist } from '@/api/home';
import React, { FC, useEffect, useState } from 'react';
import BannerComponent from './components/BannerComponent';
import SeckillComponent from './components/SeckillComponent';
import NavComponent from './components/NavComponent'
import navList from '@/utils/nav'interface IHomeProps {
};
export interface IBanner {bannerid: stringimg: stringalt: stringlink: string
}
export interface IPro {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const Home:FC<IHomeProps> = () => {const [bannerList, setBannerList] = useState<IBanner[]>([])const [seckillList, setSeckillList] = useState<IPro[]>([])useEffect(() => {getBannerList().then(res => setBannerList(res.data.data))getSeckilllist().then(res => setSeckillList(res.data.data))}, [])return (<><header className="header">home header</header><div className="content"><BannerComponent bannerList = { bannerList }/><NavComponent list = { navList } /><SeckillComponent list = { seckillList }/></div></>)
};export default Home;
// src/views/home/components/NavComponent.tsx
import { Grid, Image } from 'antd-mobile';
import React, { FC } from 'react';interface INavComponentProps {list: INav[]
};
export interface INav {navid: numbertitle: stringimgurl: string
}const NavComponent:FC<INavComponentProps> = ({ list }) => {return (<><Grid columns={5} gap={8}>{list.map(item => (<Grid.Item key = { item.navid }><Image src={ item.imgurl } style={{ width: 50, height: 50 }}/><p>{ item.title }</p></Grid.Item>))}</Grid></>)
};export default NavComponent;
11.4 构建秒杀列表实现
// src/views/home/components/SeckillComponent.tsx
import React, { FC } from 'react';interface ISeckillComponentProps {};const SeckillComponent:FC<ISeckillComponentProps> = () => {return (<><h1>秒杀列表</h1></>)
};export default SeckillComponent;
// src/views/home/Index.tsx
import { getBannerList, getSeckilllist } from '@/api/home';
import React, { FC, useEffect, useState } from 'react';
import BannerComponent from './components/BannerComponent';
import SeckillComponent from './components/SeckillComponent';
import NavComponent from './components/NavComponent'
import navList from '@/utils/nav'interface IHomeProps {
};
export interface IBanner {bannerid: stringimg: stringalt: stringlink: string
}
export interface IPro {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const Home:FC<IHomeProps> = () => {const [bannerList, setBannerList] = useState<IBanner[]>([])const [seckillList, setSeckillList] = useState<IPro[]>([])useEffect(() => {getBannerList().then(res => setBannerList(res.data.data))getSeckilllist().then(res => setSeckillList(res.data.data))}, [])return (<><header className="header">home header</header><div className="content"><BannerComponent bannerList = { bannerList }/><NavComponent list = { navList } /><SeckillComponent list = { seckillList }/></div></>)
};export default Home;
// src/views/home/components/SeckillComponent.tsx
import { Grid, Image } from 'antd-mobile';
import React, { FC } from 'react';
import { IPro } from '../Index';interface ISeckillComponentProps {list: IPro[]
};const SeckillComponent:FC<ISeckillComponentProps> = ({ list }) => {return (<><Grid columns={6} gap={8}>{ list.map(item => {return (<Grid.Item key = { item.proid }><Image src= { item.img1 } style={{width: 55, height: 55}}/><p style={{ color: '#f66', textAlign: 'center' }}>¥{ item.originprice }</p></Grid.Item>)})}</Grid></>)
};export default SeckillComponent;
11.5 构建产品列表
// src/views/home/ProComponent.tsx
import React, { FC } from 'react';
import { IPro } from '../Index';interface IProComponentProps {list: IPro[]
};const ProComponent:FC<IProComponentProps> = ({ list }) => {return (<><h1>产品列表</h1></>)
};export default ProComponent;
// src/views/home/Index.tsx
import { getBannerList, getProList, getSeckilllist } from '@/api/home';
import React, { FC, useEffect, useState } from 'react';
import BannerComponent from './components/BannerComponent';
import SeckillComponent from './components/SeckillComponent';
import NavComponent from './components/NavComponent'
import navList from '@/utils/nav'
import ProComponent from './components/ProComponent';interface IHomeProps {
};
export interface IBanner {bannerid: stringimg: stringalt: stringlink: string
}
export interface IPro {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const Home:FC<IHomeProps> = () => {const [bannerList, setBannerList] = useState<IBanner[]>([])const [seckillList, setSeckillList] = useState<IPro[]>([])const [proList, setProList] = useState<IPro[]>([])useEffect(() => {getBannerList().then(res => setBannerList(res.data.data))getSeckilllist().then(res => setSeckillList(res.data.data))getProList().then(res => setProList(res.data.data))}, [])return (<><header className="header">home header</header><div className="content"><BannerComponent bannerList = { bannerList }/><NavComponent list = { navList } /><SeckillComponent list = { seckillList }/><ProComponent list = { proList }></ProComponent></div></>)
};export default Home;
// src/views/home/ProComponent.tsx
import React, { FC, memo } from 'react';
import { Image } from 'antd-mobile'
import { IPro } from '../Index';interface IProComponentProps {list: IPro[]};const WaterfallItem = ({context}: any) => {console.log(context)return (<div className='item'><div className="itemImage"><Image src = {context.img1} /></div><div className="itemInfo"><div>{ context.proname }</div><div>¥{ context.originprice }</div></div></div>)
}
const ProComponent:FC<IProComponentProps> = memo(({ list }) => {console.log(list)return (<><ul style={{ float: 'left', width: '48%', margin: '5px 1%'}}>{list.map((item, index) => {return index % 2 === 0 ? <WaterfallItem context={item}/> : null})}</ul><ul style={{ float: 'left', width: '48%', margin: '5px 1%'}}>{list.map((item, index) => {return index % 2 === 1 ? <WaterfallItem context={item}/> : null})}</ul></>)
});export default ProComponent
11.6 实现上拉加载操作
http://ant-design-mobile.antgroup.com/zh/components/infinite-scroll
// src/views/home/Index.tsx
import { getBannerList, getProList, getSeckilllist } from '@/api/home';
import React, { FC, useEffect, useState } from 'react';
import BannerComponent from './components/BannerComponent';
import SeckillComponent from './components/SeckillComponent';
import NavComponent from './components/NavComponent'
import navList from '@/utils/nav'
import ProComponent from './components/ProComponent';
import { InfiniteScroll } from 'antd-mobile';interface IHomeProps {
};
export interface IBanner {bannerid: stringimg: stringalt: stringlink: string
}
export interface IPro {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const Home:FC<IHomeProps> = () => {const [bannerList, setBannerList] = useState<IBanner[]>([])const [seckillList, setSeckillList] = useState<IPro[]>([])const [proList, setProList] = useState<IPro[]>([])useEffect(() => {getBannerList().then(res => setBannerList(res.data.data))getSeckilllist().then(res => setSeckillList(res.data.data))getProList().then(res => setProList(res.data.data))}, [])const [hasMore, setHasMore] = useState(true)const [count, setCount] = useState(2)async function loadMore() {const res = await getProList({ count })setProList([...proList, ...res.data.data])setCount(count+1)setHasMore(res.data.data.length > 0)}return (<><header className="header">home header</header><div className="content"><BannerComponent bannerList = { bannerList }/><NavComponent list = { navList } /><SeckillComponent list = { seckillList }/><ProComponent list = { proList }></ProComponent><InfiniteScroll loadMore={loadMore} hasMore={hasMore} /></div></>)
};export default Home;
11.7 实现下拉刷新
http://ant-design-mobile.antgroup.com/zh/components/pull-to-refresh
// src/views/home/Index.tsx
import { getBannerList, getProList, getSeckilllist } from '@/api/home';
import React, { FC, useEffect, useState } from 'react';
import BannerComponent from './components/BannerComponent';
import SeckillComponent from './components/SeckillComponent';
import NavComponent from './components/NavComponent'
import navList from '@/utils/nav'
import ProComponent from './components/ProComponent';
import { InfiniteScroll, PullToRefresh } from 'antd-mobile';interface IHomeProps {
};
export interface IBanner {bannerid: stringimg: stringalt: stringlink: string
}
export interface IPro {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const Home:FC<IHomeProps> = () => {const [bannerList, setBannerList] = useState<IBanner[]>([])const [seckillList, setSeckillList] = useState<IPro[]>([])const [proList, setProList] = useState<IPro[]>([])useEffect(() => {getBannerList().then(res => setBannerList(res.data.data))getSeckilllist().then(res => setSeckillList(res.data.data))getProList().then(res => setProList(res.data.data))}, [])const [hasMore, setHasMore] = useState(true)const [count, setCount] = useState(2)async function loadMore() {const res = await getProList({ count })setProList([...proList, ...res.data.data])setCount(count+1)setHasMore(res.data.data.length > 0)}return (<><header className="header">home header</header><div className="content"><PullToRefreshonRefresh={async () => {const res = await getProList()setProList(res.data.data)setHasMore(true)setCount(2)}}><BannerComponent bannerList = { bannerList }/><NavComponent list = { navList } /><SeckillComponent list = { seckillList }/><ProComponent list = { proList }></ProComponent></PullToRefresh><InfiniteScroll loadMore={loadMore} hasMore={hasMore} /></div></>)
};export default Home;
11.8返回顶部
分析清除到底是哪一个容器产生了滚动条
分析得知 content 容器产生了滚动条,可以给它绑定一个 scroll 事件用于判断 回到顶部按钮显示还是不显示
通过 content 的dom的scrollTop 属性可以设置滚动条距离
图标是在一个单独的 npm 包中,如果你想使用图标,需要先安装它:
$ cnpm install --save antd-mobile-icons
// src/views/home/style.scss
.backTop {position: fixed;bottom: 0.6rem;right: 10px;width: 36px;height: 36px;background-color: #fff;border: 1px solid #efefef;border-radius: 50%;display: flex;justify-content: center;align-items: center;user-select: none;
}
// src/views/home/Index.tsx
import { getBannerList, getProList, getSeckilllist } from '@/api/home';
import React, { FC, useEffect, useRef, useState } from 'react';
import BannerComponent from './components/BannerComponent';
import SeckillComponent from './components/SeckillComponent';
import NavComponent from './components/NavComponent'
import navList from '@/utils/nav'
import ProComponent from './components/ProComponent';
import { InfiniteScroll, PullToRefresh } from 'antd-mobile';
import { UpOutline } from 'antd-mobile-icons';
import './style.scss'interface IHomeProps {
};
export interface IBanner {bannerid: stringimg: stringalt: stringlink: string
}
export interface IPro {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const Home:FC<IHomeProps> = () => {const [bannerList, setBannerList] = useState<IBanner[]>([])const [seckillList, setSeckillList] = useState<IPro[]>([])const [proList, setProList] = useState<IPro[]>([])useEffect(() => {getBannerList().then(res => setBannerList(res.data.data))getSeckilllist().then(res => setSeckillList(res.data.data))getProList().then(res => setProList(res.data.data))}, [])const [hasMore, setHasMore] = useState(true)const [count, setCount] = useState(2)async function loadMore() {const res = await getProList({ count })setProList([...proList, ...res.data.data])setCount(count+1)setHasMore(res.data.data.length > 0)}const [top, setTop] = useState(0)const contentRef = useRef<any>()const backTop = () => {contentRef.current.scrollTop = 0}return (<><header className="header">home header</header><div className="content" ref={contentRef} onScroll={(event) => {setTop((event.target as HTMLDivElement).scrollTop)}}><PullToRefreshonRefresh={async () => {const res = await getProList()setProList(res.data.data)setHasMore(true)setCount(2)}}><BannerComponent bannerList = { bannerList }/><NavComponent list = { navList } /><SeckillComponent list = { seckillList }/><ProComponent list = { proList }></ProComponent></PullToRefresh><InfiniteScroll loadMore={loadMore} hasMore={hasMore} />{top > 300 && <div className="backTop" onClick={ backTop }><UpOutline /></div>}</div></>)
};export default Home;
11.9 优化项目
提取首页面组件的业务逻辑,封装自定义hooks,统一导出
// src/views/home/components/index.ts
export { default as BannerComponent } from './BannerComponent';
export { default as SeckillComponent } from './SeckillComponent';
export { default as NavComponent } from './NavComponent'
export { default as ProComponent } from './ProComponent';
// src/views/home/hooks/useBanner.tsimport { getBannerList } from "@/api/home"
import { useEffect, useState } from "react"
import { IBanner } from "../Index"const useBanner = () => {const [bannerList, setBannerList] = useState<IBanner[]>([])useEffect(() => {getBannerList().then(res => setBannerList(res.data.data))}, [])return {bannerList}
}export default useBanner
// src/views/home/hooks/useNav.tsimport navList from '@/utils/nav'const useNav = () => {return {navList}
}export default useNav
// src/views/home/hooks/useSeckill.tsimport { getSeckilllist } from "@/api/home"
import { useEffect, useState } from "react"
import { IPro } from "../Index"const useSeckill = () => {const [seckillList, setSeckillList] = useState<IPro[]>([])useEffect(() => {getSeckilllist().then(res => setSeckillList(res.data.data))}, [])return {seckillList}
}export default useSeckill
// src/views/home/hooks/usePro.tsimport { getProList } from "@/api/home"
import { useEffect, useState } from "react"
import { IPro } from "../Index"const usePro = () => {const [proList, setProList] = useState<IPro[]>([]) useEffect(() => {getProList().then(res => setProList(res.data.data))}, [])const [hasMore, setHasMore] = useState(true)const [count, setCount] = useState(2)async function loadMore() {const res = await getProList({ count })setProList([...proList, ...res.data.data])setCount(count+1)setHasMore(res.data.data.length > 0)}const pullRefresh = async () => {const res = await getProList()setProList(res.data.data)setHasMore(true)setCount(2)}return {proList,loadMore,hasMore,pullRefresh}
}export default useProt
// src/views/home/hooks/useBackTop.tsximport React, { useRef, useState } from "react"const useBackTop = () => {const [top, setTop] = useState(0)const contentRef = useRef<any>()const backTop = () => {contentRef.current.scrollTop = 0}const scroll = (event: React.UIEvent<HTMLDivElement, UIEvent>) => {setTop((event.target as HTMLDivElement).scrollTop)}return {top, contentRef, backTop, scroll}
}export default useBackTop
// src/views/home/hooks/index.tsexport { default as useBanner } from './useBanner'
export { default as useNav } from './useNav'
export { default as useSeckill } from './useSeckill'
export { default as usePro } from './usePro'
export { default as useBackTop } from './useBackTop'
// src/views/home/Index.tsx
import React, { FC, useEffect, useRef, useState } from 'react';import { BannerComponent, NavComponent, SeckillComponent, ProComponent } from './components'import { InfiniteScroll, PullToRefresh } from 'antd-mobile';
import { UpOutline } from 'antd-mobile-icons';
import './style.scss'
import { useBackTop, useBanner, useNav, usePro, useSeckill } from './hooks';interface IHomeProps {
};
export interface IBanner {bannerid: stringimg: stringalt: stringlink: string
}
export interface IPro {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const Home:FC<IHomeProps> = () => {const { bannerList } = useBanner()const { navList } = useNav()const { seckillList } = useSeckill()const { proList, loadMore, pullRefresh, hasMore } = usePro()const { contentRef, scroll, top, backTop } = useBackTop()return (<><header className="header">home header</header><div className="content" ref={contentRef} onScroll={ scroll }><PullToRefreshonRefresh={ pullRefresh }><BannerComponent bannerList = { bannerList }/><NavComponent list = { navList } /><SeckillComponent list = { seckillList }/><ProComponent list = { proList }></ProComponent></PullToRefresh><InfiniteScroll loadMore={loadMore} hasMore={hasMore} />{top > 300 && <div className="backTop" onClick={ backTop }><UpOutline /></div>}</div></>)
};export default Home;
现在主流手机都有安全区域,那么写代码时一定要注意
http://ant-design-mobile.antgroup.com/zh/components/safe-area
11.10 自定义头部
// src/views/home/components/Header.scss
.header {ul {width: 100%;height: 100%;display: flex;li {height: 100%;display: flex;justify-content: center;align-items: center;color: #fff;&:nth-child(1), &:nth-child(3) {width: 50px;}&:nth-child(2) {flex: 1;.searchBox {width: 100%;height: 70%;background-color: #fff;border-radius: 16px;color: #666;display: flex;.adm-image-img {width: 40px;margin-top: 4px;margin-left: 10px;}.divider {width: 12px;font-size: 24px;margin-left: 10px;color: #999;}.antd-mobile-icon {width: 18px;height: 18px;margin-top: 6px;display: flex;justify-content: center;align-items: center;}.searchText {flex: 1;line-height: .31rem;display: flex;align-items: center;}}}}}
}
// src/views/home/components/HeaderComponent.tsx
import React, { FC } from 'react';
import { Image } from 'antd-mobile'
import { SearchOutline } from 'antd-mobile-icons';import logo from './logo.png'
import './Header.scss';
interface IHeaderComponentProps {};const HeaderComponent:FC<IHeaderComponentProps> = ({}) => {return (<header className="header"><ul><li>西安</li><li><div className="searchBox"><Image src={ logo } /><span className="divider ">|</span><SearchOutline fontSize={18} /><span className="searchText">立柜式空调</span></div></li><li>登录</li></ul></header>)
};export default HeaderComponent;
// src/views/home/components/index.ts
export { default as BannerComponent } from './BannerComponent';
export { default as SeckillComponent } from './SeckillComponent';
export { default as NavComponent } from './NavComponent'
export { default as ProComponent } from './ProComponent';
export { default as HeaderComponent } from './HeaderComponent';
// src/views/home/Index.tsx
import React, { FC } from 'react';import { BannerComponent, NavComponent, SeckillComponent, ProComponent, HeaderComponent } from './components'import { InfiniteScroll, PullToRefresh } from 'antd-mobile';
import { UpOutline } from 'antd-mobile-icons';
import './style.scss'
import { useBackTop, useBanner, useNav, usePro, useSeckill } from './hooks';interface IHomeProps {
};
export interface IBanner {bannerid: stringimg: stringalt: stringlink: string
}
export interface IPro {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const Home:FC<IHomeProps> = () => {const { bannerList } = useBanner()const { navList } = useNav()const { seckillList } = useSeckill()const { proList, loadMore, pullRefresh, hasMore } = usePro()const { contentRef, scroll, top, backTop } = useBackTop()return (<>{/* <header className="header">home header</header> */}<HeaderComponent></HeaderComponent><div className="content" ref={contentRef} onScroll={ scroll }><PullToRefreshonRefresh={ pullRefresh }><BannerComponent bannerList = { bannerList }/><NavComponent list = { navList } /><SeckillComponent list = { seckillList }/><ProComponent list = { proList }></ProComponent></PullToRefresh><InfiniteScroll loadMore={loadMore} hasMore={hasMore} />{top > 300 && <div className="backTop" onClick={ backTop }><UpOutline /></div>}</div></>)
};export default Home;
12.实现详情
12.1 构建详情页面以及路由
- 构建详情页面组件
// src/views/detail/Index.tsx
import React, { FC } from 'react';interface IDetailProps {
};const Detail:FC<IDetailProps> = () => {return (<><header className="header">detail header</header><div className="content">detail content</div></>)
};export default Detail;
- 构建路由
// src/App.tsx
import React, { FC } from 'react';import { Routes, Route, Navigate } from 'react-router-dom'import Home from '@/views/home/Index'
import Kind from '@/views/kind/Index'
import Cart from '@/views/cart/Index'
import User from '@/views/user/Index'
import NotFound from '@/views/error/404'import Footer from '@/components/Footer'import './App.scss';
import Detail from './views/detail/Index';
interface IAppProps {
}const App: FC<IAppProps> = (props) => {return (<div className="container"><div className="box"><Routes><Route path="/" element={ <Navigate to="/home" />} /><Route path="/home" element={<Home />} /><Route path="/kind" element={<Kind />} /><Route path="/cart" element={<Cart />} /><Route path="/user" element={<User />} /><Route path="/detail/:proid" element={<Detail />} /><Route path="*" element={<NotFound />} /></Routes></div><footer className="footer"><Footer /></footer></div>)
}export default App
通过访问地址发现可以跳转到详情,但是详情页面不应有 底部选项卡,需要处理
// src/components/Footer.tsx
import React, { FC } from 'react';import { NavLink } from 'react-router-dom'interface IFooterProps {};const Footer:FC<IFooterProps> = () => {return (<footer className="footer"><ul><NavLink to="/home" style = {({ isActive }) => isActive ? { color: '#f66' }: undefined }><span className="iconfont icon-shouye"></span><p>首页</p></NavLink><NavLink to="/kind" style = {({ isActive }) => isActive ? { color: '#f66' }: undefined }><span className="iconfont icon-fenlei"></span><p>分类</p></NavLink><NavLink to="/cart" style = {({ isActive }) => isActive ? { color: '#f66' }: undefined }><span className="iconfont icon-gouwuche"></span><p>购物车</p></NavLink><NavLink to="/user" style = {({ isActive }) => isActive ? { color: '#f66' }: undefined }><span className="iconfont icon-shouye1"></span><p>我的</p></NavLink></ul></footer>)
};export default Footer;
// src/App.tsx
import React, { FC } from 'react';import { Routes, Route, Navigate } from 'react-router-dom'import Home from '@/views/home/Index'
import Kind from '@/views/kind/Index'
import Cart from '@/views/cart/Index'
import User from '@/views/user/Index'
import NotFound from '@/views/error/404'import Footer from '@/components/Footer'import './App.scss';
import Detail from './views/detail/Index';
interface IAppProps {
}const App: FC<IAppProps> = (props) => {return (<div className="container"><div className="box"><Routes><Route path="/" element={ <Navigate to="/home" />} /><Route path="/home" element={<Home />} /><Route path="/kind" element={<Kind />} /><Route path="/cart" element={<Cart />} /><Route path="/user" element={<User />} /><Route path="/detail/:proid" element={<Detail />} /><Route path="*" element={<NotFound />} /></Routes></div><Routes><Route path="/home" element={<Footer />} /><Route path="/kind" element={<Footer />} /><Route path="/cart" element={<Footer />} /><Route path="/user" element={<Footer />} /></Routes></div>)
}export default App
12.2 点击列表进入产品详情
- 秒杀列表声明式进入详情
// src/views/home/components/SeckillComponent.tsx
import { Grid, Image } from 'antd-mobile';
import React, { FC } from 'react';
import { Link } from 'react-router-dom';
import { IPro } from '../Index';interface ISeckillComponentProps {list: IPro[]
};const SeckillComponent:FC<ISeckillComponentProps> = ({ list }) => {return (<><Grid columns={6} gap={8}>{ list.map(item => {return (<Link key = { item.proid } to={ '/detail/' + item.proid }><Grid.Item ><Image src= { item.img1 } style={{width: 55, height: 55}}/><p style={{ color: '#f66', textAlign: 'center' }}>¥{ item.originprice }</p></Grid.Item></Link>)})}</Grid></>)
};export default SeckillComponent;
- 产品列表编程式进入详情
// src/views/home/ProComponent.tsx
import React, { FC, memo } from 'react';
import { Image } from 'antd-mobile'
import { IPro } from '../Index';
import { useNavigate } from 'react-router-dom';interface IProComponentProps {list: IPro[]};const WaterfallItem = ({context}: any) => {console.log(context)const navigate = useNavigate()return (<div className='item' onClick={ () => {navigate('/detail/' + context.proid)}}><div className="itemImage"><Image src = {context.img1} style={{maxHeight: '1.8rem'}}/></div><div className="itemInfo"><div style={{ maxHeight: '36px', overflow: 'hidden',}}>{ context.proname }</div><div>¥{ context.originprice }</div></div></div>)
}
const ProComponent:FC<IProComponentProps> = memo(({ list }) => {console.log(list)return (<><ul style={{ float: 'left', width: '48%', margin: '5px 1%'}}>{list.map((item, index) => {return index % 2 === 0 ? <WaterfallItem context={item}/> : null})}</ul><ul style={{ float: 'left', width: '48%', margin: '5px 1%'}}>{list.map((item, index) => {return index % 2 === 1 ? <WaterfallItem context={item}/> : null})}</ul></>)
});export default ProComponent
12.3 详情页获取路由参数
// src/views/detail/Index.tsx
import React, { FC } from 'react';
import { useParams } from 'react-router-dom';interface IDetailProps {
};const Detail:FC<IDetailProps> = () => {// const params = useParams()// console.log(params)const { proid } = useParams()return (<><header className="header">detail header</header><div className="content">detail content</div></>)
};export default Detail;
12.4 封装详情页数据请求
// src/api/detail.ts
import request from '@/utils/request'interface IPager {count?: numberlimitNum?: number
}export function getProDetail (proid: string) {return request.get('/pro/detail/' + proid)
}// 详情 猜你喜欢 - 推荐
export function getRecommendList (params?: IPager) {return request.get('/pro/recommendlist', { params })
}
// src/views/detail/Index.tsx
import React, { FC, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { getDetailData } from '@/api/detail'
interface IDetailProps {};const Detail:FC<IDetailProps> = () => {const { proid } = useParams()console.log(proid)const [obj, setObj] = useState({banners: [],proname: '',originprice: 0,discount: 0,brand: '',category: '',sales: 0,issale: 1})useEffect(() => {getDetailData(proid!).then(res => {console.log(res.data.data)setObj({banners: res.data.data.banners[0].split(','),proname: res.data.data.proname,originprice: res.data.data.originprice,discount: res.data.data.discount,brand: res.data.data.brand,category: res.data.data.category,sales: res.data.data.sales,issale: res.data.data.issale})})}, [proid])return (<><header className="header">Detail header</header><div className="content">Detail content</div></>)
};export default Detail;
12.5 渲染详情页面
12.5.1 轮播图以及大图预览
// src/views/detail/Index.tsx
import { getProDetail } from '@/api/detail';
import { Image, ImageViewer, Swiper } from 'antd-mobile';
import React, { FC, useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
interface IDetailProps {
};
export interface IPro {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const Detail:FC<IDetailProps> = () => {// const params = useParams()// console.log(params)const { proid } = useParams()const [obj, setObj] = useState<IPro>({banners: [],brand: '',category: '',desc: '',discount: 0,img1: '',img2: '',img3: '',img4: '',isrecommend: 0,issale: 0,isseckill: 0,originprice: 0,proid: '',proname: '',sales: 0,stock: 0})useEffect(() => {getProDetail(proid!).then(res => {res.data.data.banners = res.data.data.banners[0].split(',')console.log(res.data.data)setObj(res.data.data)})}, [])const [visible, setVisible] = useState(false)const [current, setCurrent] = useState(0)const swiperRef = useRef<any>()return (<><header className="header">detail header</header><div className="content"><Swiper ref={swiperRef}>{obj.banners.map((item, index) => {return (<Swiper.Item key={ item } onClick = { () => {setCurrent(index)setVisible(true)// swiperRef.current.swipeTo(index, true)}}><Image src={ item } /></Swiper.Item>)})}</Swiper>{visible ? <ImageViewer.Multiimages={obj.banners}visible={visible}defaultIndex = { current }onIndexChange= { index => {swiperRef.current.swipeTo(index)}}onClose={() => {setVisible(false)}}/> : null}</div></>)
};export default Detail;
大图预览遇到了 点击穿透问题
使用tap事件代替 click 事件(tap事件原生不支持,需要额外引入插件)
使用mouse事件代替click 事件
使用fastclick事件代替click事件(一般页面引入插件即可)
https://antd-mobile-v2.surge.sh/docs/react/introduce-cn
引入 FastClick 并且设置 html
meta
(更多参考 #576)引入 Promise 的 fallback 支持 (部分安卓手机不支持 Promise)
<!DOCTYPE html> <html> <head><!-- set `maximum-scale` for some compatibility issues --><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" /><script src="https://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js"></script><script>if ('addEventListener' in document) {document.addEventListener('DOMContentLoaded', function() {FastClick.attach(document.body);}, false);}if(!window.Promise) {document.writeln('<script src="https://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js"'+'>'+'<'+'/'+'script>');}</script> </head> <body></body> </html>
public/index.html
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8" /><link rel="icon" href="%PUBLIC_URL%/favicon.ico" /><!-- <meta name="viewport" content="width=device-width, initial-scale=1" /> --><meta name="theme-color" content="#000000" /><metaname="description"content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /><!--manifest.json provides metadata used when your web app is installed on auser's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/--><link rel="manifest" href="%PUBLIC_URL%/manifest.json" /><!--Notice the use of %PUBLIC_URL% in the tags above.It will be replaced with the URL of the `public` folder during the build.Only files inside the `public` folder can be referenced from the HTML.Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" willwork correctly both with client-side routing and a non-root public URL.Learn how to configure a non-root public URL by running `npm run build`.--><title>React App</title><link rel="stylesheet" href="//at.alicdn.com/t/c/font_3665887_h3lsrioddkk.css"><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" /><script src="https://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js"></script><script>if ('addEventListener' in document) {document.addEventListener('DOMContentLoaded', function() {FastClick.attach(document.body);}, false);}if(!window.Promise) {document.writeln('<script src="https://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js"'+'>'+'<'+'/'+'script>');}</script>
</head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><!--This HTML file is a template.If you open it directly in the browser, you will see an empty page.You can add webfonts, meta tags, or analytics to this file.The build step will place the bundled scripts into the <body> tag.To begin the development, run `npm start` or `yarn start`.To create a production bundle, use `npm run build` or `yarn build`.--></body>
</html>
12.5.2 构建产品详细信息
// src/views/detail/style.scss
.proInfo {background-color: #fff;padding: 15px;border-bottom-right-radius: 16px;border-bottom-left-radius: 16px;.priceBox {span {line-height: 32px;&:nth-child(1) {font-size: 24px;color: #f66;}&:nth-child(2) {float: right;}}}.proName {font-weight: bold;font-size: 0.14rem;.adm-tag {margin-right: 5px;}}
}
// src/views/detail/Index.tsx
import { getProDetail } from '@/api/detail';
import { Image, ImageViewer, Swiper, Tag } from 'antd-mobile';
import React, { FC, useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import './style.scss'
interface IDetailProps {
};
export interface IPro {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const Detail:FC<IDetailProps> = () => {// const params = useParams()// console.log(params)const { proid } = useParams()const [obj, setObj] = useState<IPro>({banners: [],brand: '',category: '',desc: '',discount: 0,img1: '',img2: '',img3: '',img4: '',isrecommend: 0,issale: 0,isseckill: 0,originprice: 0,proid: '',proname: '',sales: 0,stock: 0})useEffect(() => {getProDetail(proid!).then(res => {res.data.data.banners = res.data.data.banners[0].split(',')console.log(res.data.data)setObj(res.data.data)})}, [])const [visible, setVisible] = useState(false)const [current, setCurrent] = useState(0)const swiperRef = useRef<any>()return (<><header className="header">detail header</header><div className="content"><Swiper ref={swiperRef}>{obj.banners.map((item, index) => {return (<Swiper.Item key={ item } onClick = { () => {setCurrent(index)setVisible(true)// swiperRef.current.swipeTo(index, true)}}><Image src={ item } /></Swiper.Item>)})}</Swiper>{visible ? <ImageViewer.Multiimages={obj.banners}visible={visible}defaultIndex = { current }onIndexChange= { index => {swiperRef.current.swipeTo(index)}}onClose={() => {setVisible(false)}}/> : null}<div className="proInfo"><div className="priceBox"><span>¥{ obj.originprice }</span><span>销量:{ obj.sales }</span></div><div className="proName"><Tag color='danger'>{ obj.brand }</Tag><Tag color='primary'>{ obj.category }</Tag><span>{ obj.proname }</span></div></div></div></>)
};export default Detail;
12.5.3 猜你喜欢
// src/views/detail/ProComponent.tsx
import React, { FC, memo } from 'react';
import { Image } from 'antd-mobile'
import { IPro } from './Index';
import { useNavigate } from 'react-router-dom';interface IProComponentProps {list: IPro[]};const WaterfallItem = ({context}: any) => {console.log(context)const navigate = useNavigate()return (<div className='item' onClick={ () => {navigate('/detail/' + context.proid)}}><div className="itemImage"><Image src = {context.img1} style={{maxHeight: '1.8rem'}}/></div><div className="itemInfo"><div style={{ maxHeight: '36px', overflow: 'hidden',}}>{ context.proname }</div><div>¥{ context.originprice }</div></div></div>)
}
const ProComponent:FC<IProComponentProps> = memo(({ list }) => {console.log(list)return (<><ul style={{ float: 'left', width: '48%', margin: '5px 1%'}}>{list.map((item, index) => {return index % 2 === 0 ? <WaterfallItem context={item}/> : null})}</ul><ul style={{ float: 'left', width: '48%', margin: '5px 1%'}}>{list.map((item, index) => {return index % 2 === 1 ? <WaterfallItem context={item}/> : null})}</ul></>)
});export default ProComponent
// src/views/detail/Index.tsx
import { getProDetail, getRecommendList } from '@/api/detail';
import { Divider, Image, ImageViewer, Swiper, Tag } from 'antd-mobile';
import React, { FC, useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import ProComponent from './ProComponent';
import './style.scss'
interface IDetailProps {
};
export interface IPro {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const Detail:FC<IDetailProps> = () => {// const params = useParams()// console.log(params)const { proid } = useParams()const [obj, setObj] = useState<IPro>({banners: [],brand: '',category: '',desc: '',discount: 0,img1: '',img2: '',img3: '',img4: '',isrecommend: 0,issale: 0,isseckill: 0,originprice: 0,proid: '',proname: '',sales: 0,stock: 0})useEffect(() => {getProDetail(proid!).then(res => {res.data.data.banners = res.data.data.banners[0].split(',')console.log(res.data.data)setObj(res.data.data)})}, [])const [visible, setVisible] = useState(false)const [current, setCurrent] = useState(0)const swiperRef = useRef<any>()const [proList, setProList] = useState([]) // ++++useEffect(() => { // ++++getRecommendList().then(res => setProList(res.data.data))}, [])return (<><header className="header">detail header</header><div className="content"><Swiper ref={swiperRef}>{obj.banners.map((item, index) => {return (<Swiper.Item key={ item } onClick = { () => {setCurrent(index)setVisible(true)// swiperRef.current.swipeTo(index, true)}}><Image src={ item } /></Swiper.Item>)})}</Swiper>{visible ? <ImageViewer.Multiimages={obj.banners}visible={visible}defaultIndex = { current }onIndexChange= { index => {swiperRef.current.swipeTo(index)}}onClose={() => {setVisible(false)}}/> : null}<div className="proInfo"><div className="priceBox"><span>¥{ obj.originprice }</span><span>销量:{ obj.sales }</span></div><div className="proName"><Tag color='danger'>{ obj.brand }</Tag><Tag color='primary'>{ obj.category }</Tag><span>{ obj.proname }</span></div></div><Dividerstyle={{color: '#1677ff',borderColor: '#1677ff',borderStyle: 'dashed',}}>猜你喜欢</Divider><ProComponent list = { proList } /></div></>)
};export default Detail;
12.5.4 详情底部
// src/views/detail/style.scss
.proInfo {background-color: #fff;padding: 15px;border-bottom-right-radius: 16px;border-bottom-left-radius: 16px;.priceBox {span {line-height: 32px;&:nth-child(1) {font-size: 24px;color: #f66;}&:nth-child(2) {float: right;}}}.proName {font-weight: bold;font-size: 0.14rem;.adm-tag {margin-right: 5px;}}
}.detailFooter {position: fixed;bottom: 0;height: 0.5rem;border-top: 1px solid #000;background-color: #fff;width: 100%;display: flex;li {display: flex;height: 100%;flex-direction: column;justify-content: center;align-items: center;&:nth-child(1), &:nth-child(2), &:nth-child(3) {flex: 1}&:nth-child(4){flex: 3}&:nth-child(5) {flex: 3}}
}
// src/views/detail/Footer.tsx
import { Button } from 'antd-mobile';
import React, { FC } from 'react';
import './style.scss'
interface IFooterProps {
};const Footer:FC<IFooterProps> = () => {return (<ul className='detailFooter'><li>店铺</li><li>购物车</li><li>收藏</li><li><Button color="warning" shape="rounded">加入购物车</Button></li><li><Button color="danger" shape="rounded">立即购买</Button></li></ul>)
};export default Footer;
// src/views/detail/Index.tsx
import { getProDetail, getRecommendList } from '@/api/detail';
import { Divider, Image, ImageViewer, Swiper, Tag } from 'antd-mobile';
import React, { FC, useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import Footer from './Footer';
import ProComponent from './ProComponent';
import './style.scss'
interface IDetailProps {
};
export interface IPro {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const Detail:FC<IDetailProps> = () => {// const params = useParams()// console.log(params)const { proid } = useParams()const [obj, setObj] = useState<IPro>({banners: [],brand: '',category: '',desc: '',discount: 0,img1: '',img2: '',img3: '',img4: '',isrecommend: 0,issale: 0,isseckill: 0,originprice: 0,proid: '',proname: '',sales: 0,stock: 0})useEffect(() => {getProDetail(proid!).then(res => {res.data.data.banners = res.data.data.banners[0].split(',')console.log(res.data.data)setObj(res.data.data)})}, [])const [visible, setVisible] = useState(false)const [current, setCurrent] = useState(0)const swiperRef = useRef<any>()const [proList, setProList] = useState([]) // ++++useEffect(() => { // ++++getRecommendList().then(res => setProList(res.data.data))}, [])return (<><header className="header">detail header</header><div className="content"><Swiper ref={swiperRef}>{obj.banners.map((item, index) => {return (<Swiper.Item key={ item } onClick = { () => {setCurrent(index)setVisible(true)// swiperRef.current.swipeTo(index, true)}}><Image src={ item } /></Swiper.Item>)})}</Swiper>{visible ? <ImageViewer.Multiimages={obj.banners}visible={visible}defaultIndex = { current }onIndexChange= { index => {swiperRef.current.swipeTo(index)}}onClose={() => {setVisible(false)}}/> : null}<div className="proInfo"><div className="priceBox"><span>¥{ obj.originprice }</span><span>销量:{ obj.sales }</span></div><div className="proName"><Tag color='danger'>{ obj.brand }</Tag><Tag color='primary'>{ obj.category }</Tag><span>{ obj.proname }</span></div></div><Dividerstyle={{color: '#1677ff',borderColor: '#1677ff',borderStyle: 'dashed',}}>猜你喜欢</Divider><ProComponent list = { proList } /><Footer /></div></>)
};export default Detail;
12.5.5 详情头部
// src/views/detail/style.scss
.proInfo {background-color: #fff;padding: 15px;border-bottom-right-radius: 16px;border-bottom-left-radius: 16px;.priceBox {span {line-height: 32px;&:nth-child(1) {font-size: 24px;color: #f66;}&:nth-child(2) {float: right;}}}.proName {font-weight: bold;font-size: 0.14rem;.adm-tag {margin-right: 5px;}}
}.detailFooter {position: fixed;bottom: 0;height: 0.5rem;border-top: 1px solid #000;background-color: #fff;width: 100%;display: flex;li {display: flex;height: 100%;flex-direction: column;justify-content: center;align-items: center;&:nth-child(1), &:nth-child(2), &:nth-child(3) {flex: 1}&:nth-child(4){flex: 3}&:nth-child(5) {flex: 3}}
}
.myHeader {user-select: none;position: fixed;top: 0;width: 100%;z-index: 999;.header1 {height: 0.44rem;padding: 6px 15px;box-sizing: border-box;ul {width: 100%;height: 100%;display: flex;li {&:nth-child(1), &:nth-child(3) {font-size: 32px;width: 44px;}&:nth-child(2) {flex: 1;}}}}.header2 {height: 0.44rem;padding: 6px 15px;box-sizing: border-box;background-color: #fff;ul {width: 100%;height: 100%;display: flex;li {&:nth-child(1), &:nth-child(3) {font-size: 24px;width: 44px;}&:nth-child(2) {flex: 1;display: flex;span {flex: 1;display: flex;justify-content: center;align-items: center;}}}}}
}
// src/views/detail/Header.tsx
import React, { FC } from 'react';
import {LeftOutline, MoreOutline} from 'antd-mobile-icons'
import './style.scss'
import { useNavigate } from 'react-router-dom';
interface IHeaderComponentProps {};const HeaderComponent:FC<IHeaderComponentProps> = ({}) => {const navigate = useNavigate()return (<div className='myHeader'><header className="header2" v-show="scrollTop > 300"><ul><li className="left" onClick={ () => navigate(-1) } ><LeftOutline /></li><li className="middle"><span>详情</span><span>推荐</span></li><li className="right"><MoreOutline /></li></ul></header></div>)
};export default HeaderComponent;
// src/views/detail/Index.tsx
import { getProDetail, getRecommendList } from '@/api/detail';
import { Divider, Image, ImageViewer, Swiper, Tag } from 'antd-mobile';
import React, { FC, useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import Footer from './Footer';
import HeaderComponent from './Header';
import ProComponent from './ProComponent';
import './style.scss'
interface IDetailProps {
};
export interface IPro {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const Detail:FC<IDetailProps> = () => {// const params = useParams()// console.log(params)const { proid } = useParams()const [obj, setObj] = useState<IPro>({banners: [],brand: '',category: '',desc: '',discount: 0,img1: '',img2: '',img3: '',img4: '',isrecommend: 0,issale: 0,isseckill: 0,originprice: 0,proid: '',proname: '',sales: 0,stock: 0})useEffect(() => {getProDetail(proid!).then(res => {res.data.data.banners = res.data.data.banners[0].split(',')console.log(res.data.data)setObj(res.data.data)})}, [])const [visible, setVisible] = useState(false)const [current, setCurrent] = useState(0)const swiperRef = useRef<any>()const [proList, setProList] = useState([]) // ++++useEffect(() => { // ++++getRecommendList().then(res => setProList(res.data.data))}, [])return (<>{/* <header className="header">detail header</header> */}<HeaderComponent /><div className="content"><Swiper ref={swiperRef}>{obj.banners.map((item, index) => {return (<Swiper.Item key={ item } onClick = { () => {setCurrent(index)setVisible(true)// swiperRef.current.swipeTo(index, true)}}><Image src={ item } /></Swiper.Item>)})}</Swiper>{visible ? <ImageViewer.Multiimages={obj.banners}visible={visible}defaultIndex = { current }onIndexChange= { index => {swiperRef.current.swipeTo(index)}}onClose={() => {setVisible(false)}}/> : null}<div className="proInfo"><div className="priceBox"><span>¥{ obj.originprice }</span><span>销量:{ obj.sales }</span></div><div className="proName"><Tag color='danger'>{ obj.brand }</Tag><Tag color='primary'>{ obj.category }</Tag><span>{ obj.proname }</span></div></div><Dividerstyle={{color: '#1677ff',borderColor: '#1677ff',borderStyle: 'dashed',}}>猜你喜欢</Divider><ProComponent list = { proList } /><Footer /></div></>)
};export default Detail;
13.登录功能
其实要登录需要先注册,借用vue项目的注册账户,此处直接实现登录
13.1 构建注册组件
// src/views/register/components/Step1.tsx
import React, { FC } from 'react';
import { Button } from 'antd-mobile'
import { useNavigate } from 'react-router-dom';
interface IRegisterStep1Props {};const RegisterStep1:FC<IRegisterStep1Props> = () => {const navigate = useNavigate()return (<><h1>注册第一步</h1><Button color='danger' onClick={ () => navigate('/register/step2')}>下一步</Button></>)
};export default RegisterStep1;
// src/views/register/components/Step2.tsx
import React, { FC } from 'react';
import { Button } from 'antd-mobile'
import { useNavigate } from 'react-router-dom';
interface IRegisterStep2Props {};const RegisterStep2:FC<IRegisterStep2Props> = () => {const navigate = useNavigate()return (<><h1>注册第二步</h1><Button color='danger' onClick={ () => navigate('/register/step3')}>下一步</Button></>)
};export default RegisterStep2;
// src/views/register/components/Step3.tsx
import React, { FC } from 'react';
import { Button } from 'antd-mobile'
import { useNavigate } from 'react-router-dom';
interface IRegisterStep3Props {};const RegisterStep3:FC<IRegisterStep3Props> = () => {const navigate = useNavigate()return (<><h1>注册第三步</h1><Button color='danger' onClick={ () => navigate(-3)}>完成</Button></>)
};export default RegisterStep3;
// src/views/register/components/index.ts
export { default as Step1 } from './Step1'
export { default as Step2 } from './Step2'
export { default as Step3 } from './Step3'
// src/views/regiter/Index.tsx
import React, { FC } from 'react';
import { Outlet } from 'react-router-dom';interface IRegisterProps {
};const Register:FC<IRegisterProps> = () => {return (<><header className="header">Register header</header><div className="content"><Outlet /></div></>)
};export default Register;
13.2 构建登录组件
// src/views/login/Index.tsx
import React, { FC } from 'react';interface ILoginProps {
};const Login:FC<ILoginProps> = () => {return (<><header className="header">Login header</header><div className="content">Login content</div></>)
};export default Login;
13.3 设置登录以及注册路由
注册路由使用嵌套路由
// src/App.tsx
import React, { FC } from 'react';import { Routes, Route, Navigate } from 'react-router-dom'import Home from '@/views/home/Index'
import Kind from '@/views/kind/Index'
import Cart from '@/views/cart/Index'
import User from '@/views/user/Index'
import NotFound from '@/views/error/404'import Footer from '@/components/Footer'import './App.scss';
import Detail from './views/detail/Index';
import Login from './views/login/Index';
import Register from './views/register/Index';
import RegisterStep1 from './views/register/components/Step1';
import RegisterStep2 from './views/register/components/Step2';
import RegisterStep3 from './views/register/components/Step3';
interface IAppProps {
}const App: FC<IAppProps> = (props) => {return (<div className="container"><div className="box"><Routes><Route path="/" element={ <Navigate to="/home" />} /><Route path="/home" element={<Home />} /><Route path="/kind" element={<Kind />} /><Route path="/cart" element={<Cart />} /><Route path="/user" element={<User />} /><Route path="/login" element={<Login />} /><Route path="/register" element={<Register />} ><Route path="/register" element={<Navigate to="/register/step1" replace={true} />} /><Route path="/register/step1" element={<RegisterStep1 />} /><Route path="/register/step2" element={<RegisterStep2 />} /><Route path="/register/step3" element={<RegisterStep3 />} /></Route><Route path="/detail/:proid" element={<Detail />} /><Route path="*" element={<NotFound />} /></Routes></div><Routes><Route path="/home" element={<Footer />} /><Route path="/kind" element={<Footer />} /><Route path="/cart" element={<Footer />} /><Route path="/user" element={<Footer />} /></Routes></div>)
}export default App
13.4 修改登录组件
// src/views/login/Index.tsx
import { Button, Form, Input } from 'antd-mobile';
import React, { FC } from 'react';interface ILoginProps {
};const Login:FC<ILoginProps> = () => {const loginFn = (values: any) => {console.log(values)}return (<><header className="header">Login header</header><div className="content"><Formlayout='horizontal'footer={<Button block type='submit' color='danger' size='large'>提交</Button>}onFinish = { loginFn }><Form.Itemname='loginname'rules={[{ required: true, message: '账户名不能为空' }]}><Input onChange={console.log} placeholder='用户名/邮箱/手机号' /></Form.Item><Form.Itemname='password'rules={[{ required: true, message: '密码不能为空' }]}><Input onChange={console.log} placeholder='请输入密码' /></Form.Item></Form></div></>)
};export default Login;
13.5 封装用户数据请求
// src/api/user.ts
import request from './../utils/request'// 检测手机号是否被注册过
export function doCheckPhone (params: { tel: string }) {return request.post('/user/docheckphone', params)
}// 发送短信验证码
export function doSendMsgCode (params: { tel: string }) {return request.post('/user/dosendmsgcode', params)
}// 验证验证码
export function doCheckCode (params: { tel: string, telcode: string }) {return request.post('/user/docheckcode', params)
}// 设置密码完成注册
export function doFinishRegister (params: { tel: string, password: string }) {return request.post('/user/dofinishregister', params)
}// 登录
export function doLogin (params: { loginname: string, password: string }) {return request.post('/user/login', params)
}
13.6 实现登录功能
// src/views/login/Index.tsx
import { doLogin } from '@/api/user';
import { Button, Form, Input, Toast } from 'antd-mobile';
import React, { FC } from 'react';
import { useNavigate } from 'react-router-dom';interface ILoginProps {
};const Login:FC<ILoginProps> = () => {const navigate = useNavigate()const loginFn = (values: any) => {console.log(values)doLogin(values).then(res => {if (res.data.code === '10011') {Toast.show({content: '密码错误',duration: 1000})} else if (res.data.code === '10010') {Toast.show({content: '该用户还未注册',duration: 1000})} else {Toast.show({content: '登录成功',duration: 1000})// 保存数据到本地// 保存数据到状态管理器// 返回上一页localStorage.setItem('loginState', String(true))localStorage.setItem('userid', res.data.data.userid)localStorage.setItem('token', res.data.data.token)navigate(-1)}})}return (<><header className="header">Login header</header><div className="content"><Formlayout='horizontal'footer={<Button block type='submit' color='danger' size='large'>提交</Button>}onFinish = { loginFn }><Form.Itemname='loginname'rules={[{ required: true, message: '账户名不能为空' }]}><Input onChange={console.log} placeholder='用户名/邮箱/手机号' /></Form.Item><Form.Itemname='password'rules={[{ required: true, message: '密码不能为空' }]}><Input onChange={console.log} placeholder='请输入密码' /></Form.Item></Form></div></>)
};export default Login;
14.mobx状态管理器
https://cn.mobx.js.org/ — v5
https://www.mobxjs.com/ -v6
14.1 安装
$ cnpm i mobx mobx-react -S
14.2 创建状态管理器
// src/store/modules/user.tsimport { makeAutoObservable } from "mobx"class UserStore {// 初始化数据loginState = localStorage.getItem('loginState') === 'true'token = localStorage.getItem('token') || ''userid = localStorage.getItem('userid') || ''constructor () {// 讲此类设置为可被观察的makeAutoObservable(this)// this.changeLoginState = this.changeLoginState.bind(this)// this.changeToken = this.changeToken.bind(this)// this.changeUserId = this.changeUserId.bind(this)}changeLoginState (action: { payload: boolean}) {this.loginState = action.payload}changeToken (action: { payload: string}) {this.token = action.payload}changeUserId (action: { payload: string}) {this.userid = action.payload}
}export default UserStore
// src/store/index.tsimport { makeAutoObservable } from "mobx";
import UserStore from "./modules/user";class Store {userconstructor () {makeAutoObservable(this)this.user = new UserStore()}
}export default new Store()
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';import { HashRouter } from 'react-router-dom'import ErrorBoundary from './ErrorBundary';
import App from './App';
import reportWebVitals from './reportWebVitals';import { Provider } from 'mobx-react'
import store from './store';const root = ReactDOM.createRoot(document.getElementById('root') as HTMLDivElement
);
root.render(<React.StrictMode><ErrorBoundary><HashRouter><Provider store = { store }><App /></Provider></HashRouter></ErrorBoundary></React.StrictMode>
);reportWebVitals();
// src/views/login/Index.tsx
import { doLogin } from '@/api/user';
import { Button, Form, Input, Toast } from 'antd-mobile';
import { inject, observer } from 'mobx-react';
import React, { FC } from 'react';
import { useNavigate } from 'react-router-dom';interface ILoginProps {
};const Login:FC<ILoginProps> = (props: any) => {const navigate = useNavigate()const loginFn = (values: any) => {console.log(values)doLogin(values).then(res => {if (res.data.code === '10011') {Toast.show({content: '密码错误',duration: 1000})} else if (res.data.code === '10010') {Toast.show({content: '该用户还未注册',duration: 1000})} else {Toast.show({content: '登录成功',duration: 1000})// 保存数据到本地// 保存数据到状态管理器// 返回上一页localStorage.setItem('loginState', String(true))localStorage.setItem('userid', res.data.data.userid)localStorage.setItem('token', res.data.data.token)console.log(props)props.store.user.changeLoginState({ payload: true })props.store.user.changeUserId({ payload: res.data.data.userid })props.store.user.changeToken({ payload: res.data.data.token })navigate(-1)}})}return (<><header className="header">Login header</header><div className="content"><Formlayout='horizontal'footer={<Button block type='submit' color='danger' size='large'>提交</Button>}onFinish = { loginFn }><Form.Itemname='loginname'rules={[{ required: true, message: '账户名不能为空' }]}><Input onChange={console.log} placeholder='用户名/邮箱/手机号' /></Form.Item><Form.Itemname='password'rules={[{ required: true, message: '密码不能为空' }]}><Input onChange={console.log} placeholder='请输入密码' /></Form.Item></Form></div></>)
};// inject('store') 将入口的文件的 传递的 store 接收,传递组件的 props属性 ,组件可以通过 props.store访问状态管理器
// observer() 将此组件设置为观察者,一旦检测到store的数据发生改变,更新视图
export default inject('store')(observer(Login));
15.加入购物车
15.1 封装数据请求
给请求添加token
// src/utils/request.ts
// 1.引入axios
import axios from 'axios'// 2.项目环境
// 生产环境 process.env.NODE_ENV === 'production' cnpm run build
// 测试环境 ?
// 开发环境 process.env.NODE_ENV === 'devlopment cnpm run start
const isDev = process.env.NODE_ENV === 'development'// 3.给axios添加默认选项
// axios.defaults.withCredentials = false // 设置跨域是否需要携带凭证
// axios.defaults.timeout = 6000 // 6秒超时时间
// axios.defaults.baseURL = isDev ? 'http://121.89.205.189:3000/api' : 'http://121.89.205.189:3000/api'// 4.自定义axios
const ins = axios.create({baseURL: isDev ? 'http://121.89.205.189:3000/api' : 'http://121.89.205.189:3000/api',timeout: 10000
})// 5.设置拦截器
// 请求的拦截器 所有的请求在开始之前先执行请求拦截器,再执行自己的请求
ins.interceptors.request.use((config) => {// 设置请求的loading显示 --- 使用组件不必要 ---- js模块显示// 设置token,一般token传递给后端通过 请求头传递 config.headers.token = ''config.headers.token = localStorage.getItem('token')return config
}, (err) => {return Promise.reject(err)
})// 响应拦截器 所有的接口返回值先执行响应拦截器,再返回自己的响应的数据
ins.interceptors.response.use((response) => {// 关闭loading动画 --- 使用组件不必要 ---- js模块隐藏// 验证token,如果验证通过,返回数,如果验证不通过,直接跳转到登录页面if (response.data.code === '10119') {window.location.href="/#/login"}return response
}, (err) => Promise.reject(err))// 6.暴露自定义axios
export default ins
// src/api/cart.ts
import request from '@/utils/request'// 加入购物车
export function addCart (params: { userid: string, proid: string, num: number }) {return request.post('/cart/add', params)
}// 获取购物车列表数据
export function getCartListData (params: { userid: string }) {return request.post('/cart/list', params)
}// 删除某个用户的购物车的所有数据
export function removeAllData (params: { userid: string }) {return request.post('/cart/removeall', params)
}// 删除某个用户的一条购物车的数据
export function removeOneData (params: { cartid: string }) {return request.post('/cart/remove', params)
}// 更新某个用户的一条购物车的数据的选中状态
export function selectOneData (params: { cartid: string, flag: boolean }) {return request.post('/cart/selectone', params)
}// 更新某个用户的购物车的所有数据的选中状态
export function selectAllData (params: { userid: string, type: boolean }) {return request.post('/cart/selectall', params)
}// 更新某个用户的购物车的某个产品的数量
export function updateOneDataNum (params: { cartid: string, num: number }) {return request.post('/cart/updatenum', params)
}// 推荐商品接口
export function getCartRecommendData () {return request.get('/pro/recommendlist')
}
15.2 加入购物车
底部组件提供了 各个选项的事件,需要按照组件提供写法去写
// src/views/detail/Index.tsx
import { getProDetail, getRecommendList } from '@/api/detail';
import { Divider, Image, ImageViewer, Swiper, Tag } from 'antd-mobile';
import React, { FC, useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import Footer from './Footer';
import HeaderComponent from './Header';
import ProComponent from './ProComponent';
import './style.scss'
interface IDetailProps {
};
export interface IPro {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const Detail:FC<IDetailProps> = () => {// const params = useParams()// console.log(params)const { proid } = useParams()const [obj, setObj] = useState<IPro>({banners: [],brand: '',category: '',desc: '',discount: 0,img1: '',img2: '',img3: '',img4: '',isrecommend: 0,issale: 0,isseckill: 0,originprice: 0,proid: '',proname: '',sales: 0,stock: 0})const [visible, setVisible] = useState(false)const [current, setCurrent] = useState(0)const swiperRef = useRef<any>()const [proList, setProList] = useState([]) // ++++useEffect(() => { // ++++getRecommendList().then(res => setProList(res.data.data))}, [])useEffect(() => {getProDetail(proid!).then(res => {// console.log(swiperRef)res.data.data.banners = res.data.data.banners[0].split(',')// console.log(res.data.data)setObj(res.data.data)swiperRef.current && swiperRef.current!.swipeTo(0)})}, [proid])return (<>{/* <header className="header">detail header</header> */}<HeaderComponent /><div className="content">{obj.banners && <Swiper ref={swiperRef}>{obj.banners && obj.banners.map((item, index) => {return (<Swiper.Item key={ item } onClick = { () => {setCurrent(index)setVisible(true)// swiperRef.current.swipeTo(index, true)}}><Image src={ item } /></Swiper.Item>)})}</Swiper> }{visible ? <ImageViewer.Multiimages={obj.banners}visible={visible}defaultIndex = { current }onIndexChange= { index => {swiperRef.current!.swipeTo(index)}}onClose={() => {setVisible(false)}}/> : null}<div className="proInfo"><div className="priceBox"><span>¥{ obj.originprice }</span><span>销量:{ obj.sales }</span></div><div className="proName"><Tag color='danger'>{ obj.brand }</Tag><Tag color='primary'>{ obj.category }</Tag><span>{ obj.proname }</span></div></div><Dividerstyle={{color: '#1677ff',borderColor: '#1677ff',borderStyle: 'dashed',}}>猜你喜欢</Divider><ProComponent list = { proList } /><Footer proid = { proid! }/></div></>)
};export default Detail;
// src/views/detail/Footer.tsx
import { addCart } from '@/api/cart';
import { Button, Toast } from 'antd-mobile';
import { inject, observer } from 'mobx-react';
import React, { FC } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import './style.scss'
interface IFooterProps {store?: any;// [x: string]: any;proid: string
};const Footer:FC<IFooterProps> = (props) => {// console.log('proid', proid)const navigate = useNavigate()const loginState = props.store.user.loginStateconst userid = props.store.user.useridconst addCartFn = () => {if (loginState) {// 调用加入购物车接口addCart({userid,proid: props.proid,num: 1}).then((res) => {if (res.data.code !== '10119') {Toast.show('加入购物车成功')}})} else {navigate('/login')}}return (<ul className='detailFooter'><li>店铺</li><li><Link to="/cart" >购物车</Link></li><li>收藏</li><li><Button size="small" color="warning" shape="rounded" onClick={ addCartFn }>加入购物车</Button></li><li><Button size="small" color="danger" shape="rounded">立即购买</Button></li></ul>)
};export default inject('store')(observer(Footer));
16.购物车相关
16.1 判断登录状态
实现类似于vue的导航守卫,定义路由时处理
// src/App.tsx
import React, { FC } from 'react';import { Routes, Route, Navigate } from 'react-router-dom'import Home from '@/views/home/Index'
import Kind from '@/views/kind/Index'
import Cart from '@/views/cart/Index'
import User from '@/views/user/Index'
import NotFound from '@/views/error/404'import Footer from '@/components/Footer'import './App.scss';
import Detail from './views/detail/Index';
import Login from './views/login/Index';
import Register from './views/register/Index';
import RegisterStep1 from './views/register/components/Step1';
import RegisterStep2 from './views/register/components/Step2';
import RegisterStep3 from './views/register/components/Step3';
import { inject, observer } from 'mobx-react';
interface IAppProps {store?: any
}const App: FC<IAppProps> = (props) => {return (<div className="container"><div className="box"><Routes><Route path="/" element={ <Navigate to="/home" />} /><Route path="/home" element={<Home />} /><Route path="/kind" element={<Kind />} />{/* <Route path="/cart" element={<Cart />} /> */}<Route path="/cart" element={// localStorage.getItem('loginState') === 'true' ? <Cart /> : <Navigate to="/login" />props.store.user.loginState ? <Cart /> : <Navigate to="/login" replace={true} />} /><Route path="/user" element={<User />} /><Route path="/login" element={<Login />} /><Route path="/register" element={<Register />} ><Route path="/register" element={<Navigate to="/register/step1" replace={true} />} /><Route path="/register/step1" element={<RegisterStep1 />} /><Route path="/register/step2" element={<RegisterStep2 />} /><Route path="/register/step3" element={<RegisterStep3 />} /></Route><Route path="/detail/:proid" element={<Detail />} /><Route path="*" element={<NotFound />} /></Routes></div><Routes><Route path="/home" element={<Footer />} /><Route path="/kind" element={<Footer />} /><Route path="/cart" element={<Footer />} /><Route path="/user" element={<Footer />} /></Routes></div>)
}export default inject('store')(observer(App))
16.2 判断是否有数据
// src/views/cart/Index.tsx
import { getCartListData } from '@/api/cart';
import { Button, Empty, List, Image, Stepper } from 'antd-mobile';
import { inject, observer } from 'mobx-react';
import React, { FC, useEffect, useState } from 'react';interface ICartProps {store?: any
};
interface ICartItem {cartid: stringdiscount: numberflag: booleanimg1: stringnum: numberoriginprice: numberproid: stringproname: stringuserid: string
}
const Cart:FC<ICartProps> = ({ store:{ user: { userid }}}) => {const [cartList, setCartList] = useState<ICartItem[]>([])const [empty, setEmpty] = useState<boolean>(true)const getCartListDataFn = () => {console.log('userid', userid)getCartListData({ userid }).then(res => {console.log(res.data)if (res.data.code === '10020') {setEmpty(true)} else {setEmpty(false)setCartList(res.data.data)}})}useEffect(() => {getCartListDataFn()}, [])return (<><header className="header">cart header</header><div className="content">{empty ?<Emptystyle={{ padding: '64px 0' }}imageStyle={{ width: 128 }}description={ <div><p>购物车空空如也</p><Button color="danger">立即购物</Button></div>}/> : <List>{cartList.map(item => (<List.Itemkey={item.cartid}prefix={<Imagesrc={item.img1}fit='cover'width={40}height={40}/>}description={<div><span style={{ color: '#f66'}}>¥{item.originprice}</span><Stepperstyle={{ float: 'right'}}defaultValue={item.num}onChange={value => {console.log(value)}}/></div>}>{item.proname}</List.Item>))}</List>}</div></>)
};export default inject('store')(observer(Cart));
16.3 数量更新
// src/views/cart/Index.tsx
import { getCartListData, updateOneDataNum } from '@/api/cart';
import { Button, Empty, List, Image, Stepper } from 'antd-mobile';
import { inject, observer } from 'mobx-react';
import React, { FC, useEffect, useState } from 'react';interface ICartProps {store?: any
};
interface ICartItem {cartid: stringdiscount: numberflag: booleanimg1: stringnum: numberoriginprice: numberproid: stringproname: stringuserid: string
}
const Cart:FC<ICartProps> = ({ store:{ user: { userid }}}) => {const [cartList, setCartList] = useState<ICartItem[]>([])const [empty, setEmpty] = useState<boolean>(true)const getCartListDataFn = () => {console.log('userid', userid)getCartListData({ userid }).then(res => {console.log(res.data)if (res.data.code === '10020') {setEmpty(true)} else {setEmpty(false)setCartList(res.data.data)}})}useEffect(() => {getCartListDataFn()}, [])return (<><header className="header">cart header</header><div className="content">{empty ?<Emptystyle={{ padding: '64px 0' }}imageStyle={{ width: 128 }}description={ <div><p>购物车空空如也</p><Button color="danger">立即购物</Button></div>}/> : <List>{cartList.map(item => (<List.Itemkey={item.cartid}prefix={<Imagesrc={item.img1}fit='cover'width={40}height={40}/>}description={<div><span style={{ color: '#f66'}}>¥{item.originprice}</span><Stepperstyle={{ float: 'right'}}defaultValue={item.num}onChange={value => {console.log(value)updateOneDataNum({cartid: item.cartid, num: value}).then(() => {getCartListDataFn()})}}/></div>}>{item.proname}</List.Item>))}</List>}</div></>)
};export default inject('store')(observer(Cart));
16.4 删除
// src/views/cart/Index.tsx
import { getCartListData, removeOneData, updateOneDataNum } from '@/api/cart';
import { Button, Empty, List, Image, Stepper, SwipeAction } from 'antd-mobile';
import { inject, observer } from 'mobx-react';
import React, { FC, useEffect, useState } from 'react';interface ICartProps {store?: any
};
interface ICartItem {cartid: stringdiscount: numberflag: booleanimg1: stringnum: numberoriginprice: numberproid: stringproname: stringuserid: string
}
const Cart:FC<ICartProps> = ({ store:{ user: { userid }}}) => {const [cartList, setCartList] = useState<ICartItem[]>([])const [empty, setEmpty] = useState<boolean>(true)const getCartListDataFn = () => {console.log('userid', userid)getCartListData({ userid }).then(res => {console.log(res.data)if (res.data.code === '10020') {setEmpty(true)} else {setEmpty(false)setCartList(res.data.data)}})}useEffect(() => {getCartListDataFn()}, [])return (<><header className="header">cart header</header><div className="content">{empty ?<Emptystyle={{ padding: '64px 0' }}imageStyle={{ width: 128 }}description={ <div><p>购物车空空如也</p><Button color="danger">立即购物</Button></div>}/> : <List>{cartList.map(item => (<SwipeActionkey={item.cartid}rightActions={[{key: 'delete',text: '删除',color: 'danger',},]}onAction = { ({key}) => {console.log(key)if (key === 'delete') {removeOneData({cartid: item.cartid}).then(() => getCartListDataFn())}}}><List.Itemkey={item.cartid}prefix={<Imagesrc={item.img1}fit='cover'width={40}height={40}/>}description={<div><span style={{ color: '#f66'}}>¥{item.originprice}</span><Stepperstyle={{ float: 'right'}}defaultValue={item.num}onChange={value => {console.log(value)updateOneDataNum({cartid: item.cartid, num: value}).then(() => {getCartListDataFn()})}}/></div>}>{item.proname}</List.Item></SwipeAction>))}</List>}</div></>)
};export default inject('store')(observer(Cart));
16.4 选择
// src/views/cart/Index.tsx
import { getCartListData, removeOneData, selectAllData, selectOneData, updateOneDataNum } from '@/api/cart';
import { Button, Empty, List, Image, Stepper, SwipeAction, Checkbox } from 'antd-mobile';
import { inject, observer } from 'mobx-react';
import React, { FC, useEffect, useState } from 'react';interface ICartProps {store?: any
};
interface ICartItem {cartid: stringdiscount: numberflag: booleanimg1: stringnum: numberoriginprice: numberproid: stringproname: stringuserid: string
}
const Cart:FC<ICartProps> = ({ store:{ user: { userid }}}) => {const [cartList, setCartList] = useState<ICartItem[]>([])const [empty, setEmpty] = useState<boolean>(true)const [checked, setChecked] = useState<boolean>(true)const getCartListDataFn = () => {console.log('userid', userid)getCartListData({ userid }).then(res => {console.log(res.data)if (res.data.code === '10020') {setEmpty(true)} else {setEmpty(false)setCartList(res.data.data)const flag = res.data.data.every((item: ICartItem) => item.flag)setChecked(flag)}})}useEffect(() => {getCartListDataFn()}, [])return (<><header className="header">cart header</header><div className="content">{empty ?<Emptystyle={{ padding: '64px 0' }}imageStyle={{ width: 128 }}description={ <div><p>购物车空空如也</p><Button color="danger">立即购物</Button></div>}/> : <><List>{cartList.map(item => (<SwipeActionkey={item.cartid}rightActions={[{key: 'delete',text: '删除',color: 'danger',},]}onAction = { ({key}) => {console.log(key)if (key === 'delete') {removeOneData({cartid: item.cartid}).then(() => getCartListDataFn())}}}><List.Itemkey={item.cartid}prefix={<div style={{ display: 'flex'}}><div onClick={() => {selectOneData({ cartid: item.cartid, flag: !item.flag}).then(res => {getCartListDataFn()})}}><Checkbox checked={ item.flag }></Checkbox></div><Imagesrc={item.img1}fit='cover'width={80}height={80}/></div>}description={<div><span style={{ color: '#f66'}}>¥{item.originprice}</span><Stepperstyle={{ float: 'right'}}defaultValue={item.num}onChange={value => {console.log(value)updateOneDataNum({cartid: item.cartid, num: value}).then(() => {getCartListDataFn()})}}/></div>}>{item.proname}</List.Item></SwipeAction>))}</List><div style={{position: 'fixed',bottom: '0',width: '100%',height: '0.5rem',backgroundColor: '#ccc',display: 'flex',zIndex: 999}}><div onClick={ event => {event.preventDefault()selectAllData({userid, type: !checked}).then(() => {getCartListDataFn()setChecked(!checked)})}}><Checkbox checked={checked} >全选</Checkbox></div><div><p>总价:</p><p>总数:</p></div><Button color='danger' size="small">提交订单</Button></div></>}</div></>)
};export default inject('store')(observer(Cart));
16.5 计算总价以及总数量
使用useMemo计算属性
// src/views/cart/Index.tsx
import { getCartListData, removeOneData, selectAllData, selectOneData, updateOneDataNum } from '@/api/cart';
import { Button, Empty, List, Image, Stepper, SwipeAction, Checkbox } from 'antd-mobile';
import { inject, observer } from 'mobx-react';
import React, { FC, useEffect, useMemo, useState } from 'react';interface ICartProps {store?: any
};
interface ICartItem {cartid: stringdiscount: numberflag: booleanimg1: stringnum: numberoriginprice: numberproid: stringproname: stringuserid: string
}
const Cart:FC<ICartProps> = ({ store:{ user: { userid }}}) => {const [cartList, setCartList] = useState<ICartItem[]>([])const [empty, setEmpty] = useState<boolean>(true)const [checked, setChecked] = useState<boolean>(true)const totalNum = useMemo(() => {return cartList.reduce((sum, item) => {return item.flag ? sum += item.num : sum += 0}, 0)}, [cartList])const totalPrice = useMemo(() => {return cartList.reduce((sum, item) => {return item.flag ? sum += item.originprice * item.num : sum += 0}, 0)}, [cartList])const getCartListDataFn = () => {console.log('userid', userid)getCartListData({ userid }).then(res => {console.log(res.data)if (res.data.code === '10020') {setEmpty(true)} else {setEmpty(false)setCartList(res.data.data)const flag = res.data.data.every((item: ICartItem) => item.flag)setChecked(flag)}})}useEffect(() => {getCartListDataFn()}, [])return (<><header className="header">cart header</header><div className="content">{empty ?<Emptystyle={{ padding: '64px 0' }}imageStyle={{ width: 128 }}description={ <div><p>购物车空空如也</p><Button color="danger">立即购物</Button></div>}/> : <><List>{cartList.map(item => (<SwipeActionkey={item.cartid}rightActions={[{key: 'delete',text: '删除',color: 'danger',},]}onAction = { ({key}) => {console.log(key)if (key === 'delete') {removeOneData({cartid: item.cartid}).then(() => getCartListDataFn())}}}><List.Itemkey={item.cartid}prefix={<div style={{ display: 'flex'}}><div onClick={() => {selectOneData({ cartid: item.cartid, flag: !item.flag}).then(res => {getCartListDataFn()})}}><Checkbox checked={ item.flag }></Checkbox></div><Imagesrc={item.img1}fit='cover'width={80}height={80}/></div>}description={<div><span style={{ color: '#f66'}}>¥{item.originprice}</span><Stepperstyle={{ float: 'right'}}defaultValue={item.num}onChange={value => {console.log(value)updateOneDataNum({cartid: item.cartid, num: value}).then(() => {getCartListDataFn()})}}/></div>}>{item.proname}</List.Item></SwipeAction>))}</List><div style={{position: 'fixed',bottom: '0',width: '100%',height: '0.5rem',backgroundColor: '#ccc',display: 'flex',zIndex: 999}}><div onClick={ event => {event.preventDefault()selectAllData({userid, type: !checked}).then(() => {getCartListDataFn()setChecked(!checked)})}}><Checkbox checked={checked} >全选</Checkbox></div><div><p>总价:{ totalPrice }</p><p>总数:{ totalNum }</p></div><Button color='danger' size="small">提交订单</Button></div></>}</div></>)
};export default inject('store')(observer(Cart));
16.6 购物车头部
// src/views/cart/Index.tsx
import { getCartListData, removeOneData, selectAllData, selectOneData, updateOneDataNum } from '@/api/cart';
import { Button, Empty, List, Image, Stepper, SwipeAction, Checkbox, NavBar } from 'antd-mobile';
import { inject, observer } from 'mobx-react';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';interface ICartProps {store?: any
};
interface ICartItem {cartid: stringdiscount: numberflag: booleanimg1: stringnum: numberoriginprice: numberproid: stringproname: stringuserid: string
}
const Cart:FC<ICartProps> = ({ store:{ user: { userid }}}) => {const [cartList, setCartList] = useState<ICartItem[]>([])const [empty, setEmpty] = useState<boolean>(true)const [checked, setChecked] = useState<boolean>(true)const totalNum = useMemo(() => {return cartList.reduce((sum, item) => {return item.flag ? sum += item.num : sum += 0}, 0)}, [cartList])const totalPrice = useMemo(() => {return cartList.reduce((sum, item) => {return item.flag ? sum += item.originprice * item.num : sum += 0}, 0)}, [cartList])const getCartListDataFn = () => {console.log('userid', userid)getCartListData({ userid }).then(res => {console.log(res.data)if (res.data.code === '10020') {setEmpty(true)} else {setEmpty(false)setCartList(res.data.data)const flag = res.data.data.every((item: ICartItem) => item.flag)setChecked(flag)}})}useEffect(() => {getCartListDataFn()}, [])const navigate = useNavigate()return (<><header className="header"><NavBarstyle={{'--height': '0.44rem','--border-bottom': '1px #eee solid',color: '#fff'}}onBack={ () => navigate(-1) }>购物车</NavBar></header><div className="content">{empty ?<Emptystyle={{ padding: '64px 0' }}imageStyle={{ width: 128 }}description={ <div><p>购物车空空如也</p><Button color="danger">立即购物</Button></div>}/> : <><List>{cartList.map(item => (<SwipeActionkey={item.cartid}rightActions={[{key: 'delete',text: '删除',color: 'danger',},]}onAction = { ({key}) => {console.log(key)if (key === 'delete') {removeOneData({cartid: item.cartid}).then(() => getCartListDataFn())}}}><List.Itemkey={item.cartid}prefix={<div style={{ display: 'flex'}}><div onClick={() => {selectOneData({ cartid: item.cartid, flag: !item.flag}).then(res => {getCartListDataFn()})}}><Checkbox checked={ item.flag }></Checkbox></div><Imagesrc={item.img1}fit='cover'width={80}height={80}/></div>}description={<div><span style={{ color: '#f66'}}>¥{item.originprice}</span><Stepperstyle={{ float: 'right'}}defaultValue={item.num}onChange={value => {console.log(value)updateOneDataNum({cartid: item.cartid, num: value}).then(() => {getCartListDataFn()})}}/></div>}>{item.proname}</List.Item></SwipeAction>))}</List><div style={{position: 'fixed',bottom: '0',width: '100%',height: '0.5rem',backgroundColor: '#ccc',display: 'flex',zIndex: 999}}><div onClick={ event => {event.preventDefault()selectAllData({userid, type: !checked}).then(() => {getCartListDataFn()setChecked(!checked)})}}><Checkbox checked={checked} >全选</Checkbox></div><div><p>总价:{ totalPrice }</p><p>总数:{ totalNum }</p></div><Button color='danger' size="small">提交订单</Button></div></>}</div></>)
};export default inject('store')(observer(Cart));
17.项目上线
默认执行cnpm run build
打包出来的项目资源以绝对路径方式引入
通过给 package.json
文件中添加"homepage": "./"
更改为相对路径
执行cnpm run build
打包,上传服务器,服务器测试
http://121.89.205.189:3000/m-react/#/home
相关文章:
【React】pro-mobile
1.项目介绍 实现react移动端项目 2.目标: 能够应用CRAReactMobxAntd-mobile开发C端项目掌握基于React的C端项目开发流程学会如何应用next优化项目 3.使用技术栈 脚手架:cra dva-cliumi 脚本:ts react版本:react v18 2022年更…...
Substrate 基础教程(Tutorials) -- 授权特定节点
五、授权特定节点 在添加可信节点中,您看到了如何使用一组已知的验证器节点构建一个简单的网络。该教程演示了一个简化版的许可网络(permissioned network)。在一个被许可的网络中,只有被授权的节点(authorized nodes…...
使用qemu-img转换镜像格式
qemu功能强大,详细了解其功能请到官网查看 https://www.qemu.org/docs/master/system/images.html qemu-img能将RAW、qcow2、VMDK、VDI、VHD(vpc)、VHDX、qcow1或QED格式的镜像转换成VHD格式,也可以实现RAW和VHD格式的互相转换。 …...
Springboot怎么集成Thymeleaf模板引擎?
Thymeleaf介绍Thymeleaf,是一个XML/XHTML/HTML模板引擎,开源的java库,可以用于SpingMVC项目中,用于代替JSP、FreeMarker或者其他的模板引擎;页面与数据分离,提高了开发效率,让代码重用更容易。S…...
LiveGBS国标GB/T28181视频流媒体平台-功能视频集中录制存储云端录像H264|H265|HEVC视频存储
LiveGBS国标GB/T28181视频流媒体平台-视频集中录制存储云端录像H264|H265|HEVC视频存储1、云端录像存储2、手动配置录像2.1、按需录像2.2、一直录像3、录像计划3.1、录像计划入口3.2、新增录像计划3.3、编辑录像计划3.4、关联通道4、查看云端录像4.1、查看录像4.1.1、时间轴模式…...
IntelliJ IDEA如何整合Maven图文教程详解
Maven 1.Maven简述 Maven是一个构建工具,服务与构建.使用Maven配置好项目后,输入简单的命令,如:mvn clean install,Maven会帮我们处理那些繁琐的任务. Maven是跨平台的. Maven最大化的消除了构建的重复. Maven可以帮助我们标准化构建过程.所有的项目都是简单一致的,简化了学习…...
图数据库认证考试 NGCP 错题解析 vol.02:这 10 道题竟无一人全部答对
如果你读过「NebulaGraph 错题解析第一期」,大概知道在错题解析未出来之前,NebulaGraph 专业技能认证 NGCP(全称 NebulaGraph Certified Professional)的通过率仅有 16.7%。但是,经过上一轮 NebulaGraph 认证考试出题人…...
188888
81. 一个敏捷项目正在进行八次迭代中的第五次迭代。在最后一次迭代计划之后,团队得知市场上出现一个新的竞争对手,有必要更快地加快进程来确保不失去市场份额。 项目经理应该怎么做? A 将竞争对手的功能添加到产品积压待办清单中,…...
华为机试题:HJ99 自守数(python)
文章目录(1)题目描述(2)Python3实现(3)知识点详解1、input():获取控制台(任意形式)的输入。输出均为字符串类型。1.1、input() 与 list(input()) 的区别、及其相互转换方…...
如何提高推广邮件的发送成功率?
随着经济的发展,国际之间的贸易往来越加频繁,很多外贸企业需要发送大量的商业推广邮件,来获得销售订单开拓公司业务市场。 随之而来的问题也是越来越多,给众多的外贸企业带来诸多的困扰。外贸企业在发送推广邮件中究竟会遇到什么问…...
关于提高PX4抗风性
滚转角速率控制器:(MC_ROLLRATE_P, MC_ROLLRATE_I, MC_ROLLRATE_D) 滚转角速率控制器:(MC_PITCHRATE_P, MC_PITCHRATE_I,MC_PITCHRATE_D) 滚转角速率控制器…...
AVL 树实现
AVL 树的概念 也许因为插入的值不够随机,也许因为经过某些插入或删除操作,二叉搜索树可能会失去平衡,甚至可能退化为单链表,造成搜索效率低。 AVL Tree 是一个「加上了额外平衡条件」的二叉搜索树,其平衡条件的建立是…...
跟我学c++高级篇——模板元编程之八惰性加载
一、Lazy evaluation 惰性加载或者延迟计算,在前面的文章《跟我学c中级篇——迟延计算》中分析过。叫法怎么叫都可以,只要大家明白这个意思即可。Lazy evaluation一般可用于下面的情况: 1、模板中的对象非立刻的模板实例化,也就是…...
【Python入门第二十二天】Python 类和对象
Python 类/对象 Python 是一种面向对象的编程语言。 Python 中的几乎所有东西都是对象,拥有属性和方法。 类(Class)类似对象构造函数,或者是用于创建对象的“蓝图”。 创建类 如需创建类,请使用 class 关键字&…...
qml的进度条
QML是一种用于创建动态用户界面的声明式语言,它支持使用JavaScript表达式来定义属性绑定和信号处理器。在本文中,我们将介绍如何使用JavaScript在QML中绘制一个进度条(ProgressBar),并设置其前景色和背景色。进度条是一…...
Pycharm补丁包使用教程
虽然社区版在大多情况下已经够用,但是有很多功能都是没有的,对照起一些教程之类的就很不方便 现在直接教一种简单中的简单的补丁包使用方法 我这里用的是 pycharm 19.2.6 注意右下角的configure 一般别的方法都是 打开,然后添加路径&#…...
用VAE生成图像
用VAE生成图像自编码器AE,auto-encoderVAE讲讲为什么是log_var为什么要用重参数化技巧用VAE生成图像变分自编码器是自编码器的改进版本,自编码器AE是一种无监督学习,但它无法产生新的内容,变分自编码器对其潜在空间进行拓展&#…...
你只会说MVC模型是什么但是不会实现?今天带你走通Web、Servlet、MVC、SpringMVC。代码演示很清晰
文章目录HTTP请求和HTTP响应从0手写一个Web服务器,看看能有多累人使用Servlet实现一个服务器,看看多简单Serlvet的创建Servlet的运行Servlet的其他问题Servlet这么爽,我们简单地探索一下它的原理JSP跟Servlet合作啦,我们来看一下他…...
C++中邻接矩阵、邻接表、链式前向星具体用法及讲解
图论在提高组中几乎占据半壁江山,而今天要讲的就是如何存储一个图一.邻接矩阵原理要建立一个图,根本的要素就是边和点而想要让计算机存储边和点就需要用到一些数据结构邻接矩阵是最简单的他使用了一个二维数组,来表示一个图假设数组名为map那…...
appium的安装详解
安装appium 爬虫手机APP需要实现自动化,所以要使用appnium来实现点击,输入,滑动等操作。由于appnium的安装较为繁琐,所以特意整理一篇文章来展示安装的详细过程过程中。 安装appnium共有3个步骤 安装 Android SDK安装 JDK安装 …...
STM32之 串口
串口通信串行接口简称串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方 式的扩展接口。串行接口(Serial Interface)是指数据一位一位地顺序传送。其特点是通信线路简 单,只…...
CSDN 编程竞赛三十三期题解
竞赛总览 CSDN 编程竞赛三十三期题解:比赛详情 (csdn.net) 竞赛题解 题目1、奇偶排序 给定一个存放整数的数组,重新排列数组使得数组左边为奇数,右边为偶数(奇数和偶数的顺序根据输入的数字顺序排列)。 第七期竞赛…...
逆向练习之 mingyue.exe wp
目录 一.查壳 二.主函数 三.operate函数 四.storage函数及4618和4620指针功能的解释 五.judge函数 六.求解flag 七.其他--ida字符识别问题 一.查壳 64位无壳 二.主函数 1.这里的pointer_4618和4620是两个相邻的八字节内存单元,其中4620是字符串链表表头head 2.puts和s…...
LeetCode 热题 HOT 100 Java 题解 -- Part 3
练习地址 Part 1 : https://blog.csdn.net/qq_41080854/article/details/128829494 Part 2 : https://blog.csdn.net/qq_41080854/article/details/129278336 LeetCode 热题 HOT 100 Java 题解 -- Part 376. 最佳买卖股票时机含冷冻期77. 戳气球78. 零钱兑换79. 打家劫舍 III…...
QML键盘事件
在QML中,当有一个按键按下或释放时,会产生一个键盘事件,将其传递给获得有焦点的QML项目(讲focus属性设置为true,则获得焦点)。 按键处理的基本流程: Qt接收密钥操作并生成密钥事件。如果 QQuic…...
跨域问题怎么解决
解决跨域,原因:域名不同,域名相同端口不同;二级域名不同 什么是跨域? 就是两个项目之间通讯,如果访问的域名与ajax访问的地址不一致情况,默认情况浏览器有一个安全机制。 postman不一定能测试…...
微服务网关Gateway和Zuul的区别
spring-cloud-Gateway是spring-cloud的一个子项目。而zuul则是netflix公司的项目,只是spring将zuul集成在spring-cloud中使用而已。 因为zuul2.0连续跳票和zuul1的性能表现不是很理想,所以催生了spring团队开发了Gateway项目。 Zuul: 使用的…...
专访华西二院吴邦华:隐私计算+AI全栈技术,构筑智慧医院建设的坚实数据底座|爱分析访谈
从IT时代步入DT时代,医疗大数据成为智慧医院建设的重要驱动力。经过多年信息化系统建设,很多医院已经积累了大量的医疗数据资源,但由于各业务系统间数据孤岛化严重、系统架构落后、数据缺乏深度治理等问题存在,导致现有数据深度及…...
《C++ Primer Plus》第18章:探讨 C++ 新标准(6)
可变参数模板 可变参数模板(variadic template)让您能够创建这样的模板函数和模板类,即可接收可变数量的参数。这里介绍可变参数模板函数。例如,假设要编写一个函数,它可接受任意数量的参数,参数的类型只需…...
.Net Core中使用是SQL Server的邮件发送功能
.Net Core中使用是sqlserver的邮件发送功能准备需求启用SQL Server的电子邮件功能检查和测试在.net Core中调用在sqlsrver的管理中有一个数据库邮件功能,再此可以使用sqlserver来自动发送一些邮件,但是有一些需要插入附件的邮件则需要使用程序代码来解决,下面就是使用C#来调用s…...
网站排名做不上去吗/百度云盘下载
一、防sql注入办法 在apache commons-lang(2.3以上版本)中为我们提供了一个方便做转义的工具类,主要是为了防止sql注入,xss注入攻击的功能。总共提供了以下几个方法: 1.escapeSql 提供sql转移功能,防止sql注入攻击,例如…...
合肥网站建设网站模板/广告投放都有哪些平台
开发工具下载地址...
网站建设方案封面/湘潭网页设计
Asciidoc中的“”号可以在文中任意位置存在,而成对的""号会对其中间的文字起到着色的效果,比如以下的正文: - 熟悉C语言,熟悉STL并了解对象模型,熟悉c0x常用特性,了解Qt基础 转化之后会出现这样的显示效果&a…...
江苏省建设工人考勤网站/cms系统
在接口的地方加上请求头。//跨域请求header(Access-Control-Allow-Origin:*);不要在ajax里面加!!!!!!!!!转载于:https://www.cnblogs.com/lyc94620/p/9112529.html...
怎么区分网站是模板做的/淘宝新店怎么快速做起来
为什么80%的码农都做不了架构师?>>> canvas 英音 /knvəs/ 美音 /knvəs/ 帆布, 画布 canvas的基本介绍 canvas是html5中新增的一个画布标签。这个标签的默认宽高为300*150设置canvas标签的宽高需要使用表格的形式,width和height…...
福州公司建设网站/现在百度怎么优化排名
头文件<functional> 预定义的function object 对对象排序或比较时,默认以less<>为比较准则,因此默认的排序操作总是产生升序. … … Function Adapter和Binder 所谓的Function adapter(函数适配器,函数改造器),指能够将不通的…...