【React】pro-mobile
创始人
2024-05-28 13:22:34
0

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(
);reportWebVitals();
// src/App.tsx
import React, { FC } from 'react';interface IAppProps {
}const App: FC = (props) => {return (<>App)
}export default App
// src/ErrorBundary.tsx
import React from 'react'
// 如何给类组件添加类型注解
interface IState {hasError: boolean
}
class ErrorBoundary extends React.Component  {
// 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 

代码出错了,请仔细检查一下

;}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 = (props) => {return (
) }export default App

6.构建项目基本页面

思考每个页面的头部和内容区域是根据用户的选择而一起改变的,那么可以创建以下四个基本页面

6.1 构建首页面

// src/views/home/Index.tsx
import React, { FC } from 'react';interface IHomeProps {
};const Home:FC = () => {return (<>
home header
home content
) };export default Home;

6.2 构建分类页面

// src/views/kind/Index.tsx
import React, { FC } from 'react';interface IKindProps {};const Kind:FC = () => {return (<>
kind header
kind content
) };export default Kind;

6.3 构建购物车页面

// src/views/cart/Index.tsx
import React, { FC } from 'react';interface ICartProps {};const Cart:FC = () => {return (<>
cart header
cart content
) };export default Cart;

6.4 构建个人中心页面

// src/views/user/Index.tsx
import React, { FC } from 'react';interface IUserProps {};const User:FC = () => {return (<>
user header
user content
) };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(
);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 = (props) => {return (
} />} />} />} />} />} />
) }export default App

此时地址栏分别输入
http://localhost:3000/homehttp://localhost:3000/kindhttp://localhost:3000/carthttp://localhost:3000/user
查看项目运行结果,
可以得知已经可以通过路由显示不同的页面
但是用户一般都是通过底部选项卡来切换页面的

7.构建页面底部组件

src文件夹下创建components文件夹,在components文件夹下创建底部组件

因为底部选项卡需要字体图标,可以选择 iconfont阿里字体图标库,搜索图标,加入购物车,添加至项目mobile-vue-app,选择font-class,点击查看在线链接,拷贝css链接

项目根目录下public/index.html中引入css链接


React App

底部组件展示如下:

// src/components/Footer.tsx
import React, { FC } from 'react';interface IFooterProps {};const Footer:FC = () => {return (
  • 首页

  • 分类

  • 购物车

  • 我的

) };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 = () => {return (
    isActive ? { color: '#f66' }: undefined }>

    首页

    isActive ? { color: '#f66' }: undefined }>

    分类

    isActive ? { color: '#f66' }: undefined }>

    购物车

    isActive ? { color: '#f66' }: undefined }>

    我的

) };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 = () => {return (<>

轮播图

) };export default BannerComponent;
// src/views/home/Index.tsx
import React, { FC } from 'react';
import BannerComponent from './components/BannerComponent';interface IHomeProps {
};const Home:FC = () => {return (<>
home header
) };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 = () => {const [bannerList, setBannerList] = useState([])useEffect(() => {getBannerList().then(res => setBannerList(res.data.data))}, [])return (<>
home header
) };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 = ({ bannerList }) => {return (<>{ height: '1.5rem', overflow: 'hidden' }}>{bannerList.map((item) => ())})
};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 = () => {const [bannerList, setBannerList] = useState([])const [seckillList, setSeckillList] = useState([])useEffect(() => {getBannerList().then(res => setBannerList(res.data.data))getSeckilllist().then(res => setSeckillList(res.data.data))}, [])return (<>
home header
) };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 = ({ list }) => {return (<>{list.map(item => ({ width: 50, height: 50 }}/>

{ item.title }

))}
) };export default NavComponent;

11.4 构建秒杀列表实现

