react-mobile 1.项目介绍 实现react移动端项目
2.目标: 能够应用CRA+React+Mobx+Antd-mobile开发C端项目 掌握基于React的C端项目开发流程 学会如何应用next优化项目 3.使用技术栈 4.构建项目 1 $ npx create-react-app react-mobile-app --template typescript
4.1 是否抽离配置文件 一般企业级项目,很少会直接抽离配置文件
抽离配置文件目的:对webpack进行二次封装
推荐使用 craco 进行覆盖
4.2 使用craco覆盖webpack配置 https://www.npmjs.com/package/@craco/craco
1 $ cnpm i @craco/craco -D
为了支持 commonjs 规范,安装如下模块
@types/*
这种文件称之为 ts 中的声明文件(ts中的定义的类型的一个整合
)
项目根目录创建 craco.config.js
,代码如下:
1 2 3 4 5 6 7 8 9 10 const path = require ('path' )module .exports = { webpack : { alias : { '@' : path.resolve (__dirname, 'src' ) } } } export {}
为了使 TS 文件引入时的别名路径能够正常解析,需要配置 tsconifg.json
,在 compilerOptions
选项里添加 path 等属性。为了防止配置被覆盖,需要单独创建一个文件 tsconfig.path.json
,添加以下代码
1 2 3 4 5 6 7 8 9 10 11 12 { "compilerOptions" : { "baseUrl" : "." , "paths" : { "@/*" : [ "./src/*" ] } , "types" : [ "node" ] } }
在 tsconifg.json
引入配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 { "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
如下:
1 2 3 4 5 "scripts" : { "start" : "craco start" , "build" : "craco build" , "test" : "craco test" } ,
4.3 确定项目 css 预处理器 https://create-react-app.bootcss.com/docs/adding-a-sass-stylesheet
1 $ cnpm i node-sass sass -D
cra 默认自带sass支持,只需要安装模块即可自动启动
4.4 改造项目目录结构 1 2 3 4 5 6 7 8 9 10 11 12 13 - mobile-react-app - src - api - components - router - store - utils - views App.tsx index.tsx logo.svg react-app-env.d.ts reportWebVitals.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 HTMLElement ); root.render ( <React.StrictMode > <ErrorBoundary > <App /> </ErrorBoundary > </React.StrictMode > ); reportWebVitals ();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IAppProps { }; const App :FC <IAppProps > = () => { return ( <div > <h1 > hello world</h1 > </div > ) }; export default App ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import React from 'react' interface IState { hasError : boolean } class ErrorBoundary extends React.Component <any , IState > { constructor (props: any ) { super (props); this .state = { hasError : false }; } static getDerivedStateFromError (error: any ) { return { hasError : true }; } componentDidCatch (error: any , info: { componentStack: any ; } ) { console .log (info.componentStack ); } render ( ) { if (this .state .hasError ) { return <h1 > 代码出错了,请自信检查一下</h1 > ; } return this .props .children ; } } export default ErrorBoundary
5 构建项目基本结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 * { padding : 0 ; margin : 0 ; list-style : none; text-decoration : none; } html , body , #root , .container { height : 100% ; } html { font-size : 26.666666667vw ; } body { font-size : 12px ; } @media only screen and (orientation : landscape) { html { font-size : 100px ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 .container { height : 100% ; display : flex; flex-direction : column; .header { height : 0.44rem ; background-color : #f66 ; } .content { flex : 1 ; overflow : auto; } .footer { height : 0.5rem ; background-color : #efefef ; } } .landscape-tip { position : fixed; top : 0 ; right : 0 ; bottom : 0 ; left : 0 ; background-color : rgba (0 , 0 , 0 , .8 ); color : #fff ; display : none; justify-content : center; align-items : center; } @media only screen and (orientation : landscape) { .landscape-tip { display : flex; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import React , { FC } from 'react' ;import '@/App.scss' ; interface IAppProps {}; const App :FC <IAppProps > = () => { return ( <div className ='container' > <header className ="header" > header</header > <div className ="content" > content</div > <footer className ="footer" > footer</footer > <div className ='landscape-tip' > 请将屏幕竖向浏览 </div > </div > ) }; export default App ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import React from 'react' ;import ReactDOM from 'react-dom/client' ;import ErrorBoundary from '@/ErrorBundary' ; import App from '@/App' ;import reportWebVitals from '@/reportWebVitals' ;import '@/main.scss' ; const root = ReactDOM .createRoot ( document .getElementById ('root' ) as HTMLElement ); root.render ( <React.StrictMode > <ErrorBoundary > <App /> </ErrorBoundary > </React.StrictMode > ); reportWebVitals ();
6.构建项目基本页面 思考每个页面的头部和内容区域是根据用户的选择而一起改变的,那么可以创建以下四个基本页面
6.1 构建首页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IHomeProps {}; const Home :FC <IHomeProps > = () => { return ( <> <header className ="header" > home header</header > <div className ="content" > home content</div > </> ) }; export default Home ;
6.2 构建分类页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import React , { FC } from 'react' ;interface IKindProps { }; const Kind :FC <IKindProps > = () => { return ( <> <header className ="header" > kind header</header > <div className ="content" > kind content</div > </> ) }; export default Kind ;
6.3 构建购物车页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import React , { FC } from 'react' ;interface ICartProps { }; const Cart :FC <ICartProps > = () => { return ( <> <header className ="header" > cart header</header > <div className ="content" > cart content</div > </> ) }; export default Cart ;
6.4 构建个人中心页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import React , { FC } from 'react' ;interface IUserProps { }; const User :FC <IUserProps > = () => { return ( <> <header className ="header" > user header</header > <div className ="content" > user content</div > </> ) }; export default User ;
6.引入路由 https://reactrouter.com/en/main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import React , { FC } from 'react' ;import { BrowserRouter , Routes , Route } from 'react-router-dom' import '@/App.scss' ;import Cart from '@/views/cart/Index' ;import Home from '@/views/home/Index' ;import Kind from '@/views/kind/Index' ;import User from '@/views/user/Index' ;interface IAppProps {}; const App : FC <IAppProps > = () => { return ( <BrowserRouter > <div className ='container' > <Routes > <Route path ="/home" element ={ <Home /> } /> <Route path ="/kind" element ={ <Kind /> } /> <Route path ="/cart" element ={ <Cart /> } /> <Route path ="/user" element ={ <User /> } /> </Routes > <footer className ="footer" > footer</footer > <div className ='landscape-tip' > 请将屏幕竖向浏览 </div > </div > </BrowserRouter > ) }; 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链接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="utf-8" /> <link rel ="icon" href ="%PUBLIC_URL%/favicon.ico" /> <meta name ="viewport" content ="width=device-width, initial-scale=1" /> <meta name ="theme-color" content ="#000000" /> <meta name ="description" content ="Web site created using create-react-app" /> <link rel ="apple-touch-icon" href ="%PUBLIC_URL%/logo192.png" /> <link rel ="manifest" href ="%PUBLIC_URL%/manifest.json" /> <title > React App</title > <link rel ="stylesheet" href ="//at.alicdn.com/t/c/font_3665887_2jcst3szxcd.css" > </head > <body > <noscript > You need to enable JavaScript to run this app.</noscript > <div id ="root" > </div > </body > </html >
底部组件展示如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import React , { FC } from 'react' ;interface IFooterProps { }; const Footer :FC <IFooterProps > = () => { return ( <ul > <li > <span className ="iconfont icon-shouye" > </span > <p > 首页</p > </li > <li > <span className ="iconfont icon-fenlei" > </span > <p > 分类</p > </li > <li > <span className ="iconfont icon-gouwuche" > </span > <p > 购物车</p > </li > <li > <span className ="iconfont icon-shouye1" > </span > <p > 我的</p > </li > </ul > ) }; export default Footer ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 .container { height : 100% ; display : flex; flex-direction : column; .header { height : 0.44rem ; background-color : #f66 ; } .content { flex : 1 ; overflow : auto; } .footer { height : 0.5rem ; background-color : #efefef ; user-select: none; 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.20rem ; } p { font-size : 0.12rem ; } } } } } .landscape-tip { position : fixed; top : 0 ; right : 0 ; bottom : 0 ; left : 0 ; background-color : rgba (0 , 0 , 0 , .8 ); color : #fff ; display : none; justify-content : center; align-items : center; } @media only screen and (orientation : landscape) { .landscape-tip { display : flex; } }
8.点击页面底部跳转路由 此项选择使用声明式导航跳转
react提供了两个可以使用 声明式导航跳转方式 : Link NavLink
如果不需要设置选中的样式,可以使用Link 组件
如果需要设置选中的样式,建议使用NavLink
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import React , { FC } from 'react' ;import { NavLink } from 'react-router-dom' interface IFooterProps { }; const Footer :FC <IFooterProps > = () => { return ( <ul > <NavLink to ="/home" style ={({ isActive }) => isActive ? { color: '#f66 '} : undefined }> <span className ="iconfont icon-shouye" > </span > <p > 首页</p > </NavLink > <NavLink to ="/kind" style ={({ isActive }) => isActive ? { color: '#f66 '} : undefined }> <span className ="iconfont icon-fenlei" > </span > <p > 分类</p > </NavLink > <NavLink to ="/cart" style ={({ isActive }) => isActive ? { color: '#f66 '} : undefined }> <span className ="iconfont icon-gouwuche" > </span > <p > 购物车</p > </NavLink > <NavLink to ="/user" style ={({ isActive }) => isActive ? { color: '#f66 '} : undefined }> <span className ="iconfont icon-shouye1" > </span > <p > 我的</p > </NavLink > </ul > ) }; export default Footer ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 // src/App.scss .container { height: 100%; display: flex; flex-direction: column; .header { height: 0.44rem; background-color: #f66; } .content { flex: 1; overflow: auto; } .footer { // height: 50px; height: 0.5rem; background-color: #efefef; user-select: none; 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.20rem; } p { font-size: 0.12rem; } } } } } .landscape-tip { position: fixed; top: 0; right: 0; bottom: 0; left: 0; background-color: rgba(0, 0, 0, .8); color: #fff; display: none; justify-content: center; align-items: center; } @media only screen and (orientation: landscape) { // 横屏 .landscape-tip { display: flex; } }
此时发现 当用户只输入 / 时,也就是路由的重定向需要实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import React from 'react' ;import ReactDOM from 'react-dom/client' ;import ErrorBoundary from '@/ErrorBundary' ; import App from '@/App' ;import reportWebVitals from '@/reportWebVitals' ;import { BrowserRouter } from 'react-router-dom' import '@/main.scss' ;const root = ReactDOM .createRoot ( document .getElementById ('root' ) as HTMLElement ); root.render ( <React.StrictMode > <ErrorBoundary > <BrowserRouter > // ++++++ <App /> </BrowserRouter > </ErrorBoundary > </React.StrictMode > ); reportWebVitals ();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import React , { FC , useEffect } from 'react' ;import { Routes , Route , useLocation, useNavigate } from 'react-router-dom' import '@/App.scss' ;import Cart from '@/views/cart/Index' ;import Home from '@/views/home/Index' ;import Kind from '@/views/kind/Index' ;import User from '@/views/user/Index' ;import Footer from '@/components/Footer' interface IAppProps {}; const App : FC <IAppProps > = () => { const { pathname } = useLocation () const navigate = useNavigate () useEffect (() => { console .log (pathname) pathname === '/' && navigate ('/home' , { replace : true }) }, []) return ( <div className ='container' > <Routes > <Route path ="/home" element ={ <Home /> } /> <Route path ="/kind" element ={ <Kind /> } /> <Route path ="/cart" element ={ <Cart /> } /> <Route path ="/user" element ={ <User /> } /> </Routes > <footer className ="footer" > <Footer /> </footer > <div className ='landscape-tip' > 请将屏幕竖向浏览 </div > </div > ) }; export default App ;
2.使用声明式实现重定向 – 在某条件下使用
https://reactrouter.com/en/6.4.2/components/navigate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import React , { FC } from 'react' ;import { Routes , Route , Navigate } from 'react-router-dom' import '@/App.scss' ;import Cart from '@/views/cart/Index' ;import Home from '@/views/home/Index' ;import Kind from '@/views/kind/Index' ;import User from '@/views/user/Index' ;import Footer from '@/components/Footer' interface IAppProps {}; const App : FC <IAppProps > = () => { return ( <div className ='container' > <Routes > <Route path ="/" element ={ <Navigate to ="/home" replace ={true}/ > } /> <Route path ="/home" element ={ <Home /> } /> <Route path ="/kind" element ={ <Kind /> } /> <Route path ="/cart" element ={ <Cart /> } /> <Route path ="/user" element ={ <User /> } /> </Routes > <footer className ="footer" > <Footer /> </footer > <div className ='landscape-tip' > 请将屏幕竖向浏览 </div > </div > ) }; export default App ;
3.使用嵌套路由 配合index属性
具体实现后台管理系统项目中实现
/banner /banner/add /banner/list
/banner ====> /banner/list
如果使用的式react-router-domv5版本
1 2 3 import { Redirect } from 'react-router-dom' <Redirect from ="/" to="/home" />
9.引入UI组件库 http://ant-design-mobile.antgroup.com/zh
react 移动端项目建议使用 Ant Design Mobile
直接引入组件即可,antd-mobile 会自动为你加载 css 样式文件
10.封装数据请求 在vue/react项目中建议使用 axios
作为数据请求的方案
axios官网:http://www.axios-js.com/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import axios from 'axios' const isDev = process.env .NODE_ENV === 'development' axios.defaults .withCredentials = false const ins = axios.create ({ baseURL : isDev ? 'http://121.89.205.189:3001/api' : 'http://121.89.205.189:3001/api' , timeout : 6000 }) ins.interceptors .request .use ((config ) => { return config }, (err ) => { return Promise .reject (err) }) ins.interceptors .response .use ((response ) => { return response }, (err ) => Promise .reject (err)) export default ins
11.构建首页 11.1 封装首页相关数据请求 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import request from '@/utils/request' interface IPageParams { limitNum?: number count?: number } export function getBannerListData () { return request.get ('/banner/list' ) } export function getSeckillListData () { return request.get ('/pro/seckilllist' ) } export function getProListData (params?: IPageParams ) { return request.get ('/pro/list' , { params }) }
11.2 构建首页轮播图组件以及渲染 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IBannerComponentProps { }; const BannerComponent :FC <IBannerComponentProps > = () => { return ( <> <h1 > 轮播图</h1 > </> ) }; export default BannerComponent ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import React , { FC } from 'react' ;import { Button } from 'antd-mobile' ;import BannerComponent from './components/BannerComponent' interface IHomeProps {}; const Home :FC <IHomeProps > = () => { return ( <> <header className ="header" > home header</header > <div className ="content" > {/* 轮播图 */} <BannerComponent /> </div > </> ) }; export default Home ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import React , { FC , useState, useEffect } from 'react' ;import { getBannerListData } from '@/api/home' import BannerComponent from './components/BannerComponent' interface IHomeProps {}; const Home :FC <IHomeProps > = () => { const [bannerList, setBannerList] = useState ([]) useEffect (() => { getBannerListData ().then (res => { setBannerList (res.data .data ) }) }, []) return ( <> <header className ="header" > home header</header > <div className ="content" > {/* 轮播图 */} <BannerComponent list = { bannerList }/> </div > </> ) }; export default Home ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import React , { FC } from 'react' ;import { Swiper , Image } from 'antd-mobile' interface IBanner { bannerid : string img : string alt : string link : string flag : boolean } interface IBannerComponentProps { list : IBanner [] }; const BannerComponent :FC <IBannerComponentProps > = ({ list } ) => { return ( <div style ={{ padding: '15px '}}> <Swiper autoplay loop ={true} style ={{ '--border-radius ': '8px ', }}> { list && list.map((item) => { return ( <Swiper.Item key = {item.bannerid} > <Image src ={item.img}/ > </Swiper.Item > ) }) } </Swiper > </div > ) }; export default BannerComponent ;
11.3 构建nav导航组件以及渲染 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import React , { FC , useState, useEffect } from 'react' ;import nav_List from '@/utils/nav' import { getBannerListData } from '@/api/home' import BannerComponent from './components/BannerComponent' import NavComponent from './components/NavComponent' interface IHomeProps {}; const Home :FC <IHomeProps > = () => { const [bannerList, setBannerList] = useState ([]) useEffect (() => { getBannerListData ().then (res => { console .log (res.data ) setBannerList (res.data .data ) }) }, []) const [navList] = useState (nav_List) return ( <> <header className ="header" > home header</header > <div className ="content" > {/* 轮播图 */} <BannerComponent list = { bannerList }/> {/* nav导航 */} <NavComponent list = { navList } /> </div > </> ) }; export default Home ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import React , { FC } from 'react' ;import { Grid , Image } from 'antd-mobile' interface INav { navid : number title : string imgurl : string } interface INavComponentProps { list : INav [] }; const NavComponent :FC <INavComponentProps > = ({ list } ) => { return ( <Grid columns ={5} gap ={0} > { list && list.map(item => { return ( <Grid.Item key = {item.navid} style ={{ display: 'flex ', flexDirection: 'column ', justifyContent: 'center ', alignItems: 'center '}}> <Image src ={ item.imgurl } style ={{ width: 44 , height: 44 }}> </Image > <p > { item.title }</p > </Grid.Item > ) }) } </Grid > ) }; export default NavComponent ;
11.4 构建秒杀列表实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface ISeckillComponentProps { }; const SeckillComponent :FC <ISeckillComponentProps > = () => { return ( <> <h1 > 秒杀列表</h1 > </> ) }; export default SeckillComponent ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import React , { FC , useState, useEffect } from 'react' ;import nav_List from '@/utils/nav' import { getBannerListData, getSeckillListData } from '@/api/home' import BannerComponent from './components/BannerComponent' import NavComponent from './components/NavComponent' import SeckillComponent from './components/SeckillComponent' interface IHomeProps {}; const Home :FC <IHomeProps > = () => { const [bannerList, setBannerList] = useState ([]) useEffect (() => { getBannerListData ().then (res => { console .log (res.data ) setBannerList (res.data .data ) }) }, []) const [navList] = useState (nav_List) const [seckillList, setSeckillList] = useState ([]) useEffect (() => { getSeckillListData ().then (res => { setSeckillList (res.data .data ) }) }, []) return ( <> <header className ="header" > home header</header > <div className ="content" > {/* 轮播图 */} <BannerComponent list = { bannerList }/> {/* nav导航 */} <NavComponent list = { navList } /> {/* 秒杀列表 */} <SeckillComponent list = { seckillList } /> </div > </> ) }; export default Home ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 import React , { FC , useEffect, useState } from 'react' ;import { Image } from 'antd-mobile' import './Seckill.scss' ;interface IPro { banners : string [] brand : string category : string desc : string discount : number img1 : string img2 : string img3 : string img4 : string isrecommend : number issale : number isseckill : number originprice : number proid : string proname : string sales : number stock : number } interface ISeckillComponentProps { list : IPro [] }; const SeckillComponent :FC <ISeckillComponentProps > = ({list} ) => { const [time, setTime] = useState (0 ) const [field, setField] = useState (0 ) const [hour, setHour] = useState (0 ) const [minutes, setMinutes] = useState (0 ) const [seconds, setSeconds] = useState (0 ) useEffect (() => { function timeCountDown () { let originTime = new Date (new Date ().toLocaleDateString ()); let hours = new Date ().getHours (); if (hours % 2 === 0 ) { setField (hours) let twohour = originTime.getTime () + (hours + 2 ) * 60 * 60 * 1000 setTime (twohour.valueOf () - new Date ().valueOf ()) } else { setField (hours - 1 ) let twohour = originTime.getTime () + 60 * (hours + 1 ) * 60 * 1000 ; setTime (twohour - new Date ().valueOf ()) } } const timer = setInterval (() => { timeCountDown () }, 1000 ) return () => { clearInterval (timer) } }, []) useEffect (() => { console .log ('1' , time) const hour = Math .floor (time / 3600000 ) const minutes = Math .floor ((time - hour * 3600000 ) / 60000 ) const seconds = Math .floor ((time - hour * 3600000 - minutes * 60000 ) / 1000 ) setHour (hour) setMinutes (minutes) setSeconds (seconds) }, [time]) return ( <div className ="seckillBox" > <div className ="title_wrap" > <ul > <li > <span > 嗨购秒杀</span > <span > { field }点场</span > {/* { hour } - { minutes } - { seconds } */} <div className ="myTimer" > <span className ="block" > { hour < 10 ? '0' + hour : hour }</span > <span className ="colon" > :</span > <span className ="block" > { minutes < 10 ? '0' + minutes : minutes }</span > <span className ="colon" > :</span > <span className ="block" > { seconds < 10 ? '0' + seconds : seconds }</span > </div > </li > <li > 爆款轮番秒 </li > </ul > </div > <ul className ="seckillList" > { list && list.map(item => ( <li key = { item.proid }> <Image src ={item.img1} /> <p > ¥{ item.originprice }</p > </li > )) } </ul > </div > ) }; export default SeckillComponent ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 .seckillBox { width : 96% ; height : 1.3rem ; background-color : #fff ; border-radius : 15px ; margin : 10px 2% ; .title_wrap { width : 100% ; height : 0.36rem ; background : url ('./title_wrap.png' ) no-repeat center center; background-size : cover; overflow : hidden; ul { display : flex; padding : 0 10px ; box-sizing : border-box; li { &:nth-child (1 ) { flex : 3 ; display : flex; &>span { line-height : 0.36rem ; &:nth-child (2 ) { color : #f66 ; } margin-right : 10px ; } .myTimer { padding-top : 0.08rem ; span { line-height : 0.2rem ; } } } &:nth-child (2 ) { flex : 2 ; display : flex; justify-content : flex-end; font-size : 0.12rem ; color : #f66 ; line-height : 0.36rem ; .van-icon { font-size : 0.12rem ; transform-origin : left top; transform : rotate (225deg ); line-height : 0.36rem ; margin-top : 0.34rem ; box-sizing : border-box; } } } } } .seckillList { padding : 0 10px ; box-sizing : border-box; margin-top : 0.1rem ; li { float : left; width : calc (100% / 6 ); height : 0.94rem ; .adm-image-img { width : 96% ; margin : 0 2% ; height : 0.54rem ; overflow : hidden; } p { text-align : center; color : #f66 ; line-height : 0.3rem ; font-weight : bold; } } } } .colon { display : inline-block; margin : 0 2px ; font-weight : bold; font-size : 0.16rem ; color : #ee0a24 ; } .block { display : inline-block; color : #fff ; width : 16px ; font-size : 12px ; font-weight : bold; text-align : center; background-color : #ee0a24 ; border-radius : 4px ; }
11.5 构建产品列表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IProComponentProps { }; const ProComponent :FC <IProComponentProps > = ({} ) => { return ( <> <h1 > 产品列表</h1 > </> ) }; export default ProComponent ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import React , { FC , useState, useEffect } from 'react' ;import nav_List from '@/utils/nav' import { getBannerListData, getProListData, getSeckillListData } from '@/api/home' import BannerComponent from './components/BannerComponent' import NavComponent from './components/NavComponent' import SeckillComponent from './components/SeckillComponent' import ProComponent from './components/ProComponent' interface IHomeProps {}; const Home :FC <IHomeProps > = () => { const [bannerList, setBannerList] = useState ([]) useEffect (() => { getBannerListData ().then (res => { console .log (res.data ) setBannerList (res.data .data ) }) }, []) const [navList] = useState (nav_List) const [seckillList, setSeckillList] = useState ([]) useEffect (() => { getSeckillListData ().then (res => { setSeckillList (res.data .data ) }) }, []) const [proList, setProList] = useState ([]) useEffect (() => { getProListData ().then (res => setProList (res.data .data )) }, []) return ( <> <header className ="header" > home header</header > <div className ="content" > {/* 轮播图 */} <BannerComponent list = { bannerList }/> {/* nav导航 */} <NavComponent list = { navList } /> {/* 秒杀列表 */} <SeckillComponent list = { seckillList } /> {/* 产品列表 */} <ProComponent list = { proList } /> </div > </> ) }; export default Home ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 .proList { display : flex; width : 96% ; margin : 10px 2% 15px ; .left { width : 49% ; min-height : 300px ; margin-right : 1% ; } .right { width : 49% ; min-height : 300px ; margin-left : 1% ; } ul { li { margin-top : 5px ; background-color : #fff ; border-radius : 10px ; overflow : hidden; .itemImage { width : 100% ; .van-image { min-height : 1.3rem ; } } .itemInfo { padding : 10px ; .itemTitle { font-size : 0.14rem ; } .itemPrice { color : #f66 ; font-size : 0.2rem ; span { font-size : 0.14rem ; } } } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 import React , { FC } from 'react' ;import { Image , Ellipsis } from 'antd-mobile' ;import './Pro.scss' ;interface IPro { banners : string [] brand : string category : string desc : string discount : number img1 : string img2 : string img3 : string img4 : string isrecommend : number issale : number isseckill : number originprice : number proid : string proname : string sales : number stock : number } interface IProComponentProps { list : IPro [] }; const ProComponent :FC <IProComponentProps > = ({ list } ) => { return ( <div className ="proList" > <ul className ="left" > { list && list.map((item, index) => { if (index % 2 === 0) { return ( <li key = { item.proid }> <div className ="itemImage" > <Image src ={item.img1} /> </div > <div className ="itemInfo" > <div className ="itemTitle" > <Ellipsis direction ='end' rows ={2} content ={item.proname } /> </div > <div className ="itemPrice" > ¥{ item.originprice / 1 }</div > </div > </li > ) } else { return null } }) } </ul > <ul className ="right" > { list && list.map((item, index) => { if (index % 2 === 1) { return ( <li key = { item.proid }> <div className ="itemImage" > <Image src ={item.img1} /> </div > <div className ="itemInfo" > <div className ="itemTitle " > <Ellipsis direction ='end' rows ={2} content ={ item.proname } /> </div > <div className ="itemPrice" > ¥{ item.originprice / 1 }</div > </div > </li > ) } else { return null } }) } </ul > </div > ) }; export default ProComponent ;
11.6 实现上拉加载操作 http://ant-design-mobile.antgroup.com/zh/components/infinite-scroll
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 import React , { FC , useState, useEffect } from 'react' ;import { InfiniteScroll } from 'antd-mobile' ; import nav_List from '@/utils/nav' import { getBannerListData, getProListData, getSeckillListData } from '@/api/home' import BannerComponent from './components/BannerComponent' import NavComponent from './components/NavComponent' import SeckillComponent from './components/SeckillComponent' import ProComponent from './components/ProComponent' interface IPro { banners : string [] brand : string category : string desc : string discount : number img1 : string img2 : string img3 : string img4 : string isrecommend : number issale : number isseckill : number originprice : number proid : string proname : string sales : number stock : number } interface IHomeProps {}; const Home :FC <IHomeProps > = () => { const [bannerList, setBannerList] = useState ([]) useEffect (() => { getBannerListData ().then (res => { console .log (res.data ) setBannerList (res.data .data ) }) }, []) const [navList] = useState (nav_List) const [seckillList, setSeckillList] = useState<IPro []>([]) useEffect (() => { getSeckillListData ().then (res => { setSeckillList (res.data .data ) }) }, []) const [proList, setProList] = useState<IPro []>([]) useEffect (() => { getProListData ().then (res => setProList (res.data .data )) }, []) const [hasMore, setHasMOre] = useState<boolean >(true ) const [count, setCount] = useState<number >(2 ) const loadMore = async ( ) => { const res = await getProListData ({ count }) if (res.data .data .length === 0 ) { setHasMOre (false ) } else { setCount (count + 1 ) setProList ([ ...proList, ...res.data .data ]) } } return ( <> <header className ="header" > home header</header > <div className ="content" > {/* 轮播图 */} <BannerComponent list = { bannerList }/> {/* nav导航 */} <NavComponent list = { navList } /> {/* 秒杀列表 */} <SeckillComponent list = { seckillList } /> {/* 产品列表 */} <ProComponent list = { proList } /> {/* 上拉加载组件 */} <InfiniteScroll loadMore ={ loadMore } hasMore = { hasMore } /> </div > </> ) }; export default Home ;
11.7 实现下拉刷新 http://ant-design-mobile.antgroup.com/zh/components/pull-to-refresh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 import React , { FC , useState, useEffect } from 'react' ;import { InfiniteScroll , PullToRefresh } from 'antd-mobile' ; import nav_List from '@/utils/nav' import { getBannerListData, getProListData, getSeckillListData } from '@/api/home' import BannerComponent from './components/BannerComponent' import NavComponent from './components/NavComponent' import SeckillComponent from './components/SeckillComponent' import ProComponent from './components/ProComponent' interface IPro { banners : string [] brand : string category : string desc : string discount : number img1 : string img2 : string img3 : string img4 : string isrecommend : number issale : number isseckill : number originprice : number proid : string proname : string sales : number stock : number } interface IHomeProps {}; const Home :FC <IHomeProps > = () => { const [bannerList, setBannerList] = useState ([]) useEffect (() => { getBannerListData ().then (res => { console .log (res.data ) setBannerList (res.data .data ) }) }, []) const [navList] = useState (nav_List) const [seckillList, setSeckillList] = useState<IPro []>([]) useEffect (() => { getSeckillListData ().then (res => { setSeckillList (res.data .data ) }) }, []) const [proList, setProList] = useState<IPro []>([]) useEffect (() => { getProListData ().then (res => setProList (res.data .data )) }, []) const [hasMore, setHasMOre] = useState<boolean >(true ) const [count, setCount] = useState<number >(2 ) const loadMore = async ( ) => { const res = await getProListData ({ count }) if (res.data .data .length === 0 ) { setHasMOre (false ) } else { setHasMOre (true ) setCount (count + 1 ) setProList ([ ...proList, ...res.data .data ]) } } const onRefresh = async ( ) => { const res = await getProListData () setProList (res.data .data ) setHasMOre (true ) setCount (2 ) } return ( <> <header className ="header" > home header</header > <div className ="content" > <PullToRefresh onRefresh = { onRefresh }> {/* 轮播图 */} <BannerComponent list = { bannerList }/> {/* nav导航 */} <NavComponent list = { navList } /> {/* 秒杀列表 */} <SeckillComponent list = { seckillList } /> {/* 产品列表 */} <ProComponent list = { proList } /> </PullToRefresh > {/* 上拉加载组件 */} <InfiniteScroll loadMore ={ loadMore } hasMore = { hasMore } /> </div > </> ) }; export default Home ;
11.8返回顶部 分析清除到底是哪一个容器产生了滚动条
分析得知 content 容器产生了滚动条,可以给它绑定一个 scroll 事件用于判断 回到顶部按钮显示还是不显示
通过 content 的dom的scrollTop 属性可以设置滚动条距离
图标是在一个单独的 npm 包中,如果你想使用图标,需要先安装它:
1 $ cnpm install --save antd-mobile-icons
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .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; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 import React , { FC , useState, useEffect, useRef } from 'react' ;import { InfiniteScroll , PullToRefresh } from 'antd-mobile' ; import { UpOutline } from 'antd-mobile-icons' ;import './style.scss' import nav_List from '@/utils/nav' import { getBannerListData, getProListData, getSeckillListData } from '@/api/home' import BannerComponent from './components/BannerComponent' import NavComponent from './components/NavComponent' import SeckillComponent from './components/SeckillComponent' import ProComponent from './components/ProComponent' interface IPro { banners : string [] brand : string category : string desc : string discount : number img1 : string img2 : string img3 : string img4 : string isrecommend : number issale : number isseckill : number originprice : number proid : string proname : string sales : number stock : number } interface IHomeProps {}; const Home :FC <IHomeProps > = () => { const [bannerList, setBannerList] = useState ([{ alt : "" , bannerid : "c" , flag : true , img : "" , link : "" }]) useEffect (() => { getBannerListData ().then (res => { console .log (res.data ) setBannerList (res.data .data ) }) }, []) const [navList] = useState (nav_List) const [seckillList, setSeckillList] = useState<IPro []>([]) useEffect (() => { getSeckillListData ().then (res => { setSeckillList (res.data .data ) }) }, []) const [proList, setProList] = useState<IPro []>([]) useEffect (() => { getProListData ().then (res => setProList (res.data .data )) }, []) const [hasMore, setHasMOre] = useState<boolean >(true ) const [count, setCount] = useState<number >(2 ) const loadMore = async ( ) => { const res = await getProListData ({ count }) if (res.data .data .length === 0 ) { setHasMOre (false ) } else { setHasMOre (true ) setCount (count + 1 ) setProList ([ ...proList, ...res.data .data ]) } } const onRefresh = async ( ) => { const res = await getProListData () setProList (res.data .data ) setHasMOre (true ) setCount (2 ) } const contentRef = useRef<any >() const [top, setTop] = useState<number >(0 ) const scroll = ( ) => { console .log (contentRef.current .scrollTop ) setTop (contentRef.current .scrollTop ) } const backTop = ( ) => { contentRef.current .scrollTop = 0 } return ( <> <header className ="header" > home header</header > <div className ="content" ref ={ contentRef } onScroll = { scroll }> <PullToRefresh onRefresh = { onRefresh }> {/* 轮播图 */} <BannerComponent list = { bannerList }/> {/* nav导航 */} <NavComponent list = { navList } /> {/* 秒杀列表 */} <SeckillComponent list = { seckillList } /> {/* 产品列表 */} <ProComponent list = { proList } /> </PullToRefresh > {/* 上拉加载组件 */} <InfiniteScroll loadMore ={ loadMore } hasMore = { hasMore } /> { top > 300 ? <div className ="backTop" onClick ={ backTop }> <UpOutline /> </div > : null } </div > </> ) }; export default Home ;
11.9 优化项目 提取首页面组件的业务逻辑,封装自定义hooks,统一导出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export { default as BannerComponent } from './BannerComponent' export { default as NavComponent } from './NavComponent' export { default as SeckillComponent } from './SeckillComponent' export { default as ProComponent } from './ProComponent'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { useState, useEffect } from 'react' import { getBannerListData } from '@/api/home' export default function useBanner () { const [bannerList, setBannerList] = useState ([{ alt : "" , bannerid : "c" , flag : true , img : "" , link : "" }]) useEffect (() => { getBannerListData ().then (res => { console .log (res.data ) setBannerList (res.data .data ) }) }, []) return { bannerList } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { useState, useEffect } from 'react' import { getSeckillListData } from '@/api/home' export default function useSeckill () { const [seckillList, setSeckillList] = useState ([]) useEffect (() => { getSeckillListData ().then (res => { setSeckillList (res.data .data ) }) }, []) return { seckillList } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import { useState, useEffect } from 'react' import { getProListData } from '@/api/home' interface IPro { banners : string [] brand : string category : string desc : string discount : number img1 : string img2 : string img3 : string img4 : string isrecommend : number issale : number isseckill : number originprice : number proid : string proname : string sales : number stock : number } export default function usePro () { const [proList, setProList] = useState<IPro []>([]) useEffect (() => { getProListData ().then (res => setProList (res.data .data )) }, []) const [hasMore, setHasMOre] = useState<boolean >(true ) const [count, setCount] = useState<number >(2 ) const loadMore = async ( ) => { const res = await getProListData ({ count }) if (res.data .data .length === 0 ) { setHasMOre (false ) } else { setHasMOre (true ) setCount (count + 1 ) setProList ([ ...proList, ...res.data .data ]) } } const onRefresh = async ( ) => { const res = await getProListData () setProList (res.data .data ) setHasMOre (true ) setCount (2 ) } return { proList, onRefresh, hasMore, loadMore } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { useState, useRef } from 'react' export default function useBacktop () { const contentRef = useRef (null ) const [top, setTop] = useState<number >(0 ) const scroll = ( ) => { const test = contentRef.current as unknown as HTMLDivElement setTop (test.scrollTop ) } const backTop = ( ) => { const test = contentRef.current as unknown as HTMLDivElement test.scrollTop = 0 } return { contentRef, top, scroll, backTop } }
1 2 3 4 5 // src/views/home/hooks/index.ts export { default as useBanner } from './useBanner' export { default as useSeckill } from './useSeckill' export { default as usePro } from './usePro' export { default as useBacktop } from './useBacktop'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 import React , { FC , useState } from 'react' ;import { InfiniteScroll , PullToRefresh } from 'antd-mobile' ; import { UpOutline } from 'antd-mobile-icons' ;import './style.scss' import nav_List from '@/utils/nav' import { BannerComponent , NavComponent , SeckillComponent , ProComponent } from './components' import { useBacktop, useBanner, usePro, useSeckill } from './hooks' interface IHomeProps {}; const Home :FC <IHomeProps > = () => { const { bannerList } = useBanner () const [navList] = useState (nav_List) const { seckillList } = useSeckill () const { onRefresh, proList, loadMore, hasMore } = usePro () const { contentRef, top, scroll, backTop } = useBacktop () return ( <> <header className ="header" > home header</header > <div className ="content" ref ={ contentRef } onScroll = { scroll }> <PullToRefresh onRefresh = { onRefresh }> {/* 轮播图 */} <BannerComponent list = { bannerList }/> {/* nav导航 */} <NavComponent list = { navList } /> {/* 秒杀列表 */} <SeckillComponent list = { seckillList } /> {/* 产品列表 */} <ProComponent list = { proList } /> </PullToRefresh > {/* 上拉加载组件 */} <InfiniteScroll loadMore ={ loadMore } hasMore = { hasMore } /> { top > 300 ? <div className ="backTop" onClick ={ backTop }> <UpOutline /> </div > : null } </div > </> ) }; export default Home ;
现在主流手机都有安全区域,那么写代码时一定要注意
http://ant-design-mobile.antgroup.com/zh/components/safe-area
11.10 自定义头部 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 .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; } } } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import React , { FC } from 'react' ;import { Image } from 'antd-mobile' import { SearchOutline } from 'antd-mobile-icons' ;import logo from './logo.png' import './Header.scss' ;interface IHeaderComponentProps { }; const HeaderComponent :FC <IHeaderComponentProps > = ({} ) => { return ( <header className ="header" > <ul > <li > 西安</li > <li > <div className ="searchBox" > <Image src ={ logo } /> <span className ="divider " > |</span > <SearchOutline fontSize ={18} /> <span className ="searchText" > 立柜式空调</span > </div > </li > <li > 登录</li > </ul > </header > ) }; export default HeaderComponent ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export { default as BannerComponent } from './BannerComponent' export { default as NavComponent } from './NavComponent' export { default as SeckillComponent } from './SeckillComponent' export { default as ProComponent } from './ProComponent' export { default as HeaderComponent } from './HeaderComponent'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 import React , { FC , useState } from 'react' ;import { InfiniteScroll , PullToRefresh } from 'antd-mobile' ; import { UpOutline } from 'antd-mobile-icons' ;import './style.scss' import nav_List from '@/utils/nav' import { BannerComponent , NavComponent , SeckillComponent , ProComponent , HeaderComponent } from './components' import { useBacktop, useBanner, usePro, useSeckill } from './hooks' interface IHomeProps {}; const Home :FC <IHomeProps > = () => { const { bannerList } = useBanner () const [navList] = useState (nav_List) const { seckillList } = useSeckill () const { onRefresh, proList, loadMore, hasMore } = usePro () const { contentRef, top, scroll, backTop } = useBacktop () return ( <> <HeaderComponent /> <div className ="content" ref ={ contentRef } onScroll = { scroll }> <PullToRefresh onRefresh = { onRefresh }> {/* 轮播图 */} <BannerComponent list = { bannerList }/> {/* nav导航 */} <NavComponent list = { navList } /> {/* 秒杀列表 */} <SeckillComponent list = { seckillList } /> {/* 产品列表 */} <ProComponent list = { proList } /> </PullToRefresh > {/* 上拉加载组件 */} <InfiniteScroll loadMore ={ loadMore } hasMore = { hasMore } /> { top > 300 ? <div className ="backTop" onClick ={ backTop }> <UpOutline /> </div > : null } </div > </> ) }; export default Home ;
12.实现详情 12.1 构建详情页面以及路由 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import React , { FC } from 'react' ;interface IDetailProps { }; const Detail :FC <IDetailProps > = () => { return ( <> <header className ="header" > Detail header</header > <div className ="content" > Detail content</div > </> ) }; export default Detail ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import React , { FC } from 'react' ;import { Routes , Route , Navigate } from 'react-router-dom' import '@/App.scss' ;import Cart from '@/views/cart/Index' ;import Home from '@/views/home/Index' ;import Kind from '@/views/kind/Index' ;import User from '@/views/user/Index' ;import Detail from '@/views/detail/Index' ; import Footer from '@/components/Footer' ;interface IAppProps {}; const App : FC <IAppProps > = () => { return ( <div className ='container' > <Routes > <Route path ="/" element ={ <Navigate to ="/home" replace ={true}/ > } /> <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 /> } /> </Routes > <footer className ="footer" > <Footer /> </footer > <div className ='landscape-tip' > 请将屏幕竖向浏览 </div > </div > ) }; export default App ;
通过访问地址发现可以跳转到详情,但是详情页面不应有 底部选项卡,需要处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import React , { FC } from 'react' ;import { NavLink } from 'react-router-dom' interface IFooterProps { }; const Footer :FC <IFooterProps > = () => { return ( <footer className ='footer' > <ul > <NavLink to ="/home" style ={({ isActive }) => isActive ? { color: '#f66 '} : undefined }> <span className ="iconfont icon-shouye" > </span > <p > 首页</p > </NavLink > <NavLink to ="/kind" style ={({ isActive }) => isActive ? { color: '#f66 '} : undefined }> <span className ="iconfont icon-fenlei" > </span > <p > 分类</p > </NavLink > <NavLink to ="/cart" style ={({ isActive }) => isActive ? { color: '#f66 '} : undefined }> <span className ="iconfont icon-gouwuche" > </span > <p > 购物车</p > </NavLink > <NavLink to ="/user" style ={({ isActive }) => isActive ? { color: '#f66 '} : undefined }> <span className ="iconfont icon-shouye1" > </span > <p > 我的</p > </NavLink > </ul > </footer > ) }; export default Footer ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import React , { FC } from 'react' ;import { Routes , Route , Navigate } from 'react-router-dom' import '@/App.scss' ;import Cart from '@/views/cart/Index' ;import Home from '@/views/home/Index' ;import Kind from '@/views/kind/Index' ;import User from '@/views/user/Index' ;import Detail from '@/views/detail/Index' ;import Footer from '@/components/Footer' ;interface IAppProps {}; const App : FC <IAppProps > = () => { return ( <div className ='container' > <Routes > <Route path ="/" element ={ <Navigate to ="/home" replace ={true}/ > } /> {/* <Route path ="/home" element ={ <Home /> } /> <Route path ="/kind" element ={ <Kind /> } /> <Route path ="/cart" element ={ <Cart /> } /> <Route path ="/user" element ={ <User /> } /> */} <Route path ="/home" element ={ <> <Home /> <Footer /> </> } /> <Route path ="/kind" element ={ <> <Kind /> <Footer /> </> } /> <Route path ="/cart" element ={ <> <Cart /> <Footer /> </> } /> <Route path ="/user" element ={ <> <User /> <Footer /> </> } /> <Route path ="/detail/:proid" element ={ <Detail /> } /> </Routes > {} { } <div className='landscape-tip' > 请将屏幕竖向浏览 </div> </div> ) }; export default App ;
12.2 点击列表进入产品详情 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 import React , { FC , useEffect, useState } from 'react' ;import { Image } from 'antd-mobile' import './Seckill.scss' ;import { Link } from 'react-router-dom' ; interface IPro { banners : string [] brand : string category : string desc : string discount : number img1 : string img2 : string img3 : string img4 : string isrecommend : number issale : number isseckill : number originprice : number proid : string proname : string sales : number stock : number } interface ISeckillComponentProps { list : IPro [] }; const SeckillComponent :FC <ISeckillComponentProps > = ({list} ) => { const [time, setTime] = useState (0 ) const [field, setField] = useState (0 ) const [hour, setHour] = useState (0 ) const [minutes, setMinutes] = useState (0 ) const [seconds, setSeconds] = useState (0 ) useEffect (() => { function timeCountDown () { let originTime = new Date (new Date ().toLocaleDateString ()); let hours = new Date ().getHours (); if (hours % 2 === 0 ) { setField (hours) let twohour = originTime.getTime () + (hours + 2 ) * 60 * 60 * 1000 setTime (twohour.valueOf () - new Date ().valueOf ()) } else { setField (hours - 1 ) let twohour = originTime.getTime () + 60 * (hours + 1 ) * 60 * 1000 ; setTime (twohour - new Date ().valueOf ()) } } const timer = setInterval (() => { timeCountDown () }, 1000 ) return () => { clearInterval (timer) } }, []) useEffect (() => { const hour = Math .floor (time / 3600000 ) const minutes = Math .floor ((time - hour * 3600000 ) / 60000 ) const seconds = Math .floor ((time - hour * 3600000 - minutes * 60000 ) / 1000 ) setHour (hour) setMinutes (minutes) setSeconds (seconds) }, [time]) return ( <div className ="seckillBox" > <div className ="title_wrap" > <ul > <li > <span > 嗨购秒杀</span > <span > { field }点场</span > {/* { hour } - { minutes } - { seconds } */} <div className ="myTimer" > <span className ="block" > { hour < 10 ? '0' + hour : hour }</span > <span className ="colon" > :</span > <span className ="block" > { minutes < 10 ? '0' + minutes : minutes }</span > <span className ="colon" > :</span > <span className ="block" > { seconds < 10 ? '0' + seconds : seconds }</span > </div > </li > <li > 爆款轮番秒 </li > </ul > </div > <ul className ="seckillList" > { list && list.map(item => ( <li key = { item.proid }> {/* 使用Link组件跳转即可 +++++++++++++++++++++ */} <Link to ={ '/detail /' + item.proid }> <Image src ={item.img1} /> <p > ¥{ item.originprice }</p > </Link > </li > )) } </ul > </div > ) }; export default SeckillComponent ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 import React , { FC } from 'react' ;import { Image , Ellipsis } from 'antd-mobile' ;import './Pro.scss' ;import { useNavigate } from 'react-router-dom' ;interface IPro { banners : string [] brand : string category : string desc : string discount : number img1 : string img2 : string img3 : string img4 : string isrecommend : number issale : number isseckill : number originprice : number proid : string proname : string sales : number stock : number } interface IProComponentProps { list : IPro [] }; const ProComponent :FC <IProComponentProps > = ({ list } ) => { const navigate = useNavigate () const toDetail = (proid: string ) => { navigate ('/detail/' + proid) } return ( <div className ="proList" > <ul className ="left" > { list && list.map((item, index) => { if (index % 2 === 0) { return ( <li key = { item.proid } onClick = { () => toDetail(item.proid) }> <div className ="itemImage" > <Image src ={item.img1} /> </div > <div className ="itemInfo" > <div className ="itemTitle" > <Ellipsis direction ='end' rows ={2} content ={item.proname } /> </div > <div className ="itemPrice" > ¥{ item.originprice / 1 }</div > </div > </li > ) } else { return null } }) } </ul > <ul className ="right" > { list && list.map((item, index) => { if (index % 2 === 1) { return ( <li key = { item.proid } onClick = { () => toDetail(item.proid) }> <div className ="itemImage" > <Image src ={item.img1} /> </div > <div className ="itemInfo" > <div className ="itemTitle " > <Ellipsis direction ='end' rows ={2} content ={ item.proname } /> </div > <div className ="itemPrice" > ¥{ item.originprice / 1 }</div > </div > </li > ) } else { return null } }) } </ul > </div > ) }; export default ProComponent ;
12.3 详情页获取路由参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import React , { FC } from 'react' ;import { useParams } from 'react-router-dom' ;interface IDetailProps { }; const Detail :FC <IDetailProps > = () => { const { proid } = useParams () console .log (proid) return ( <> <header className ="header" > Detail header</header > <div className ="content" > Detail content</div > </> ) }; export default Detail ;
12.4 封装详情页数据请求 1 2 3 4 5 6 7 8 9 10 11 // src/api/detail.ts import request from '../utils/request' export function getDetailData (proid: string) { return request.get('/pro/detail/' + proid) } // 推荐商品接口 export function getDetailRecommendData () { return request.get('/pro/recommendlist') }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import React , { FC , useEffect, useState } from 'react' ;import { useParams } from 'react-router-dom' ;import { getDetailData } from '@/api/detail' interface IDetailProps { }; const Detail :FC <IDetailProps > = () => { const { proid } = useParams () console .log (proid) const [obj, setObj] = useState ({ banners : [], proname : '' , originprice : 0 , discount : 0 , brand : '' , category : '' , sales : 0 , issale : 1 }) useEffect (() => { getDetailData (proid!).then (res => { console .log (res.data .data ) setObj ({ banners : res.data .data .banners [0 ].split (',' ), proname : res.data .data .proname , originprice : res.data .data .originprice , discount : res.data .data .discount , brand : res.data .data .brand , category : res.data .data .category , sales : res.data .data .sales , issale : res.data .data .issale }) }) }, [proid]) return ( <> <header className ="header" > Detail header</header > <div className ="content" > Detail content</div > </> ) }; export default Detail ;
12.5 渲染详情页面 12.5.1 轮播图以及大图预览 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 .bannerBox { position : relative; height : 3.5rem ; overflow : hidden; } .tip { position : absolute; bottom : 0.3rem ; right : 0 ; z-index : 1 ; width : 60px ; height : 24px ; background-color : rgba (0 , 0 , 0 , 0.2 ); color : #fff ; display : flex; justify-content : center; align-items : center; border-bottom-left-radius : 12px ; border-top-left-radius : 12px ; span { &:nth-child (1 ) { font-size : 0.16rem ; } } } .imagePreviewBox { position : fixed; top : 0 ; right : 0 ; left : 0 ; bottom : 0 ; background-color : #000 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import { Swiper , Image , ImageViewer } from 'antd-mobile' ;import React , { FC , useState, useRef } from 'react' ;import styled from './Banner.module.scss' interface IBannnerComponentProps { banners : string [] current : number onChange : Function }; const BannnerComponent :FC <IBannnerComponentProps > = ({ banners, current, onChange } ) => { const [visible, setVisible] = useState (false ) const mySwiperRef = useRef (null ) return ( <div className ={ styled.bannerBox }> <Swiper ref = { mySwiperRef } defaultIndex ={ current } onIndexChange = { (index: number ) => { onChange(index) }} > {/* <Swiper.Item > <Image src ="" /> </Swiper.Item > */} { banners && banners.map((item, index) => { return ( <Swiper.Item key = { index } onClick = { () => { setVisible(true) }}> <Image src = { item } /> </Swiper.Item > ) }) } </Swiper > <div className ={ styled.tip }> <span > { current + 1 }</span > / <span > { banners.length }</span > </div > { visible ? <ImageViewer.Multi images ={banners} visible ={visible} defaultIndex ={current} onIndexChange = {(index: number ) => { (mySwiperRef.current! as any).swipeTo(index) // 预览图片时 底片也跟着改变 onChange(index) }} onClose={() => { setVisible(false) }} /> : null } </div > ) }; export default BannnerComponent ;
1 2 export { default as BannerComponent } from './BannerComponent'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import React , { FC , useEffect, useState } from 'react' ;import { useParams } from 'react-router-dom' ;import { getDetailData } from '@/api/detail' import { BannerComponent } from './components' interface IDetailProps { }; const Detail :FC <IDetailProps > = () => { const { proid } = useParams () console .log (proid) const [obj, setObj] = useState ({ banners : [], proname : '' , originprice : 0 , discount : 0 , brand : '' , category : '' , sales : 0 , issale : 1 }) useEffect (() => { getDetailData (proid!).then (res => { console .log (res.data .data ) setObj ({ banners : res.data .data .banners [0 ].split (',' ), proname : res.data .data .proname , originprice : res.data .data .originprice , discount : res.data .data .discount , brand : res.data .data .brand , category : res.data .data .category , sales : res.data .data .sales , issale : res.data .data .issale }) }) }, [proid]) const [current, setCurrent] = useState (0 ) const onChange = (index: number ) => { setCurrent (index) } return ( <> <header className ="header" > Detail header</header > <div className ="content" > {/* 轮播图 */} <BannerComponent banners = { obj.banners } current = { current } onChange = { onChange }/> </div > </> ) }; export default Detail ;
大图预览遇到了 点击穿透问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <!DOCTYPE html > <html > <head > <meta name ="viewport" content ="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" /> <script src ="https://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js" > </script > <script > if ('addEventListener' in document ) { document .addEventListener ('DOMContentLoaded' , function ( ) { FastClick .attach (document .body ); }, false ); } if (!window .Promise ) { document .writeln ('<script src="https://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js"' +'>' +'<' +'/' +'script>' ); } </script > </head > <body > </body > </html >
public/index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="utf-8" /> <link rel ="icon" href ="%PUBLIC_URL%/favicon.ico" /> <meta name ="viewport" content ="width=device-width, initial-scale=1, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" /> <meta name ="theme-color" content ="#000000" /> <meta name ="description" content ="Web site created using create-react-app" /> <link rel ="apple-touch-icon" href ="%PUBLIC_URL%/logo192.png" /> <link rel ="manifest" href ="%PUBLIC_URL%/manifest.json" /> <title > React App</title > <link rel ="stylesheet" href ="//at.alicdn.com/t/c/font_3665887_2jcst3szxcd.css" > <script src ="https://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js" > </script > <script > if ('addEventListener' in document ) { document .addEventListener ('DOMContentLoaded' , function ( ) { FastClick .attach (document .body ); }, false ); } if (!window .Promise ) { document .writeln ('<script src="https://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js"' +'>' +'<' +'/' +'script>' ); } </script > </head > <body > <noscript > You need to enable JavaScript to run this app.</noscript > <div id ="root" > </div > </body > </html >
12.5.2 构建产品详细信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 .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 ; } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import React , { FC } from 'react' ;import { Tag } from 'antd-mobile' import './ProInfo.scss' ;interface IProInfoCompomnentProps { originprice : number sales :number brand : string category : string proname : string }; const ProInfoCompomnent :FC <IProInfoCompomnentProps > = ({ originprice, sales, brand, category, proname } ) => { return ( <div className ="proInfo" > <div className ="priceBox" > <span > ¥{ originprice }</span > <span > 销量:{ sales }</span > </div > <div className ="proName" > <Tag color ='danger' > { brand }</Tag > <Tag color ='primary' > { category }</Tag > <span > { proname }</span > </div > </div > ) }; export default ProInfoCompomnent ;
1 2 3 export { default as BannerComponent } from './BannerComponent' export { default as ProInfoComponent } from './ProInfoComponent'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import React , { FC , useEffect, useState } from 'react' ;import { useParams } from 'react-router-dom' ;import { getDetailData } from '@/api/detail' import { BannerComponent , ProInfoComponent } from './components' interface IDetailProps { }; const Detail :FC <IDetailProps > = () => { const { proid } = useParams () console .log (proid) const [obj, setObj] = useState ({ banners : [], proname : '' , originprice : 0 , discount : 0 , brand : '' , category : '' , sales : 0 , issale : 1 }) useEffect (() => { getDetailData (proid!).then (res => { console .log (res.data .data ) setObj ({ banners : res.data .data .banners [0 ].split (',' ), proname : res.data .data .proname , originprice : res.data .data .originprice , discount : res.data .data .discount , brand : res.data .data .brand , category : res.data .data .category , sales : res.data .data .sales , issale : res.data .data .issale }) }) }, [proid]) const [current, setCurrent] = useState (0 ) const onChange = (index: number ) => { setCurrent (index) } return ( <> <header className ="header" > Detail header</header > <div className ="content" > {/* 轮播图 */} <BannerComponent banners = { obj.banners } current = { current } onChange = { onChange }/> {/* 产品详情 */} <ProInfoComponent originprice = { obj.originprice } sales = { obj.sales } brand = { obj.brand } category = { obj.category } proname = { obj.proname } /> </div > </> ) }; export default Detail ;
12.5.3 猜你喜欢 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 import React , { FC , useEffect, useState } from 'react' ;import { useParams } from 'react-router-dom' ;import { getDetailData, getDetailRecommendData } from '@/api/detail' import { Divider } from 'antd-mobile' import { BannerComponent , ProInfoComponent } from './components' import { ProComponent } from '@/views/home/components' interface IDetailProps { }; const Detail :FC <IDetailProps > = () => { const { proid } = useParams () console .log (proid) const [obj, setObj] = useState ({ banners : [], proname : '' , originprice : 0 , discount : 0 , brand : '' , category : '' , sales : 0 , issale : 1 }) useEffect (() => { getDetailData (proid!).then (res => { console .log (res.data .data ) setObj ({ banners : res.data .data .banners [0 ].split (',' ), proname : res.data .data .proname , originprice : res.data .data .originprice , discount : res.data .data .discount , brand : res.data .data .brand , category : res.data .data .category , sales : res.data .data .sales , issale : res.data .data .issale }) }) }, [proid]) const [current, setCurrent] = useState (0 ) const onChange = (index: number ) => { setCurrent (index) } const [proList, setProList] = useState ([]) useEffect (() => { getDetailRecommendData ().then (res => setProList (res.data .data )) }, []) return ( <> <header className ="header" > Detail header</header > <div className ="content" > {/* 轮播图 */} <BannerComponent banners = { obj.banners } current = { current } onChange = { onChange }/> {/* 产品详情 */} <ProInfoComponent originprice = { obj.originprice } sales = { obj.sales } brand = { obj.brand } category = { obj.category } proname = { obj.proname } /> {/* 猜你喜欢 // ++++ */} <Divider style ={{ color: '#1677ff ', borderColor: '#1677ff ', borderStyle: 'dashed ', }} > 猜你喜欢 </Divider > <ProComponent list = { proList } /> </div > </> ) }; export default Detail ;
12.5.4 详情底部 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 .action_box { border-top : 0.01rem solid #bfbfbf55 ; position : fixed; bottom : 0 ; display : flex; align-items : center; width : 100% ; height : 0.5rem ; .action_item { padding-left : 0.1rem ; width : 0.4rem ; height : 100% ; justify-content : center; display : flex; flex-direction : column; align-items : center; .antd-mobile-icon { width : 0.24rem ; height : 0.24rem ; } } .button { margin : 0 0.1rem ; height : 0.36rem ; border-radius : 0.2rem ; overflow : hidden; flex : 1 ; display : flex; justify-content : space-between; align-items : center; Button { span { font-size : 0.14rem ; } height : 100% ; border-radius : 0 ; border : none; flex : 1 ; } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 import React , { FC } from "react" ;import { StarOutline , ShopbagOutline , MessageOutline , StarFill , } from "antd-mobile-icons" ; import "./index.scss" ;import { Button } from "antd-mobile" ;interface IActionBarProps { BgColor ?:string ; CartText ?: string ; CartTextColor ?: string ; buyTextColor?: string ; BuyText ?: string ; CartColor ?: string ; buyColor?: string ; cart?: boolean ; buy?: boolean ; Customer ?: boolean ; onCustomerClick?: any ; shop?: boolean ; onShopClick?: any ; star?: boolean ; onStar?: boolean ; onStarColor?: string ; onStarClick?: any ; onCartClick?: any ; onBuyClick?: any ; } const ActionBar : FC <IActionBarProps > = ({ BgColor = "#fff" , CartText = "加入购物车" , //购物车文字默认值 BuyText = "立即购买" , //默认值 CartColor = "#7233d9" , //购物车按钮颜色默认值 CartTextColor = "#fff" , //购物车字体默认值 buyColor = "#ff64fe" , //默认值 buyTextColor = "#fff" , //默认值 onStarColor = "#76c6b8" ,//收藏选中颜色默认值 cart = true , //购物车按钮是否存在 buy = true , //购买按钮是否存在 Customer = true , //客服按钮是否存在 onStar = false , //星星选中状态 shop = true , //购物袋按钮是否存在 star = true ,//收藏按钮是否存在 onCustomerClick = () => {}, onShopClick = () => {}, onStarClick = () => {}, onCartClick = () => {}, onBuyClick = () => {}, } ) => { return ( <> <div className ="action_box" style ={{backgroundColor:BgColor}} > <div className ="action_Customer action_item" style ={{ display: Customer ? "" : "none " }} onClick ={(event) => onCustomerClick()} > <MessageOutline /> <p > 客服</p > </div > <div className ="action_shop action_item" style ={{ display: shop ? "" : "none " }} onClick ={(event) => onShopClick()} > <ShopbagOutline /> <p > 购物车</p > </div > <div className ="action_star action_item" style ={{ display: star ? "" : "none " }} onClick ={(event) => { onStarClick(); }} > {(function fn() { return onStar ? <StarFill color ={onStarColor} /> : <StarOutline /> ; })()} <p > 收藏</p > </div > <div className ="button" > <Button className ="action_goBuy" style ={{ color: buyTextColor , backgroundColor: buyColor , display: buy ? "" : "none ", }} onClick ={() => onBuyClick()} > {BuyText} </Button > <Button className ="action_addCart" style ={{ color: CartTextColor , backgroundColor: CartColor , display: cart ? "" : "none ", }} onClick ={() => onCartClick()} > {CartText} </Button > </div > </div > </> ); }; export default ActionBar ;
1 2 3 4 5 6 export { default as BannerComponent } from './BannerComponent' export { default as ProInfoComponent } from './ProInfoComponent' export { default as HeaderComponent } from './HeaderComponent' export { default as ActionBar } from './action/Index'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 import React , { FC , useEffect, useState } from 'react' ;import { useParams } from 'react-router-dom' ;import { getDetailData, getDetailRecommendData } from '@/api/detail' import { Divider } from 'antd-mobile' import { BannerComponent , ProInfoComponent , ActionBar } from './components' import { ProComponent } from '@/views/home/components' interface IDetailProps { }; const Detail :FC <IDetailProps > = () => { const { proid } = useParams () console .log (proid) const [obj, setObj] = useState ({ banners : [], proname : '' , originprice : 0 , discount : 0 , brand : '' , category : '' , sales : 0 , issale : 1 }) useEffect (() => { getDetailData (proid!).then (res => { console .log (res.data .data ) setObj ({ banners : res.data .data .banners [0 ].split (',' ), proname : res.data .data .proname , originprice : res.data .data .originprice , discount : res.data .data .discount , brand : res.data .data .brand , category : res.data .data .category , sales : res.data .data .sales , issale : res.data .data .issale }) setCurrent (0 ) }) }, [proid]) const [current, setCurrent] = useState (0 ) const onChange = (index: number ) => { setCurrent (index) } const [proList, setProList] = useState ([]) useEffect (() => { getDetailRecommendData ().then (res => setProList (res.data .data )) }, []) return ( <> <header className ="header" > Detail header</header > <div className ="content" > {/* 轮播图 */} <BannerComponent banners = { obj.banners } current = { current } onChange = { onChange }/> {/* 产品详情 */} <ProInfoComponent originprice = { obj.originprice } sales = { obj.sales } brand = { obj.brand } category = { obj.category } proname = { obj.proname } /> {/* 猜你喜欢 */} <Divider style ={{ color: '#1677ff ', borderColor: '#1677ff ', borderStyle: 'dashed ', }} > 猜你喜欢 </Divider > <ProComponent list = { proList } /> {/* 底部选项 */} <ActionBar /> </div > </> ) }; export default Detail ;
12.5.5 详情头部 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 .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; } } } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import React , { FC } from 'react' ;import {LeftOutline , MoreOutline } from 'antd-mobile-icons' import './Header.scss' import { useNavigate } from 'react-router-dom' ;interface IHeaderComponentProps { }; const HeaderComponent :FC <IHeaderComponentProps > = ({} ) => { const navigate = useNavigate () return ( <div className ='myHeader' > <header className ="header2" v-show ="scrollTop > 300" > <ul > <li className ="left" onClick ={ () => navigate(-1) } > <LeftOutline /> </li > <li className ="middle" > <span > 详情</span > <span > 推荐</span > </li > <li className ="right" > <MoreOutline /> </li > </ul > </header > </div > ) }; export default HeaderComponent ;
1 2 3 4 export { default as BannerComponent } from './BannerComponent' export { default as ProInfoComponent } from './ProInfoComponent' export { default as HeaderComponent } from './HeaderComponent'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 import React , { FC , useEffect, useState } from 'react' ;import { useParams } from 'react-router-dom' ;import { getDetailData, getDetailRecommendData } from '@/api/detail' import { Divider } from 'antd-mobile' import { BannerComponent , ProInfoComponent , HeaderComponent } from './components' import { ProComponent } from '@/views/home/components' interface IDetailProps { }; const Detail :FC <IDetailProps > = () => { const { proid } = useParams () console .log (proid) const [obj, setObj] = useState ({ banners : [], proname : '' , originprice : 0 , discount : 0 , brand : '' , category : '' , sales : 0 , issale : 1 }) useEffect (() => { getDetailData (proid!).then (res => { console .log (res.data .data ) setObj ({ banners : res.data .data .banners [0 ].split (',' ), proname : res.data .data .proname , originprice : res.data .data .originprice , discount : res.data .data .discount , brand : res.data .data .brand , category : res.data .data .category , sales : res.data .data .sales , issale : res.data .data .issale }) }) }, [proid]) const [current, setCurrent] = useState (0 ) const onChange = (index: number ) => { setCurrent (index) } const [proList, setProList] = useState ([]) useEffect (() => { getDetailRecommendData ().then (res => setProList (res.data .data )) }, []) return ( <> {/* <header className ="header" > Detail header</header > */} <HeaderComponent /> <div className ="content" > {/* 轮播图 */} <BannerComponent banners = { obj.banners } current = { current } onChange = { onChange }/> {/* 产品详情 */} <ProInfoComponent originprice = { obj.originprice } sales = { obj.sales } brand = { obj.brand } category = { obj.category } proname = { obj.proname } /> {/* 猜你喜欢 */} <Divider style ={{ color: '#1677ff ', borderColor: '#1677ff ', borderStyle: 'dashed ', }} > 猜你喜欢 </Divider > <ProComponent list = { proList } /> {/* 底部选项 */} </div > </> ) }; export default Detail ;
13.登录功能 其实要登录需要先注册,借用vue项目的注册账户,此处直接实现登录
13.1 构建注册组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import React , { FC } from 'react' ;import { Button } from 'antd-mobile' import { useNavigate } from 'react-router-dom' ;interface IRegisterStep1Props { }; const RegisterStep1 :FC <IRegisterStep1Props > = () => { const navigate = useNavigate () return ( <> <h1 > 注册第一步</h1 > <Button color ='danger' onClick ={ () => navigate('/register/step2')}>下一步</Button > </> ) }; export default RegisterStep1 ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import React , { FC } from 'react' ;import { Button } from 'antd-mobile' import { useNavigate } from 'react-router-dom' ;interface IRegisterStep2Props { }; const RegisterStep2 :FC <IRegisterStep2Props > = () => { const navigate = useNavigate () return ( <> <h1 > 注册第二步</h1 > <Button color ='danger' onClick ={ () => navigate('/register/step3') }>下一步</Button > </> ) }; export default RegisterStep2 ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import React , { FC } from 'react' ;import { Button } from 'antd-mobile' import { useNavigate } from 'react-router-dom' ;interface IRegisterStep3Props { }; const RegisterStep3 :FC <IRegisterStep3Props > = () => { const navigate = useNavigate () return ( <> <h1 > 注册第三步</h1 > <Button color ='danger' onClick ={ () => navigate(-3) }>完成</Button > </> ) }; export default RegisterStep3 ;
1 2 3 4 export { default as Step1 } from './Step1' export { default as Step2 } from './Step2' export { default as Step3 } from './Step3'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import React , { FC } from 'react' ;import { Outlet } from 'react-router-dom' ;interface IRegisterProps { }; const Register :FC <IRegisterProps > = () => { return ( <> <header className ="header" > Register header</header > <div className ="content" > <Outlet /> </div > </> ) }; export default Register ;
13.2 构建登录组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import React , { FC } from 'react' ;interface ILoginProps { }; const Login :FC <ILoginProps > = () => { return ( <> <header className ="header" > Login header</header > <div className ="content" > Login content</div > </> ) }; export default Login ;
13.3 设置登录以及注册路由 注册路由使用嵌套路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import React , { FC } from 'react' ;import { Routes , Route , Navigate } from 'react-router-dom' import '@/App.scss' ;import Cart from '@/views/cart/Index' ;import Home from '@/views/home/Index' ;import Kind from '@/views/kind/Index' ;import User from '@/views/user/Index' ;import Detail from '@/views/detail/Index' ;import Footer from '@/components/Footer' ;import Login from './views/login/Index' ;import Register from './views/register/Index' ;import { Step1 , Step2 , Step3 } from './views/register/components' ;interface IAppProps {}; const App : FC <IAppProps > = () => { return ( <div className ='container' > <Routes > <Route path ="/" element ={ <Navigate to ="/home" replace ={true}/ > } /> {/* <Route path ="/home" element ={ <Home /> } /> <Route path ="/kind" element ={ <Kind /> } /> <Route path ="/cart" element ={ <Cart /> } /> <Route path ="/user" element ={ <User /> } /> */} <Route path ="/home" element ={ <> <Home /> <Footer /> </> } /> <Route path ="/kind" element ={ <> <Kind /> <Footer /> </> } /> <Route path ="/cart" element ={ <> <Cart /> <Footer /> </> } /> <Route path ="/user" element ={ <> <User /> <Footer /> </> } /> <Route path ="/detail/:proid" element ={ <Detail /> } /> <Route path ="/login" element ={ <Login /> } /> <Route path ="/register" element ={ <Register /> }> <Route index element = { <Navigate to ="/register/step1" replace ={true} /> } /> <Route path ='/register/step1' element = { <Step1 /> } /> <Route path ='/register/step2' element = { <Step2 /> } /> <Route path ='/register/step3' element = { <Step3 /> } /> </Route > </Routes > {} { } <div className='landscape-tip' > 请将屏幕竖向浏览 </div> </div> ) }; export default App ;
13.4 修改登录组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 import React , { FC ,useMemo,useState } from 'react' ;import { Form , Input , Button } from 'antd-mobile' interface ILoginProps { }; const Login :FC <ILoginProps > = () => { const [loginname, setLoginname] = useState ('' ) const [password, setPassword] = useState ('' ) const onFinish = (values: any ) => { console .log (values) } const onValuesChange = (changedValues: any , allValues: any ) => { console .log (changedValues, allValues) if (allValues.loginname === undefined ) { setPassword (allValues.password ) } else if (allValues.password === undefined ) { setLoginname (allValues.loginname ) } else { setLoginname (allValues.loginname ) setPassword (allValues.password ) } } const flag = useMemo (() => { return loginname === '' || password === '' }, [loginname, password]) return ( <> <header className ="header" > Login header</header > <div className ="content" > <Form layout ='horizontal' footer ={ <Button block type ='submit' color ='danger' disabled = { flag } size ='large' > 登录 </Button > } onFinish = { onFinish } onValuesChange = { onValuesChange } > <Form.Item name ='loginname' > <Input placeholder ='手机号/用户名/邮箱' clearable /> </Form.Item > <Form.Item name ='password' > <Input placeholder ='密码' clearable type ='password' /> </Form.Item > </Form > </div > </> ) }; export default Login ;
13.5 封装用户数据请求 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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 实现登录功能 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 import React , { FC ,useMemo,useState } from 'react' ;import { Form , Input , Button , Toast , Modal , NavBar } from 'antd-mobile' import { doLogin } from '@/api/user' ;import { useNavigate, Link } from 'react-router-dom' ;interface ILoginProps { }; const Login :FC <ILoginProps > = () => { const navigate = useNavigate () const [loginname, setLoginname] = useState ('' ) const [password, setPassword] = useState ('' ) const onFinish = (values: any ) => { console .log (values) doLogin (values).then (res => { console .log (res.data ) if (res.data .code === '10011' ) { Toast .show ({ content : '密码错误' , duration : 1000 }) } else if (res.data .code === '10010' ) { Modal .confirm ({ content : <div > 该用户还未注册</div > , confirmText : '立即注册' , onConfirm : () => { navigate ('/register' ) } }) } 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 ) } }) } const onValuesChange = (changedValues: any , allValues: any ) => { console .log (changedValues, allValues) if (allValues.loginname === undefined ) { setPassword (allValues.password ) } else if (allValues.password === undefined ) { setLoginname (allValues.loginname ) } else { setLoginname (allValues.loginname ) setPassword (allValues.password ) } } const flag = useMemo (() => { return loginname === '' || password === '' }, [loginname, password]) return ( <> <header className ="header" > <NavBar style ={{ '--height ': '0.44rem ', '--border-bottom ': '1px #eee solid ', color: '#fff ' }} onBack ={ () => navigate(-1) } > 登录 </NavBar > </header > <div className ="content" > <Form layout ='horizontal' footer ={ <Button block type ='submit' color ='danger' disabled = { flag } size ='large' > 登录 </Button > } onFinish = { onFinish } onValuesChange = { onValuesChange } > <Form.Item name ='loginname' > <Input placeholder ='手机号/用户名/邮箱' clearable /> </Form.Item > <Form.Item name ='password' > <Input placeholder ='密码' clearable type ='password' /> </Form.Item > </Form > <Link to ="/register" > 手机号快速注册</Link > </div > </> ) }; export default Login ;
14.mobx状态管理器 https://cn.mobx.js.org/ — v5
https://www.mobxjs.com/ -v6
14.1 安装 1 $ cnpm i mobx mobx-react -S
14.2 创建状态管理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import { makeAutoObservable } from "mobx" ;class UserStore { loginState : boolean = Boolean (localStorage .getItem ('loginState' )) userid : string = localStorage .getItem ('userid' ) || '' token : string = localStorage .getItem ('token' ) || '' constructor () { makeAutoObservable (this ) } changeLoginState (action : { payload : any ; }) { this .loginState = action.payload } changeUserId ( action : { payload : any ; }) { this .userid = action.payload } changeToken (action : { payload : any ; }) { this .token = action.payload } } export default UserStore
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { makeAutoObservable } from 'mobx' import UserStore from './modules/user' export class Store { user constructor () { makeAutoObservable (this ) this .user = new UserStore () } } const store = new Store ()export default store
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import React from 'react' ;import ReactDOM from 'react-dom/client' ;import ErrorBoundary from '@/ErrorBundary' ; import App from '@/App' ;import reportWebVitals from '@/reportWebVitals' ;import { BrowserRouter } from 'react-router-dom' import { Provider } from 'mobx-react' import store from '@/store' import '@/main.scss' ;const root = ReactDOM .createRoot ( document .getElementById ('root' ) as HTMLElement ); root.render ( <React.StrictMode > <ErrorBoundary > <BrowserRouter > <Provider store = { store }> <App /> </Provider > </BrowserRouter > </ErrorBoundary > </React.StrictMode > ); reportWebVitals ();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 import React , { FC ,useMemo,useState } from 'react' ;import { Form , Input , Button , Toast , Modal , NavBar } from 'antd-mobile' import { doLogin } from '@/api/user' ;import { useNavigate, Link } from 'react-router-dom' ;import { inject, observer } from 'mobx-react' ; interface ILoginProps { }; const Login :FC <ILoginProps > = (props: any ) => { console .log ('login' , props) const navigate = useNavigate () const [loginname, setLoginname] = useState ('' ) const [password, setPassword] = useState ('' ) const onFinish = (values: any ) => { console .log (values) doLogin (values).then (res => { console .log (res.data ) if (res.data .code === '10011' ) { Toast .show ({ content : '密码错误' , duration : 1000 }) } else if (res.data .code === '10010' ) { Modal .confirm ({ content : <div > 该用户还未注册</div > , confirmText : '立即注册' , onConfirm : () => { navigate ('/register' ) } }) } 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 ) 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 ) } }) } const onValuesChange = (changedValues: any , allValues: any ) => { console .log (changedValues, allValues) if (allValues.loginname === undefined ) { setPassword (allValues.password ) } else if (allValues.password === undefined ) { setLoginname (allValues.loginname ) } else { setLoginname (allValues.loginname ) setPassword (allValues.password ) } } const flag = useMemo (() => { return loginname === '' || password === '' }, [loginname, password]) return ( <> <header className ="header" > <NavBar style ={{ '--height ': '0.44rem ', '--border-bottom ': '1px #eee solid ', color: '#fff ' }} onBack ={ () => navigate(-1) } > 登录 </NavBar > </header > <div className ="content" > <Form layout ='horizontal' footer ={ <Button block type ='submit' color ='danger' disabled = { flag } size ='large' > 登录 </Button > } onFinish = { onFinish } onValuesChange = { onValuesChange } > <Form.Item name ='loginname' > <Input placeholder ='手机号/用户名/邮箱' clearable /> </Form.Item > <Form.Item name ='password' > <Input placeholder ='密码' clearable type ='password' /> </Form.Item > </Form > <Link to ="/register" > 手机号快速注册</Link > </div > </> ) }; export default inject ('store' )(observer (Login ));
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import React , { FC } from 'react' ;import { observer, inject } from 'mobx-react' interface ICartProps { }; const Cart :FC <ICartProps > = (props: any ) => { console .log (props.store .user ) return ( <> <header className ="header" > cart header</header > <div className ="content" > cart content</div > </> ) }; export default inject ('store' )(observer (Cart ));
15.加入购物车 15.1 封装数据请求 给请求添加token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import axios from 'axios' const isDev = process.env .NODE_ENV === 'development' axios.defaults .withCredentials = false const ins = axios.create ({ baseURL : isDev ? 'http://121.89.205.189:3001/api' : 'http://121.89.205.189:3001/api' , timeout : 6000 }) ins.interceptors .request .use ((config ) => { (config.headers !.token as string ) = localStorage .getItem ('token' )! return config }, (err ) => { return Promise .reject (err) }) ins.interceptors .response .use ((response: any ) => { if (response.data .code === '10119' ) { window .location .href = "/login" return } else { return response } }, (err ) => Promise .reject (err)) export default ins
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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 加入购物车 底部组件提供了 各个选项的事件,需要按照组件提供写法去写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 import React , { FC , useEffect, useState } from 'react' ;import { useNavigate, useParams } from 'react-router-dom' ;import { getDetailData, getDetailRecommendData } from '@/api/detail' import { Divider , Toast } from 'antd-mobile' import { BannerComponent , ProInfoComponent , HeaderComponent , ActionBar } from './components' import { ProComponent } from '@/views/home/components' import { observer, inject } from 'mobx-react' import { addCart } from '@/api/cart' ;interface IDetailProps {}; const Detail :FC <IDetailProps > = (props: any ) => { 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 }) setCurrent (0 ) }) }, [proid]) const [current, setCurrent] = useState (0 ) const onChange = (index: number ) => { setCurrent (index) } const [proList, setProList] = useState ([]) useEffect (() => { getDetailRecommendData ().then (res => setProList (res.data .data )) }, []) const navigate = useNavigate () const addCartData = ( ) => { const loginState = props.store .user .loginState const userid = props.store .user .userid if (loginState) { addCart ({ proid : proid!, userid, num : 1 }).then (res => { if (res) { Toast .show ({ content : '加入购物车成功' }) } else { Toast .show ({ content : '请先登录' }) } }) } else { navigate ('/login' ) } } return ( <> {/* <header className ="header" > Detail header</header > */} <HeaderComponent /> <div className ="content" > {/* 轮播图 */} <BannerComponent banners = { obj.banners } current = { current } onChange = { onChange }/> {/* 产品详情 */} <ProInfoComponent originprice = { obj.originprice } sales = { obj.sales } brand = { obj.brand } category = { obj.category } proname = { obj.proname } /> {/* 猜你喜欢 */} <Divider style ={{ color: '#1677ff ', borderColor: '#1677ff ', borderStyle: 'dashed ', }} > 猜你喜欢 </Divider > <ProComponent list = { proList } /> {/* 底部选项 */} <ActionBar onCartClick = { addCartData } onShopClick = { () => navigate('/cart')} ></ActionBar > </div > </> ) }; export default inject ('store' )(observer (Detail ));
16.购物车相关 16.1 判断登录状态 实现类似于vue的导航守卫,定义路由时处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 import React , { FC } from 'react' ;import { Routes , Route , Navigate } from 'react-router-dom' import '@/App.scss' ;import Cart from '@/views/cart/Index' ;import Home from '@/views/home/Index' ;import Kind from '@/views/kind/Index' ;import User from '@/views/user/Index' ;import Detail from '@/views/detail/Index' ;import Footer from '@/components/Footer' ;import Login from './views/login/Index' ;import Register from './views/register/Index' ;import { Step1 , Step2 , Step3 } from './views/register/components' ;import { inject, observer } from 'mobx-react' ; interface IAppProps {}; const App : FC <IAppProps > = (props:any ) => { return ( <div className ='container' > <Routes > <Route path ="/" element ={ <Navigate to ="/home" replace ={true}/ > } /> {/* <Route path ="/home" element ={ <Home /> } /> <Route path ="/kind" element ={ <Kind /> } /> <Route path ="/cart" element ={ <Cart /> } /> <Route path ="/user" element ={ <User /> } /> */} <Route path ="/home" element ={ <> <Home /> <Footer /> </> } /> <Route path ="/kind" element ={ <> <Kind /> <Footer /> </> } /> {} <Route path="/cart" element={ props.store .user .loginState ? <> <Cart /> <Footer /> </> : <Navigate to ='/login' replace ={true}/ > } /> <Route path ="/user" element ={ <> <User /> <Footer /> </> } /> <Route path ="/detail/:proid" element ={ <Detail /> } /> <Route path ="/login" element ={ <Login /> } /> <Route path ="/register" element ={ <Register /> }> <Route index element = { <Navigate to ="/register/step1" replace ={true} /> } /> <Route path ='/register/step1' element = { <Step1 /> } /> <Route path ='/register/step2' element = { <Step2 /> } /> <Route path ='/register/step3' element = { <Step3 /> } /> </Route > </Routes > {} { } <div className='landscape-tip' > 请将屏幕竖向浏览 </div> </div> ) }; export default inject ('store' )(observer (App )) ;
16.2 判断是否有数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 import React , { FC , useCallback, useEffect, useMemo, useState } from 'react' ;import { observer, inject } from 'mobx-react' import { getCartListData } from '@/api/cart' ;import { Button , Empty , List , Image , Ellipsis , Stepper } from 'antd-mobile' ;import { GiftOutline } from 'antd-mobile-icons' import { useNavigate } from 'react-router-dom' ;interface IState { cartid : string discount : number flag : boolean img1 : string num : number originprice : number proid : string proname : string userid : string } interface ICartProps {}; const Cart :FC <ICartProps > = (props: any ) => { const navigate = useNavigate () const [cartList, setCartList] = useState<IState []>([]) const getCartList = useCallback (() => { const userid = props.store .user .userid console .log ('userid' , userid) getCartListData ({ userid : userid }).then (res => { console .log (res.data ) if (res.data .code === '10020' ) { setCartList ([]) } else { setCartList (res.data .data ) } }) }, [props.store .user .userid ]) useEffect (() => { getCartList () }, [getCartList]) const empty = useMemo (() => { return cartList.length === 0 }, [cartList]) return ( <> <header className ="header" > cart header</header > <div className ="content" > { empty ? <Empty style ={{ padding: '64px 0 ' }} image ={ <GiftOutline style ={{ color: 'var (--adm-color-light )', fontSize: 100 , }} /> } description= { <> <p > 购物车空空如也</p > <Button color ="danger" style ={{ marginTop: 30 }} onClick = { () => navigate('/kind')} >立即购物</Button > </> } /> : <List > { cartList && cartList.map(item => { return ( <List.Item key ={item.cartid} prefix ={ <Image src ={item.img1} style ={{ borderRadius: 20 }} fit ='cover' width ={80} height ={80} /> } description = { <div style ={{ display: 'flex ', justifyContent: 'space-between ' }}> <span > {item.originprice}</span > <Stepper digits ={0} value ={item.num } onChange = { (value ) => { console.log(value) }}/> </div > } > <Ellipsis direction ='end' rows ={2} content ={item.proname} /> </List.Item > ) }) } </List > } </div > </> ) }; export default inject ('store' )(observer (Cart ));
16.3 数量更新 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 import React , { FC , useCallback, useEffect, useMemo, useState } from 'react' ;import { observer, inject } from 'mobx-react' import { getCartListData, updateOneDataNum } from '@/api/cart' ;import { Button , Empty , List , Image , Ellipsis , Stepper } from 'antd-mobile' ;import { GiftOutline } from 'antd-mobile-icons' import { useNavigate } from 'react-router-dom' ;interface IState { cartid : string discount : number flag : boolean img1 : string num : number originprice : number proid : string proname : string userid : string } interface ICartProps {}; const Cart :FC <ICartProps > = (props: any ) => { const navigate = useNavigate () const [cartList, setCartList] = useState<IState []>([]) const getCartList = useCallback (() => { const userid = props.store .user .userid console .log ('userid' , userid) getCartListData ({ userid : userid }).then (res => { console .log (res.data ) if (res.data .code === '10020' ) { setCartList ([]) } else { setCartList (res.data .data ) } }) }, [props.store .user .userid ]) useEffect (() => { getCartList () }, [getCartList]) const empty = useMemo (() => { return cartList.length === 0 }, [cartList]) return ( <> <header className ="header" > cart header</header > <div className ="content" > { empty ? <Empty style ={{ padding: '64px 0 ' }} image ={ <GiftOutline style ={{ color: 'var (--adm-color-light )', fontSize: 100 , }} /> } description= { <> <p > 购物车空空如也</p > <Button color ="danger" style ={{ marginTop: 30 }} onClick = { () => navigate('/kind')} >立即购物</Button > </> } /> : <List > { cartList && cartList.map(item => { return ( <List.Item key ={item.cartid} prefix ={ <Image src ={item.img1} style ={{ borderRadius: 20 }} fit ='cover' width ={80} height ={80} /> } description = { <div style ={{ display: 'flex ', justifyContent: 'space-between ' }}> <span > {item.originprice}</span > <Stepper digits ={0} value ={item.num } onChange = { (value ) => { //++++++++++++++++++++++ console.log(value) updateOneDataNum({ cartid: item.cartid, num: value}).then(() => { getCartList() }) }}/> </div > } > <Ellipsis direction ='end' rows ={2} content ={item.proname} /> </List.Item > ) }) } </List > } </div > </> ) }; export default inject ('store' )(observer (Cart ));
16.4 删除以及选择 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 import React , { FC , useCallback, useEffect, useMemo, useState } from 'react' ;import { observer, inject } from 'mobx-react' import { getCartListData, removeOneData, selectAllData, selectOneData, updateOneDataNum } from '@/api/cart' ;import { Button , Empty , List , Image , Ellipsis , Stepper , SwipeAction , Modal , Checkbox } from 'antd-mobile' ;import { GiftOutline } from 'antd-mobile-icons' import { useNavigate } from 'react-router-dom' ;interface IState { cartid : string discount : number flag : boolean img1 : string num : number originprice : number proid : string proname : string userid : string } interface ICartProps {}; const Cart :FC <ICartProps > = (props: any ) => { const navigate = useNavigate () const [cartList, setCartList] = useState<IState []>([]) const [checked, setChecked] = useState (false ) const getCartList = useCallback (() => { const userid = props.store .user .userid console .log ('userid' , userid) getCartListData ({ userid : userid }).then (res => { console .log (res.data ) if (res.data .code === '10020' ) { setCartList ([]) } else { setCartList (res.data .data ) const val = res.data .data .every ((item: IState ) => item.flag ) setChecked (val) } }) }, [props.store .user .userid ]) useEffect (() => { getCartList () }, [getCartList]) const empty = useMemo (() => { return cartList.length === 0 }, [cartList]) return ( <> <header className ="header" > cart header</header > <div className ="content" > { empty ? <Empty style ={{ padding: '64px 0 ' }} image ={ <GiftOutline style ={{ color: 'var (--adm-color-light )', fontSize: 100 , }} /> } description= { <> <p > 购物车空空如也</p > <Button color ="danger" style ={{ marginTop: 30 }} onClick = { () => navigate('/kind')} >立即购物</Button > </> } /> : <List > { cartList && cartList.map(item => { return ( <SwipeAction key ={item.cartid} rightActions ={[ { key: 'delete ', text: '删除 ', color: 'danger ', onClick: () => { Modal.confirm({ content: <div > 确定删除吗</div > , confirmText: '删除', onConfirm: () => { removeOneData({ cartid: item.cartid}).then(() => { getCartList() }) } }) } }, ]} > <List.Item prefix ={ <div style ={{ display: 'flex ', alignItems: 'center '}}> <div onClick ={e => { e.stopPropagation() selectOneData({ cartid: item.cartid, flag: !item.flag }).then(() => { getCartList() }) }}> <Checkbox checked ={item.flag} /> </div > <Image src ={item.img1} style ={{ borderRadius: 20 }} fit ='cover' width ={80} height ={80} /> </div > } description = { <div style ={{ display: 'flex ', justifyContent: 'space-between ' }}> <span > {item.originprice}</span > <Stepper digits ={0} value ={item.num } onChange = { (value ) => { console.log(value) updateOneDataNum({ cartid: item.cartid, num: value}).then(() => { getCartList() }) }}/> </div > } > <Ellipsis direction ='end' rows ={2} content ={item.proname} /> </List.Item > </SwipeAction > ) }) } <div style ={{ position: 'absolute ', width: '100 %', height: '0.5rem ', backgroundColor: '#efefef ', bottom: 0 , display: 'flex ' }}> <div style ={{ flex: 1 , display: 'flex ', justifyContent: 'start ', alignItems: 'center ', marginLeft: 10 }} onClick = { () => { console.log(1) selectAllData({ userid: props.store.user.userid, type: !checked }).then(() => { getCartList() setChecked(!checked) }) }} > <Checkbox checked = { checked }> 全选</Checkbox > </div > <div style ={{ flex: 1 , display: 'flex ', justifyContent: 'center ', alignItems: 'center ' }}> 总计:10000 </div > <div style ={{ flex: 1 , display: 'flex ', justifyContent: 'center ', alignItems: 'center ' }}> <Button color ='danger' size ='small' > 提交订单</Button > </div > </div > </List > } </div > </> ) }; export default inject ('store' )(observer (Cart ));
16.5 计算总价以及总数量 使用useMemo计算属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 import React , { FC , useCallback, useEffect, useMemo, useState } from 'react' ;import { observer, inject } from 'mobx-react' import { getCartListData, removeOneData, selectAllData, selectOneData, updateOneDataNum } from '@/api/cart' ;import { Button , Empty , List , Image , Ellipsis , Stepper , SwipeAction , Modal , Checkbox } from 'antd-mobile' ;import { GiftOutline } from 'antd-mobile-icons' import { useNavigate } from 'react-router-dom' ;interface IState { cartid : string discount : number flag : boolean img1 : string num : number originprice : number proid : string proname : string userid : string } interface ICartProps {}; const Cart :FC <ICartProps > = (props: any ) => { const navigate = useNavigate () const [cartList, setCartList] = useState<IState []>([]) const [checked, setChecked] = useState (false ) const getCartList = useCallback (() => { const userid = props.store .user .userid console .log ('userid' , userid) getCartListData ({ userid : userid }).then (res => { console .log (res.data ) if (res.data .code === '10020' ) { setCartList ([]) } else { setCartList (res.data .data ) const val = res.data .data .every ((item: IState ) => item.flag ) setChecked (val) } }) }, [props.store .user .userid ]) useEffect (() => { getCartList () }, [getCartList]) const empty = useMemo (() => { return cartList.length === 0 }, [cartList]) 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.num * item.originprice : sum += 0 }, 0 ) }, [cartList]) return ( <> <header className ="header" > cart header</header > <div className ="content" > { empty ? <Empty style ={{ padding: '64px 0 ' }} image ={ <GiftOutline style ={{ color: 'var (--adm-color-light )', fontSize: 100 , }} /> } description= { <> <p > 购物车空空如也</p > <Button color ="danger" style ={{ marginTop: 30 }} onClick = { () => navigate('/kind')} >立即购物</Button > </> } /> : <List > { cartList && cartList.map(item => { return ( <SwipeAction key ={item.cartid} rightActions ={[ { key: 'delete ', text: '删除 ', color: 'danger ', onClick: () => { Modal.confirm({ content: <div > 确定删除吗</div > , confirmText: '删除', onConfirm: () => { removeOneData({ cartid: item.cartid}).then(() => { getCartList() }) } }) } }, ]} > <List.Item prefix ={ <div style ={{ display: 'flex ', alignItems: 'center '}}> <div onClick ={e => { e.stopPropagation() selectOneData({ cartid: item.cartid, flag: !item.flag }).then(() => { getCartList() }) }}> <Checkbox checked ={item.flag} /> </div > <Image src ={item.img1} style ={{ borderRadius: 20 }} fit ='cover' width ={80} height ={80} /> </div > } description = { <div style ={{ display: 'flex ', justifyContent: 'space-between ' }}> <span > {item.originprice}</span > <Stepper digits ={0} value ={item.num } onChange = { (value ) => { console.log(value) updateOneDataNum({ cartid: item.cartid, num: value}).then(() => { getCartList() }) }}/> </div > } > <Ellipsis direction ='end' rows ={2} content ={item.proname} /> </List.Item > </SwipeAction > ) }) } <div style ={{ position: 'absolute ', width: '100 %', height: '0.5rem ', backgroundColor: '#efefef ', bottom: 0 , display: 'flex ' }}> <div style ={{ flex: 1 , display: 'flex ', justifyContent: 'start ', alignItems: 'center ', marginLeft: 10 }} onClick = { () => { console.log(1) selectAllData({ userid: props.store.user.userid, type: !checked }).then(() => { getCartList() setChecked(!checked) }) }} > <Checkbox checked = { checked }> 全选</Checkbox > </div > <div style ={{ flex: 1 , display: 'flex ', flexDirection: 'column ', justifyContent: 'center ', alignItems: 'start ' }}> <p > 总价: <span style ={{ color: '#f66 '}}> { totalPrice }</span > </p > <p > 共计<span style ={{ color: '#f66 '}}> { totalNum }</span > 件商品</p > </div > <div style ={{ flex: 1 , display: 'flex ', justifyContent: 'center ', alignItems: 'center ' }}> <Button color ='danger' size ='small' > 提交订单</Button > </div > </div > </List > } </div > </> ) }; export default inject ('store' )(observer (Cart ));
16.6 购物车头部 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 import React , { FC , useCallback, useEffect, useMemo, useState } from 'react' ;import { observer, inject } from 'mobx-react' import { getCartListData, removeOneData, selectAllData, selectOneData, updateOneDataNum } from '@/api/cart' ;import { Button , Empty , List , Image , Ellipsis , Stepper , SwipeAction , Modal , Checkbox , NavBar } from 'antd-mobile' ;import { GiftOutline } from 'antd-mobile-icons' import { useNavigate } from 'react-router-dom' ;interface IState { cartid : string discount : number flag : boolean img1 : string num : number originprice : number proid : string proname : string userid : string } interface ICartProps {}; const Cart :FC <ICartProps > = (props: any ) => { const navigate = useNavigate () const [cartList, setCartList] = useState<IState []>([]) const [checked, setChecked] = useState (false ) const getCartList = useCallback (() => { const userid = props.store .user .userid console .log ('userid' , userid) getCartListData ({ userid : userid }).then (res => { console .log (res.data ) if (res.data .code === '10020' ) { setCartList ([]) } else { setCartList (res.data .data ) const val = res.data .data .every ((item: IState ) => item.flag ) setChecked (val) } }) }, [props.store .user .userid ]) useEffect (() => { getCartList () }, [getCartList]) const empty = useMemo (() => { return cartList.length === 0 }, [cartList]) 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.num * item.originprice : sum += 0 }, 0 ) }, [cartList]) return ( <> <header className ="header" > <NavBar style ={{ '--height ': '0.44rem ', '--border-bottom ': '1px #eee solid ', color: '#fff ' }} onBack ={ () => navigate(-1) } > 购物车 </NavBar > </header > <div className ="content" > { empty ? <Empty style ={{ padding: '64px 0 ' }} image ={ <GiftOutline style ={{ color: 'var (--adm-color-light )', fontSize: 100 , }} /> } description= { <> <p > 购物车空空如也</p > <Button color ="danger" style ={{ marginTop: 30 }} onClick = { () => navigate('/kind')} >立即购物</Button > </> } /> : <List > { cartList && cartList.map(item => { return ( <SwipeAction key ={item.cartid} rightActions ={[ { key: 'delete ', text: '删除 ', color: 'danger ', onClick: () => { Modal.confirm({ content: <div > 确定删除吗</div > , confirmText: '删除', onConfirm: () => { removeOneData({ cartid: item.cartid}).then(() => { getCartList() }) } }) } }, ]} > <List.Item prefix ={ <div style ={{ display: 'flex ', alignItems: 'center '}}> <div onClick ={e => { e.stopPropagation() selectOneData({ cartid: item.cartid, flag: !item.flag }).then(() => { getCartList() }) }}> <Checkbox checked ={item.flag} /> </div > <Image src ={item.img1} style ={{ borderRadius: 20 }} fit ='cover' width ={80} height ={80} /> </div > } description = { <div style ={{ display: 'flex ', justifyContent: 'space-between ' }}> <span > {item.originprice}</span > <Stepper digits ={0} value ={item.num } onChange = { (value ) => { console.log(value) updateOneDataNum({ cartid: item.cartid, num: value}).then(() => { getCartList() }) }}/> </div > } > <Ellipsis direction ='end' rows ={2} content ={item.proname} /> </List.Item > </SwipeAction > ) }) } <div style ={{ position: 'absolute ', width: '100 %', height: '0.5rem ', backgroundColor: '#efefef ', bottom: 0 , display: 'flex ' }}> <div style ={{ flex: 1 , display: 'flex ', justifyContent: 'start ', alignItems: 'center ', marginLeft: 10 }} onClick = { () => { console.log(1) selectAllData({ userid: props.store.user.userid, type: !checked }).then(() => { getCartList() setChecked(!checked) }) }} > <Checkbox checked = { checked }> 全选</Checkbox > </div > <div style ={{ flex: 1 , display: 'flex ', flexDirection: 'column ', justifyContent: 'center ', alignItems: 'start ' }}> <p > 总价: <span style ={{ color: '#f66 '}}> { totalPrice }</span > </p > <p > 共计<span style ={{ color: '#f66 '}}> { totalNum }</span > 件商品</p > </div > <div style ={{ flex: 1 , display: 'flex ', justifyContent: 'center ', alignItems: 'center ' }}> <Button color ='danger' size ='small' > 提交订单</Button > </div > </div > </List > } </div > </> ) }; export default inject ('store' )(observer (Cart ));
17.项目上线 默认执行cnpm run build
打包出来的项目资源以绝对路径方式引入
通过给 package.json
文件中添加"homepage": "./"
更改为相对路径
然后修改 入口文件处,使用 HashRouter 代替 BrowserRouter
然后修改 utils/request,未登陆时 跳转地址改为 /#/login
执行cnpm run build
打包,上传服务器,服务器测试
http://121.89.205.189:3001/gpreact/
测试账号: 188113007812 Xagp01 注册账号:http://121.89.205.189:3001/gpvue/#/register/index