怎么看网站建设时间,网页版微信小程序,北京seo公司wyhseo,做网站都需要租服务器吗1.项目介绍
实现react移动端项目
2.目标#xff1a;
能够应用CRAReactMobxAntd-mobile开发C端项目掌握基于React的C端项目开发流程学会如何应用next优化项目
3.使用技术栈 脚手架#xff1a;cra dva-cliumi 脚本#xff1a;ts react版本#xff1a;react v18 2022年更…1.项目介绍
实现react移动端项目
2.目标
能够应用CRAReactMobxAntd-mobile开发C端项目掌握基于React的C端项目开发流程学会如何应用next优化项目
3.使用技术栈 脚手架cra dva-cliumi 脚本ts react版本react v18 2022年更新 react 17 路由react-router v6 2021年10-11月 react-router v5 状态管理器mobx v6 reduxredux react-reduxredux react-redux 分模块redux react-redux 分模块 redux-thunkredux react-redux 分模块 redux-sagaredux react-redux 分模块 redux-thunk immutable redux-immutableredux react-redux 分模块 redux-saga immutable redux-immutablertkmobx v6 组件库antd-mobile v5 http://ant-design-mobile.antgroup.com/zh更像是vant UI库了 hooks
4.构建项目
$ npx create-react-app react-mobile-app --template typescript4.1 是否抽离配置文件
一般企业级项目很少会直接抽离配置文件 抽离配置文件目的对webpack进行二次封装 推荐使用 craco 进行覆盖 4.2 使用craco覆盖webpack配置
https://www.npmjs.com/package/craco/craco
$ cnpm i craco/craco -D为了支持 commonjs 规范安装如下模块
$ cnpm i types/node -Dtypes/*这种文件称之为 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 start4.3 确定项目 css 预处理器
https://create-react-app.bootcss.com/docs/adding-a-sass-stylesheet
$ cnpm i node-sass sass -Dcra 默认自带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.StrictModeErrorBoundaryApp //ErrorBoundary/React.StrictMode
);reportWebVitals();
// src/App.tsx
import React, { FC } from react;interface IAppProps {
}const App: FCIAppProps (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 ErrorBoundary5 构建项目基本结构
// 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: FCIAppProps (props) {return (div classNamecontainerdiv classNameboxheader classNameheader/headerdiv classNamecontent/div/divfooter classNamefooter/footer/div)
}export default App6.构建项目基本页面
思考每个页面的头部和内容区域是根据用户的选择而一起改变的那么可以创建以下四个基本页面
6.1 构建首页面
// src/views/home/Index.tsx
import React, { FC } from react;interface IHomeProps {
};const Home:FCIHomeProps () {return (header classNameheaderhome header/headerdiv classNamecontenthome content/div/)
};export default Home;6.2 构建分类页面
// src/views/kind/Index.tsx
import React, { FC } from react;interface IKindProps {};const Kind:FCIKindProps () {return (header classNameheaderkind header/headerdiv classNamecontentkind content/div/)
};export default Kind;6.3 构建购物车页面
// src/views/cart/Index.tsx
import React, { FC } from react;interface ICartProps {};const Cart:FCICartProps () {return (header classNameheadercart header/headerdiv classNamecontentcart content/div/)
};export default Cart;6.4 构建个人中心页面
// src/views/user/Index.tsx
import React, { FC } from react;interface IUserProps {};const User:FCIUserProps () {return (header classNameheaderuser header/headerdiv classNamecontentuser 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-domimport ErrorBoundary from ./ErrorBundary;
import App from ./App;
import reportWebVitals from ./reportWebVitals;const root ReactDOM.createRoot(document.getElementById(root) as HTMLDivElement
);
root.render(React.StrictModeErrorBoundaryHashRouterApp //HashRouter/ErrorBoundary/React.StrictMode
);reportWebVitals();
// src/App.tsx
import React, { FC } from react;import { Routes, Route, Navigate } from react-router-domimport 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/404import ./App.scss;
interface IAppProps {
}const App: FCIAppProps (props) {return (div classNamecontainerdiv classNameboxRoutesRoute 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/divfooter classNamefooter/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 langenheadmeta charsetutf-8 /link relicon href%PUBLIC_URL%/favicon.ico /meta nameviewport contentwidthdevice-width, initial-scale1 /meta nametheme-color content#000000 /metanamedescriptioncontentWeb site created using create-react-app/link relapple-touch-icon href%PUBLIC_URL%/logo192.png /!--manifest.json provides metadata used when your web app is installed on ausers mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/--link relmanifest 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.--titleReact App/titlelink relstylesheet href//at.alicdn.com/t/c/font_3665887_h3lsrioddkk.css
/headbodynoscriptYou need to enable JavaScript to run this app./noscriptdiv idroot/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:FCIFooterProps () {return (ullispan classNameiconfont icon-shouye/spanp首页/p/lilispan classNameiconfont icon-fenlei/spanp分类/p/lilispan classNameiconfont icon-gouwuche/spanp购物车/p/lilispan classNameiconfont icon-shouye1/spanp我的/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-dominterface IFooterProps {};const Footer:FCIFooterProps () {return (ulNavLink to/home style {({ isActive }) isActive ? { color: #f66 }: undefined }span classNameiconfont icon-shouye/spanp首页/p/NavLinkNavLink to/kind style {({ isActive }) isActive ? { color: #f66 }: undefined }span classNameiconfont icon-fenlei/spanp分类/p/NavLinkNavLink to/cart style {({ isActive }) isActive ? { color: #f66 }: undefined }span classNameiconfont icon-gouwuche/spanp购物车/p/NavLinkNavLink to/user style {({ isActive }) isActive ? { color: #f66 }: undefined }span classNameiconfont icon-shouye1/spanp我的/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 ins11.构建首页
11.1 封装首页相关数据请求
// src/api/home.ts
import request from /utils/requestinterface 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:FCIBannerComponentProps () {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:FCIHomeProps () {return (header classNameheaderhome header/headerdiv classNamecontentBannerComponent //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:FCIHomeProps () {const [bannerList, setBannerList] useStateIBanner[]([])useEffect(() {getBannerList().then(res setBannerList(res.data.data))}, [])return (header classNameheaderhome header/headerdiv classNamecontentBannerComponent 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:FCIBannerComponentProps ({ bannerList }) {return (Swiper style{{ height: 1.5rem, overflow: hidden }}{bannerList.map((item) (Swiper.Item key{ item.bannerid }Image style{ { width: 100% } } src{ item.img } fitcover//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/navinterface 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:FCIHomeProps () {const [bannerList, setBannerList] useStateIBanner[]([])const [seckillList, setSeckillList] useStateIPro[]([])useEffect(() {getBannerList().then(res setBannerList(res.data.data))getSeckilllist().then(res setSeckillList(res.data.data))}, [])return (header classNameheaderhome header/headerdiv classNamecontentBannerComponent 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:FCINavComponentProps ({ 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:FCISeckillComponentProps () {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/navinterface 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:FCIHomeProps () {const [bannerList, setBannerList] useStateIBanner[]([])const [seckillList, setSeckillList] useStateIPro[]([])useEffect(() {getBannerList().then(res setBannerList(res.data.data))getSeckilllist().then(res setSeckillList(res.data.data))}, [])return (header classNameheaderhome header/headerdiv classNamecontentBannerComponent 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:FCISeckillComponentProps ({ 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:FCIProComponentProps ({ 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:FCIHomeProps () {const [bannerList, setBannerList] useStateIBanner[]([])const [seckillList, setSeckillList] useStateIPro[]([])const [proList, setProList] useStateIPro[]([])useEffect(() {getBannerList().then(res setBannerList(res.data.data))getSeckilllist().then(res setSeckillList(res.data.data))getProList().then(res setProList(res.data.data))}, [])return (header classNameheaderhome header/headerdiv classNamecontentBannerComponent 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 classNameitemdiv classNameitemImageImage src {context.img1} //divdiv classNameitemInfodiv{ context.proname }/divdiv¥{ context.originprice }/div/div/div)
}
const ProComponent:FCIProComponentProps 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})}/ulul style{{ float: left, width: 48%, margin: 5px 1%}}{list.map((item, index) {return index % 2 1 ? WaterfallItem context{item}/ : null})}/ul/)
});export default ProComponent11.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:FCIHomeProps () {const [bannerList, setBannerList] useStateIBanner[]([])const [seckillList, setSeckillList] useStateIPro[]([])const [proList, setProList] useStateIPro[]([])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(count1)setHasMore(res.data.data.length 0)}return (header classNameheaderhome header/headerdiv classNamecontentBannerComponent bannerList { bannerList }/NavComponent list { navList } /SeckillComponent list { seckillList }/ProComponent list { proList }/ProComponentInfiniteScroll 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:FCIHomeProps () {const [bannerList, setBannerList] useStateIBanner[]([])const [seckillList, setSeckillList] useStateIPro[]([])const [proList, setProList] useStateIPro[]([])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(count1)setHasMore(res.data.data.length 0)}return (header classNameheaderhome header/headerdiv classNamecontentPullToRefreshonRefresh{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/PullToRefreshInfiniteScroll 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.scssinterface 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:FCIHomeProps () {const [bannerList, setBannerList] useStateIBanner[]([])const [seckillList, setSeckillList] useStateIPro[]([])const [proList, setProList] useStateIPro[]([])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(count1)setHasMore(res.data.data.length 0)}const [top, setTop] useState(0)const contentRef useRefany()const backTop () {contentRef.current.scrollTop 0}return (header classNameheaderhome header/headerdiv classNamecontent 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/PullToRefreshInfiniteScroll loadMore{loadMore} hasMore{hasMore} /{top 300 div classNamebackTop 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 ../Indexconst useBanner () {const [bannerList, setBannerList] useStateIBanner[]([])useEffect(() {getBannerList().then(res setBannerList(res.data.data))}, [])return {bannerList}
}export default useBanner// src/views/home/hooks/useNav.tsimport navList from /utils/navconst useNav () {return {navList}
}export default useNav// src/views/home/hooks/useSeckill.tsimport { getSeckilllist } from /api/home
import { useEffect, useState } from react
import { IPro } from ../Indexconst useSeckill () {const [seckillList, setSeckillList] useStateIPro[]([])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 ../Indexconst usePro () {const [proList, setProList] useStateIPro[]([]) 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(count1)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 reactconst useBackTop () {const [top, setTop] useState(0)const contentRef useRefany()const backTop () {contentRef.current.scrollTop 0}const scroll (event: React.UIEventHTMLDivElement, 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 ./componentsimport { 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:FCIHomeProps () {const { bannerList } useBanner()const { navList } useNav()const { seckillList } useSeckill()const { proList, loadMore, pullRefresh, hasMore } usePro()const { contentRef, scroll, top, backTop } useBackTop()return (header classNameheaderhome header/headerdiv classNamecontent ref{contentRef} onScroll{ scroll }PullToRefreshonRefresh{ pullRefresh }BannerComponent bannerList { bannerList }/NavComponent list { navList } /SeckillComponent list { seckillList }/ProComponent list { proList }/ProComponent/PullToRefreshInfiniteScroll loadMore{loadMore} hasMore{hasMore} /{top 300 div classNamebackTop 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:FCIHeaderComponentProps ({}) {return (header classNameheaderulli西安/lilidiv classNamesearchBoxImage src{ logo } /span classNamedivider |/spanSearchOutline fontSize{18} /span classNamesearchText立柜式空调/span/div/lili登录/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 ./componentsimport { 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:FCIHomeProps () {const { bannerList } useBanner()const { navList } useNav()const { seckillList } useSeckill()const { proList, loadMore, pullRefresh, hasMore } usePro()const { contentRef, scroll, top, backTop } useBackTop()return ({/* header classNameheaderhome header/header */}HeaderComponent/HeaderComponentdiv classNamecontent ref{contentRef} onScroll{ scroll }PullToRefreshonRefresh{ pullRefresh }BannerComponent bannerList { bannerList }/NavComponent list { navList } /SeckillComponent list { seckillList }/ProComponent list { proList }/ProComponent/PullToRefreshInfiniteScroll loadMore{loadMore} hasMore{hasMore} /{top 300 div classNamebackTop 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:FCIDetailProps () {return (header classNameheaderdetail header/headerdiv classNamecontentdetail content/div/)
};export default Detail;构建路由
// src/App.tsx
import React, { FC } from react;import { Routes, Route, Navigate } from react-router-domimport 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/404import Footer from /components/Footerimport ./App.scss;
import Detail from ./views/detail/Index;
interface IAppProps {
}const App: FCIAppProps (props) {return (div classNamecontainerdiv classNameboxRoutesRoute 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/divfooter classNamefooterFooter //footer/div)
}export default App通过访问地址发现可以跳转到详情但是详情页面不应有 底部选项卡需要处理 // src/components/Footer.tsx
import React, { FC } from react;import { NavLink } from react-router-dominterface IFooterProps {};const Footer:FCIFooterProps () {return (footer classNamefooterulNavLink to/home style {({ isActive }) isActive ? { color: #f66 }: undefined }span classNameiconfont icon-shouye/spanp首页/p/NavLinkNavLink to/kind style {({ isActive }) isActive ? { color: #f66 }: undefined }span classNameiconfont icon-fenlei/spanp分类/p/NavLinkNavLink to/cart style {({ isActive }) isActive ? { color: #f66 }: undefined }span classNameiconfont icon-gouwuche/spanp购物车/p/NavLinkNavLink to/user style {({ isActive }) isActive ? { color: #f66 }: undefined }span classNameiconfont icon-shouye1/spanp我的/p/NavLink/ul/footer)
};export default Footer;// src/App.tsx
import React, { FC } from react;import { Routes, Route, Navigate } from react-router-domimport 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/404import Footer from /components/Footerimport ./App.scss;
import Detail from ./views/detail/Index;
interface IAppProps {
}const App: FCIAppProps (props) {return (div classNamecontainerdiv classNameboxRoutesRoute 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/divRoutesRoute path/home element{Footer /} /Route path/kind element{Footer /} /Route path/cart element{Footer /} /Route path/user element{Footer /} //Routes/div)
}export default App12.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:FCISeckillComponentProps ({ 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 classNameitem onClick{ () {navigate(/detail/ context.proid)}}div classNameitemImageImage src {context.img1} style{{maxHeight: 1.8rem}}//divdiv classNameitemInfodiv style{{ maxHeight: 36px, overflow: hidden,}}{ context.proname }/divdiv¥{ context.originprice }/div/div/div)
}
const ProComponent:FCIProComponentProps 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})}/ulul style{{ float: left, width: 48%, margin: 5px 1%}}{list.map((item, index) {return index % 2 1 ? WaterfallItem context{item}/ : null})}/ul/)
});export default ProComponent12.3 详情页获取路由参数
// src/views/detail/Index.tsx
import React, { FC } from react;
import { useParams } from react-router-dom;interface IDetailProps {
};const Detail:FCIDetailProps () {// const params useParams()// console.log(params)const { proid } useParams()return (header classNameheaderdetail header/headerdiv classNamecontentdetail content/div/)
};export default Detail;12.4 封装详情页数据请求
// src/api/detail.ts
import request from /utils/requestinterface 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:FCIDetailProps () {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 classNameheaderDetail header/headerdiv classNamecontentDetail 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:FCIDetailProps () {// const params useParams()// console.log(params)const { proid } useParams()const [obj, setObj] useStateIPro({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 useRefany()return (header classNameheaderdetail header/headerdiv classNamecontentSwiper 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 nameviewport contentwidthdevice-width, initial-scale1, maximum-scale1, minimum-scale1, user-scalableno /script srchttps://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js/scriptscriptif (addEventListener in document) {document.addEventListener(DOMContentLoaded, function() {FastClick.attach(document.body);}, false);}if(!window.Promise) {document.writeln(script srchttps://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js/script);}/script
/head
body/body
/htmlpublic/index.html
!DOCTYPE html
html langenheadmeta charsetutf-8 /link relicon href%PUBLIC_URL%/favicon.ico /!-- meta nameviewport contentwidthdevice-width, initial-scale1 / --meta nametheme-color content#000000 /metanamedescriptioncontentWeb site created using create-react-app/link relapple-touch-icon href%PUBLIC_URL%/logo192.png /!--manifest.json provides metadata used when your web app is installed on ausers mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/--link relmanifest 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.--titleReact App/titlelink relstylesheet href//at.alicdn.com/t/c/font_3665887_h3lsrioddkk.cssmeta nameviewport contentwidthdevice-width, initial-scale1, maximum-scale1, minimum-scale1, user-scalableno /script srchttps://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js/scriptscriptif (addEventListener in document) {document.addEventListener(DOMContentLoaded, function() {FastClick.attach(document.body);}, false);}if(!window.Promise) {document.writeln(script srchttps://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js/script);}/script
/headbodynoscriptYou need to enable JavaScript to run this app./noscriptdiv idroot/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:FCIDetailProps () {// const params useParams()// console.log(params)const { proid } useParams()const [obj, setObj] useStateIPro({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 useRefany()return (header classNameheaderdetail header/headerdiv classNamecontentSwiper 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 classNameproInfodiv classNamepriceBoxspan{ obj.originprice }/spanspan销量{ obj.sales }/span/divdiv classNameproNameTag colordanger{ obj.brand }/TagTag colorprimary{ obj.category }/Tagspan{ 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 classNameitem onClick{ () {navigate(/detail/ context.proid)}}div classNameitemImageImage src {context.img1} style{{maxHeight: 1.8rem}}//divdiv classNameitemInfodiv style{{ maxHeight: 36px, overflow: hidden,}}{ context.proname }/divdiv¥{ context.originprice }/div/div/div)
}
const ProComponent:FCIProComponentProps 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})}/ulul 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:FCIDetailProps () {// const params useParams()// console.log(params)const { proid } useParams()const [obj, setObj] useStateIPro({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 useRefany()const [proList, setProList] useState([]) // useEffect(() { // getRecommendList().then(res setProList(res.data.data))}, [])return (header classNameheaderdetail header/headerdiv classNamecontentSwiper 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 classNameproInfodiv classNamepriceBoxspan{ obj.originprice }/spanspan销量{ obj.sales }/span/divdiv classNameproNameTag colordanger{ obj.brand }/TagTag colorprimary{ obj.category }/Tagspan{ obj.proname }/span/div/divDividerstyle{{color: #1677ff,borderColor: #1677ff,borderStyle: dashed,}}猜你喜欢/DividerProComponent 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:FCIFooterProps () {return (ul classNamedetailFooterli店铺/lili购物车/lili收藏/liliButton colorwarning shaperounded加入购物车/Button/liliButton colordanger shaperounded立即购买/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:FCIDetailProps () {// const params useParams()// console.log(params)const { proid } useParams()const [obj, setObj] useStateIPro({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 useRefany()const [proList, setProList] useState([]) // useEffect(() { // getRecommendList().then(res setProList(res.data.data))}, [])return (header classNameheaderdetail header/headerdiv classNamecontentSwiper 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 classNameproInfodiv classNamepriceBoxspan{ obj.originprice }/spanspan销量{ obj.sales }/span/divdiv classNameproNameTag colordanger{ obj.brand }/TagTag colorprimary{ obj.category }/Tagspan{ obj.proname }/span/div/divDividerstyle{{color: #1677ff,borderColor: #1677ff,borderStyle: dashed,}}猜你喜欢/DividerProComponent 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:FCIHeaderComponentProps ({}) {const navigate useNavigate()return (div classNamemyHeaderheader classNameheader2 v-showscrollTop 300ulli classNameleft onClick{ () navigate(-1) } LeftOutline //lili classNamemiddlespan详情/spanspan推荐/span/lili classNamerightMoreOutline //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:FCIDetailProps () {// const params useParams()// console.log(params)const { proid } useParams()const [obj, setObj] useStateIPro({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 useRefany()const [proList, setProList] useState([]) // useEffect(() { // getRecommendList().then(res setProList(res.data.data))}, [])return ({/* header classNameheaderdetail header/header */}HeaderComponent /div classNamecontentSwiper 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 classNameproInfodiv classNamepriceBoxspan{ obj.originprice }/spanspan销量{ obj.sales }/span/divdiv classNameproNameTag colordanger{ obj.brand }/TagTag colorprimary{ obj.category }/Tagspan{ obj.proname }/span/div/divDividerstyle{{color: #1677ff,borderColor: #1677ff,borderStyle: dashed,}}猜你喜欢/DividerProComponent 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:FCIRegisterStep1Props () {const navigate useNavigate()return (h1注册第一步/h1Button colordanger 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:FCIRegisterStep2Props () {const navigate useNavigate()return (h1注册第二步/h1Button colordanger 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:FCIRegisterStep3Props () {const navigate useNavigate()return (h1注册第三步/h1Button colordanger 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:FCIRegisterProps () {return (header classNameheaderRegister header/headerdiv classNamecontentOutlet //div/)
};export default Register;13.2 构建登录组件
// src/views/login/Index.tsx
import React, { FC } from react;interface ILoginProps {
};const Login:FCILoginProps () {return (header classNameheaderLogin header/headerdiv classNamecontentLogin content/div/)
};export default Login;13.3 设置登录以及注册路由
注册路由使用嵌套路由
// src/App.tsx
import React, { FC } from react;import { Routes, Route, Navigate } from react-router-domimport 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/404import Footer from /components/Footerimport ./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: FCIAppProps (props) {return (div classNamecontainerdiv classNameboxRoutesRoute 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 /} //RouteRoute path/detail/:proid element{Detail /} /Route path* element{NotFound /} //Routes/divRoutesRoute path/home element{Footer /} /Route path/kind element{Footer /} /Route path/cart element{Footer /} /Route path/user element{Footer /} //Routes/div)
}export default App13.4 修改登录组件
// src/views/login/Index.tsx
import { Button, Form, Input } from antd-mobile;
import React, { FC } from react;interface ILoginProps {
};const Login:FCILoginProps () {const loginFn (values: any) {console.log(values)}return (header classNameheaderLogin header/headerdiv classNamecontentFormlayouthorizontalfooter{Button block typesubmit colordanger sizelarge提交/Button}onFinish { loginFn }Form.Itemnameloginnamerules{[{ required: true, message: 账户名不能为空 }]}Input onChange{console.log} placeholder用户名/邮箱/手机号 //Form.ItemForm.Itemnamepasswordrules{[{ 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:FCILoginProps () {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 classNameheaderLogin header/headerdiv classNamecontentFormlayouthorizontalfooter{Button block typesubmit colordanger sizelarge提交/Button}onFinish { loginFn }Form.Itemnameloginnamerules{[{ required: true, message: 账户名不能为空 }]}Input onChange{console.log} placeholder用户名/邮箱/手机号 //Form.ItemForm.Itemnamepasswordrules{[{ 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 -S14.2 创建状态管理器
// src/store/modules/user.tsimport { makeAutoObservable } from mobxclass UserStore {// 初始化数据loginState localStorage.getItem(loginState) truetoken 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-domimport 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.StrictModeErrorBoundaryHashRouterProvider 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:FCILoginProps (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 classNameheaderLogin header/headerdiv classNamecontentFormlayouthorizontalfooter{Button block typesubmit colordanger sizelarge提交/Button}onFinish { loginFn }Form.Itemnameloginnamerules{[{ required: true, message: 账户名不能为空 }]}Input onChange{console.log} placeholder用户名/邮箱/手机号 //Form.ItemForm.Itemnamepasswordrules{[{ 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:FCIDetailProps () {// const params useParams()// console.log(params)const { proid } useParams()const [obj, setObj] useStateIPro({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 useRefany()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 classNameheaderdetail header/header */}HeaderComponent /div classNamecontent{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 classNameproInfodiv classNamepriceBoxspan{ obj.originprice }/spanspan销量{ obj.sales }/span/divdiv classNameproNameTag colordanger{ obj.brand }/TagTag colorprimary{ obj.category }/Tagspan{ obj.proname }/span/div/divDividerstyle{{color: #1677ff,borderColor: #1677ff,borderStyle: dashed,}}猜你喜欢/DividerProComponent 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:FCIFooterProps (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 classNamedetailFooterli店铺/liliLink to/cart 购物车/Link/lili收藏/liliButton sizesmall colorwarning shaperounded onClick{ addCartFn }加入购物车/Button/liliButton sizesmall colordanger shaperounded立即购买/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-domimport 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/404import Footer from /components/Footerimport ./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: FCIAppProps (props) {return (div classNamecontainerdiv classNameboxRoutesRoute 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 /} //RouteRoute path/detail/:proid element{Detail /} /Route path* element{NotFound /} //Routes/divRoutesRoute 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:FCICartProps ({ store:{ user: { userid }}}) {const [cartList, setCartList] useStateICartItem[]([])const [empty, setEmpty] useStateboolean(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 classNameheadercart header/headerdiv classNamecontent{empty ?Emptystyle{{ padding: 64px 0 }}imageStyle{{ width: 128 }}description{ divp购物车空空如也/pButton colordanger立即购物/Button/div}/ : List{cartList.map(item (List.Itemkey{item.cartid}prefix{Imagesrc{item.img1}fitcoverwidth{40}height{40}/}description{divspan style{{ color: #f66}}¥{item.originprice}/spanStepperstyle{{ 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:FCICartProps ({ store:{ user: { userid }}}) {const [cartList, setCartList] useStateICartItem[]([])const [empty, setEmpty] useStateboolean(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 classNameheadercart header/headerdiv classNamecontent{empty ?Emptystyle{{ padding: 64px 0 }}imageStyle{{ width: 128 }}description{ divp购物车空空如也/pButton colordanger立即购物/Button/div}/ : List{cartList.map(item (List.Itemkey{item.cartid}prefix{Imagesrc{item.img1}fitcoverwidth{40}height{40}/}description{divspan style{{ color: #f66}}¥{item.originprice}/spanStepperstyle{{ 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:FCICartProps ({ store:{ user: { userid }}}) {const [cartList, setCartList] useStateICartItem[]([])const [empty, setEmpty] useStateboolean(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 classNameheadercart header/headerdiv classNamecontent{empty ?Emptystyle{{ padding: 64px 0 }}imageStyle{{ width: 128 }}description{ divp购物车空空如也/pButton colordanger立即购物/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}fitcoverwidth{40}height{40}/}description{divspan style{{ color: #f66}}¥{item.originprice}/spanStepperstyle{{ 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:FCICartProps ({ store:{ user: { userid }}}) {const [cartList, setCartList] useStateICartItem[]([])const [empty, setEmpty] useStateboolean(true)const [checked, setChecked] useStateboolean(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 classNameheadercart header/headerdiv classNamecontent{empty ?Emptystyle{{ padding: 64px 0 }}imageStyle{{ width: 128 }}description{ divp购物车空空如也/pButton colordanger立即购物/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/divImagesrc{item.img1}fitcoverwidth{80}height{80}//div}description{divspan style{{ color: #f66}}¥{item.originprice}/spanStepperstyle{{ float: right}}defaultValue{item.num}onChange{value {console.log(value)updateOneDataNum({cartid: item.cartid, num: value}).then(() {getCartListDataFn()})}}//div}{item.proname}/List.Item/SwipeAction))}/Listdiv 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/divdivp总价/pp总数/p/divButton colordanger sizesmall提交订单/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:FCICartProps ({ store:{ user: { userid }}}) {const [cartList, setCartList] useStateICartItem[]([])const [empty, setEmpty] useStateboolean(true)const [checked, setChecked] useStateboolean(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 classNameheadercart header/headerdiv classNamecontent{empty ?Emptystyle{{ padding: 64px 0 }}imageStyle{{ width: 128 }}description{ divp购物车空空如也/pButton colordanger立即购物/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/divImagesrc{item.img1}fitcoverwidth{80}height{80}//div}description{divspan style{{ color: #f66}}¥{item.originprice}/spanStepperstyle{{ float: right}}defaultValue{item.num}onChange{value {console.log(value)updateOneDataNum({cartid: item.cartid, num: value}).then(() {getCartListDataFn()})}}//div}{item.proname}/List.Item/SwipeAction))}/Listdiv 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/divdivp总价{ totalPrice }/pp总数{ totalNum }/p/divButton colordanger sizesmall提交订单/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:FCICartProps ({ store:{ user: { userid }}}) {const [cartList, setCartList] useStateICartItem[]([])const [empty, setEmpty] useStateboolean(true)const [checked, setChecked] useStateboolean(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 classNameheaderNavBarstyle{{--height: 0.44rem,--border-bottom: 1px #eee solid,color: #fff}}onBack{ () navigate(-1) }购物车/NavBar/headerdiv classNamecontent{empty ?Emptystyle{{ padding: 64px 0 }}imageStyle{{ width: 128 }}description{ divp购物车空空如也/pButton colordanger立即购物/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/divImagesrc{item.img1}fitcoverwidth{80}height{80}//div}description{divspan style{{ color: #f66}}¥{item.originprice}/spanStepperstyle{{ float: right}}defaultValue{item.num}onChange{value {console.log(value)updateOneDataNum({cartid: item.cartid, num: value}).then(() {getCartListDataFn()})}}//div}{item.proname}/List.Item/SwipeAction))}/Listdiv 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/divdivp总价{ totalPrice }/pp总数{ totalNum }/p/divButton colordanger sizesmall提交订单/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