// src/views/home/components/SeckillComponent.tsx
import React, { FC } from 'react';interface ISeckillComponentProps {};const SeckillComponent:FC = () => {return (<>

秒杀列表

) };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 = () => {const [bannerList, setBannerList] = useState([])const [seckillList, setSeckillList] = useState([])useEffect(() => {getBannerList().then(res => setBannerList(res.data.data))getSeckilllist().then(res => setSeckillList(res.data.data))}, [])return (<>
home header
) };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 = ({ list }) => {return (<>{ list.map(item => {return ({width: 55, height: 55}}/>

{ color: '#f66', textAlign: 'center' }}>¥{ item.originprice }

)})}
) };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 = ({ list }) => {return (<>

产品列表

) };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 = () => {const [bannerList, setBannerList] = useState([])const [seckillList, setSeckillList] = useState([])const [proList, setProList] = useState([])useEffect(() => {getBannerList().then(res => setBannerList(res.data.data))getSeckilllist().then(res => setSeckillList(res.data.data))getProList().then(res => setProList(res.data.data))}, [])return (<>
home header
) };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 (
{ context.proname }
¥{ context.originprice }
) } const ProComponent:FC = memo(({ list }) => {console.log(list)return (<>
    { float: 'left', width: '48%', margin: '5px 1%'}}>{list.map((item, index) => {return index % 2 === 0 ? : null})}
    { float: 'left', width: '48%', margin: '5px 1%'}}>{list.map((item, index) => {return index % 2 === 1 ? : null})}
) });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 = () => {const [bannerList, setBannerList] = useState([])const [seckillList, setSeckillList] = useState([])const [proList, setProList] = useState([])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 (<>
home header
) };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 = () => {const [bannerList, setBannerList] = useState([])const [seckillList, setSeckillList] = useState([])const [proList, setProList] = useState([])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 (<>
home header
{const res = await getProList()setProList(res.data.data)setHasMore(true)setCount(2)}}>
) };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 = () => {const [bannerList, setBannerList] = useState([])const [seckillList, setSeckillList] = useState([])const [proList, setProList] = useState([])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()const backTop = () => {contentRef.current.scrollTop = 0}return (<>
home header
{setTop((event.target as HTMLDivElement).scrollTop)}}> {const res = await getProList()setProList(res.data.data)setHasMore(true)setCount(2)}}>{top > 300 &&
}
) };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([])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([])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([])  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()const backTop = () => {contentRef.current.scrollTop = 0}const scroll = (event: React.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 = () => {const { bannerList } = useBanner()const { navList } = useNav()const { seckillList } = useSeckill()const { proList, loadMore, pullRefresh, hasMore } = usePro()const { contentRef, scroll, top, backTop } = useBackTop()return (<>
home header
{top > 300 &&
}
) };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 = ({}) => {return (
  • 西安
  • |立柜式空调
  • 登录
) };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 = () => {const { bannerList } = useBanner()const { navList } = useNav()const { seckillList } = useSeckill()const { proList, loadMore, pullRefresh, hasMore } = usePro()const { contentRef, scroll, top, backTop } = useBackTop()return (<>{/* 
home header
*/}
{top > 300 &&
}
) };export default Home;

12.实现详情

12.1 构建详情页面以及路由

  • 构建详情页面组件
// src/views/detail/Index.tsx
import React, { FC } from 'react';interface IDetailProps {
};const Detail:FC = () => {return (<>
detail header
detail content
) };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 = (props) => {return (
} />} />} />} />} />} />} />
) }export default App

通过访问地址发现可以跳转到详情,但是详情页面不应有 底部选项卡,需要处理

// src/components/Footer.tsx
import React, { FC } from 'react';import { NavLink } from 'react-router-dom'interface IFooterProps {};const Footer:FC = () => {return (
    isActive ? { color: '#f66' }: undefined }>

    首页

    isActive ? { color: '#f66' }: undefined }>

    分类

    isActive ? { color: '#f66' }: undefined }>

    购物车

    isActive ? { color: '#f66' }: undefined }>

    我的

) };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 = (props) => {return (
} />} />} />} />} />} />} />
} />} />} />} />
) }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 = ({ list }) => {return (<>{ list.map(item => {return ({width: 55, height: 55}}/>

{ color: '#f66', textAlign: 'center' }}>¥{ item.originprice }

)})}
) };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 (
{navigate('/detail/' + context.proid)}}>
{maxHeight: '1.8rem'}}/>
{ maxHeight: '36px', overflow: 'hidden',}}>{ context.proname }
¥{ context.originprice }
) } const ProComponent:FC = memo(({ list }) => {console.log(list)return (<>
    { float: 'left', width: '48%', margin: '5px 1%'}}>{list.map((item, index) => {return index % 2 === 0 ? : null})}
    { float: 'left', width: '48%', margin: '5px 1%'}}>{list.map((item, index) => {return index % 2 === 1 ? : null})}
) });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 = () => {// const params = useParams()// console.log(params)const { proid } = useParams()return (<>
detail header
detail content
) };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 = () => {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 (<>
Detail header
Detail content
) };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 = () => {// const params = useParams()// console.log(params)const { proid } = useParams()const [obj, setObj] = useState({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()return (<>
detail header
{obj.banners.map((item, index) => {return ( {setCurrent(index)setVisible(true)// swiperRef.current.swipeTo(index, true)}}>)})}{visible ? {swiperRef.current.swipeTo(index)}}onClose={() => {setVisible(false)}}/> : null}
) };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)







public/index.html


React App

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 = () => {// const params = useParams()// console.log(params)const { proid } = useParams()const [obj, setObj] = useState({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()return (<>
detail header
{obj.banners.map((item, index) => {return ( {setCurrent(index)setVisible(true)// swiperRef.current.swipeTo(index, true)}}>)})}{visible ? {swiperRef.current.swipeTo(index)}}onClose={() => {setVisible(false)}}/> : null}
¥{ obj.originprice }销量:{ obj.sales }
{ obj.brand }{ obj.category }{ obj.proname }
) };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 (
{navigate('/detail/' + context.proid)}}>
{maxHeight: '1.8rem'}}/>
{ maxHeight: '36px', overflow: 'hidden',}}>{ context.proname }
¥{ context.originprice }
) } const ProComponent:FC = memo(({ list }) => {console.log(list)return (<>
    { float: 'left', width: '48%', margin: '5px 1%'}}>{list.map((item, index) => {return index % 2 === 0 ? : null})}
    { float: 'left', width: '48%', margin: '5px 1%'}}>{list.map((item, index) => {return index % 2 === 1 ? : null})}
) });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 = () => {// const params = useParams()// console.log(params)const { proid } = useParams()const [obj, setObj] = useState({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()const [proList, setProList] = useState([]) // ++++useEffect(() => { // ++++getRecommendList().then(res => setProList(res.data.data))}, [])return (<>
detail header
{obj.banners.map((item, index) => {return ( {setCurrent(index)setVisible(true)// swiperRef.current.swipeTo(index, true)}}>)})}{visible ? {swiperRef.current.swipeTo(index)}}onClose={() => {setVisible(false)}}/> : null}
¥{ obj.originprice }销量:{ obj.sales }
{ obj.brand }{ obj.category }{ obj.proname }
{color: '#1677ff',borderColor: '#1677ff',borderStyle: 'dashed',}}>猜你喜欢
) };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 = () => {return (
  • 店铺
  • 购物车
  • 收藏
) };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 = () => {// const params = useParams()// console.log(params)const { proid } = useParams()const [obj, setObj] = useState({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()const [proList, setProList] = useState([]) // ++++useEffect(() => { // ++++getRecommendList().then(res => setProList(res.data.data))}, [])return (<>
detail header
{obj.banners.map((item, index) => {return ( {setCurrent(index)setVisible(true)// swiperRef.current.swipeTo(index, true)}}>)})}{visible ? {swiperRef.current.swipeTo(index)}}onClose={() => {setVisible(false)}}/> : null}
¥{ obj.originprice }销量:{ obj.sales }
{ obj.brand }{ obj.category }{ obj.proname }
{color: '#1677ff',borderColor: '#1677ff',borderStyle: 'dashed',}}>猜你喜欢
) };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 = ({}) => {const navigate = useNavigate()return (
300">
  • navigate(-1) } >
  • 详情推荐
) };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 = () => {// const params = useParams()// console.log(params)const { proid } = useParams()const [obj, setObj] = useState({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()const [proList, setProList] = useState([]) // ++++useEffect(() => { // ++++getRecommendList().then(res => setProList(res.data.data))}, [])return (<>{/* 
detail header
*/}
{obj.banners.map((item, index) => {return ( {setCurrent(index)setVisible(true)// swiperRef.current.swipeTo(index, true)}}>)})}{visible ? {swiperRef.current.swipeTo(index)}}onClose={() => {setVisible(false)}}/> : null}
¥{ obj.originprice }销量:{ obj.sales }
{ obj.brand }{ obj.category }{ obj.proname }
{color: '#1677ff',borderColor: '#1677ff',borderStyle: 'dashed',}}>猜你喜欢
) };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 = () => {const navigate = useNavigate()return (<>

注册第一步

) };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 = () => {const navigate = useNavigate()return (<>

注册第二步

) };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 = () => {const navigate = useNavigate()return (<>

注册第三步

) };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 = () => {return (<>
Register header
) };export default Register;

13.2 构建登录组件

// src/views/login/Index.tsx
import React, { FC } from 'react';interface ILoginProps {
};const Login:FC = () => {return (<>
Login header
Login content
) };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 = (props) => {return (
} />} />} />} />} />} />} >} />} />} />} />} />} />
} />} />} />} />
) }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 = () => {const loginFn = (values: any) => {console.log(values)}return (<>
Login header
提交}onFinish = { loginFn }>
) };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 = () => {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 (<>
Login header
提交}onFinish = { loginFn }>
) };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( store }>
);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 = (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 (<>
Login header
提交}onFinish = { loginFn }>
) };// 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 = () => {// const params = useParams()// console.log(params)const { proid } = useParams()const [obj, setObj] = useState({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()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 (<>{/* 
detail header
*/}
{obj.banners && {obj.banners && obj.banners.map((item, index) => {return ( {setCurrent(index)setVisible(true)// swiperRef.current.swipeTo(index, true)}}>)})} }{visible ? {swiperRef.current!.swipeTo(index)}}onClose={() => {setVisible(false)}}/> : null}
¥{ obj.originprice }销量:{ obj.sales }
{ obj.brand }{ obj.category }{ obj.proname }
{color: '#1677ff',borderColor: '#1677ff',borderStyle: 'dashed',}}>猜你喜欢
) };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 = (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 (
  • 店铺
  • 购物车
  • 收藏
) };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 = (props) => {return (
} />} />} />{/* } /> */} : props.store.user.loginState ? : } />} />} />} >} />} />} />} />} />} />
} />} />} />} />
) }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 = ({ store:{ user: { userid }}}) => {const [cartList, setCartList] = useState([])const [empty, setEmpty] = useState(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 (<>
cart header
{empty ?{ padding: '64px 0' }}imageStyle={{ width: 128 }}description={

购物车空空如也

}/> : {cartList.map(item => (}description={
{ color: '#f66'}}>¥{item.originprice}{ float: 'right'}}defaultValue={item.num}onChange={value => {console.log(value)}}/>
}>{item.proname}))}
}
) };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 = ({ store:{ user: { userid }}}) => {const [cartList, setCartList] = useState([])const [empty, setEmpty] = useState(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 (<>
cart header
{empty ?{ padding: '64px 0' }}imageStyle={{ width: 128 }}description={

购物车空空如也

}/> : {cartList.map(item => (}description={
{ color: '#f66'}}>¥{item.originprice}{ float: 'right'}}defaultValue={item.num}onChange={value => {console.log(value)updateOneDataNum({cartid: item.cartid, num: value}).then(() => {getCartListDataFn()})}}/>
}>{item.proname}))}
}
) };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 = ({ store:{ user: { userid }}}) => {const [cartList, setCartList] = useState([])const [empty, setEmpty] = useState(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 (<>
cart header
{empty ?{ padding: '64px 0' }}imageStyle={{ width: 128 }}description={

购物车空空如也

}/> : {cartList.map(item => ( {console.log(key)if (key === 'delete') {removeOneData({cartid: item.cartid}).then(() => getCartListDataFn())}}}>}description={
{ color: '#f66'}}>¥{item.originprice}{ float: 'right'}}defaultValue={item.num}onChange={value => {console.log(value)updateOneDataNum({cartid: item.cartid, num: value}).then(() => {getCartListDataFn()})}}/>
}>{item.proname}))}
}
) };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 = ({ store:{ user: { userid }}}) => {const [cartList, setCartList] = useState([])const [empty, setEmpty] = useState(true)const [checked, setChecked] = useState(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 (<>
cart header
{empty ?{ padding: '64px 0' }}imageStyle={{ width: 128 }}description={

购物车空空如也

}/> : <>{cartList.map(item => ( {console.log(key)if (key === 'delete') {removeOneData({cartid: item.cartid}).then(() => getCartListDataFn())}}}>{ display: 'flex'}}>
{selectOneData({ cartid: item.cartid, flag: !item.flag}).then(res => {getCartListDataFn()})}}>
}description={
{ color: '#f66'}}>¥{item.originprice}{ float: 'right'}}defaultValue={item.num}onChange={value => {console.log(value)updateOneDataNum({cartid: item.cartid, num: value}).then(() => {getCartListDataFn()})}}/>
}>{item.proname}))}
{position: 'fixed',bottom: '0',width: '100%',height: '0.5rem',backgroundColor: '#ccc',display: 'flex',zIndex: 999}}>
{event.preventDefault()selectAllData({userid, type: !checked}).then(() => {getCartListDataFn()setChecked(!checked)})}}>全选

总价:

总数:

}
) };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 = ({ store:{ user: { userid }}}) => {const [cartList, setCartList] = useState([])const [empty, setEmpty] = useState(true)const [checked, setChecked] = useState(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 (<>
cart header
{empty ?{ padding: '64px 0' }}imageStyle={{ width: 128 }}description={

购物车空空如也

}/> : <>{cartList.map(item => ( {console.log(key)if (key === 'delete') {removeOneData({cartid: item.cartid}).then(() => getCartListDataFn())}}}>{ display: 'flex'}}>
{selectOneData({ cartid: item.cartid, flag: !item.flag}).then(res => {getCartListDataFn()})}}>
}description={
{ color: '#f66'}}>¥{item.originprice}{ float: 'right'}}defaultValue={item.num}onChange={value => {console.log(value)updateOneDataNum({cartid: item.cartid, num: value}).then(() => {getCartListDataFn()})}}/>
}>{item.proname}))}
{position: 'fixed',bottom: '0',width: '100%',height: '0.5rem',backgroundColor: '#ccc',display: 'flex',zIndex: 999}}>
{event.preventDefault()selectAllData({userid, type: !checked}).then(() => {getCartListDataFn()setChecked(!checked)})}}>全选

总价:{ totalPrice }

总数:{ totalNum }

}
) };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 = ({ store:{ user: { userid }}}) => {const [cartList, setCartList] = useState([])const [empty, setEmpty] = useState(true)const [checked, setChecked] = useState(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 (<>
{'--height': '0.44rem','--border-bottom': '1px #eee solid',color: '#fff'}}onBack={ () => navigate(-1) }>购物车
{empty ?{ padding: '64px 0' }}imageStyle={{ width: 128 }}description={

购物车空空如也

}/> : <>{cartList.map(item => ( {console.log(key)if (key === 'delete') {removeOneData({cartid: item.cartid}).then(() => getCartListDataFn())}}}>{ display: 'flex'}}>
{selectOneData({ cartid: item.cartid, flag: !item.flag}).then(res => {getCartListDataFn()})}}>
}description={
{ color: '#f66'}}>¥{item.originprice}{ float: 'right'}}defaultValue={item.num}onChange={value => {console.log(value)updateOneDataNum({cartid: item.cartid, num: value}).then(() => {getCartListDataFn()})}}/>
}>{item.proname}))}
{position: 'fixed',bottom: '0',width: '100%',height: '0.5rem',backgroundColor: '#ccc',display: 'flex',zIndex: 999}}>
{event.preventDefault()selectAllData({userid, type: !checked}).then(() => {getCartListDataFn()setChecked(!checked)})}}>全选

总价:{ totalPrice }

总数:{ totalNum }

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

相关内容

热门资讯

【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
AsusVivobook无法开... 首先,我们可以尝试重置BIOS(Basic Input/Output System)来解决这个问题。...
ASM贪吃蛇游戏-解决错误的问... 要解决ASM贪吃蛇游戏中的错误问题,你可以按照以下步骤进行:首先,确定错误的具体表现和问题所在。在贪...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...