├── src ├── components │ ├── Header.jsx │ └── indexCard.jsx ├── assets │ ├── yay.jpg │ └── css │ │ ├── login.less │ │ └── home.less ├── pages │ ├── docs.tsx │ ├── about.jsx │ ├── my.jsx │ ├── index.tsx │ ├── login.jsx │ └── home.jsx ├── routes │ └── index.js ├── layouts │ ├── index.less │ └── index.jsx ├── models │ └── user.js ├── api │ └── user.js └── utils │ └── request.js ├── typings.d.ts ├── .npmrc ├── tsconfig.json ├── README.md ├── .gitignore ├── .umirc.prod.js ├── .umirc.test.js ├── .umirc.uat.js ├── mock └── user.ts ├── .umirc.js └── package.json /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/indexCard.jsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | import 'umi/typings'; 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://mirrors.huaweicloud.com/repository/npm/ 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./src/.umi/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/yay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lakyia/react-h5-template/HEAD/src/assets/yay.jpg -------------------------------------------------------------------------------- /src/assets/css/login.less: -------------------------------------------------------------------------------- 1 | .loginContainer { 2 | position: relative; 3 | background: #fff; 4 | height: 100vh; 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-h5-template 2 | ## 基于 react18 + umi4 + antd mobile 5 + less + dva + mock + viewport 适配方案 + axios 封装 + 多环境配置,构建手机端模板,react移动脚手架 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.env.local 3 | /.umirc.local.ts 4 | /config/config.local.ts 5 | /src/.umi 6 | /src/.umi-production 7 | /src/.umi-test 8 | /dist 9 | .DS_Store -------------------------------------------------------------------------------- /src/pages/docs.tsx: -------------------------------------------------------------------------------- 1 | const DocsPage = () => { 2 | return ( 3 |
4 |

This is umi docs.

5 |
6 | ); 7 | }; 8 | 9 | export default DocsPage; 10 | -------------------------------------------------------------------------------- /.umirc.prod.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "umi"; 2 | 3 | export default defineConfig({ 4 | define: { 5 | baseApi: "https://api.somedomain.com", 6 | "process.env": { 7 | NODE_ENV: "uat", 8 | UMI_ENV: "uat", 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /.umirc.test.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "umi"; 2 | 3 | export default defineConfig({ 4 | define: { 5 | baseApi: "https://apitest.somedomain.com", 6 | "process.env": { 7 | NODE_ENV: "uat", 8 | UMI_ENV: "uat", 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /.umirc.uat.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "umi"; 2 | 3 | export default defineConfig({ 4 | define: { 5 | baseApi: "https://apiuat.somedomain.com", 6 | "process.env": { 7 | NODE_ENV: "uat", 8 | UMI_ENV: "uat", 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/pages/about.jsx: -------------------------------------------------------------------------------- 1 | export default function AboutPage() { 2 | return ( 3 |
4 |

This is About page

5 |

6 | To get started, edit pages/about.tsx and save to reload. 7 |

8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /mock/user.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "/phu/captcha/getCaptcha": { 3 | data: { 4 | captcha: "base64.......", 5 | captchaToken: "token....", 6 | }, 7 | code: 200, 8 | msg: "成功", 9 | }, 10 | // 返回值也可以是对象形式 11 | "/api/users/1": { id: 1, name: "foo" }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | routes: [ 3 | { path: "/login", component: "login" }, 4 | { 5 | path: "/", 6 | component: "@/layouts/index", 7 | routes: [ 8 | { path: "/home", component: "home" }, 9 | { path: "/my", component: "my" }, 10 | { path: "/about", component: "about" }, 11 | ], 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /src/pages/my.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from "umi"; 2 | 3 | function MyPage(props) { 4 | const { user, dispatch } = props; 5 | console.log(user); 6 | const val = user.user.test; 7 | return ( 8 |
9 |

{val}

10 |
11 | ); 12 | } 13 | const stateToProps = ({ user }) => { 14 | return { 15 | user, 16 | }; 17 | }; 18 | export default connect(stateToProps)(MyPage); 19 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import yayJpg from "../assets/yay.jpg"; 2 | 3 | export default function HomePage() { 4 | return ( 5 |
6 |

Yay! Welcome to umi!

7 |

8 | 9 |

10 |

11 | To get started, edit pages/index.tsx and save to reload. 12 |

13 |

去首页看看。

14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /.umirc.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "umi"; 2 | const pxtorem = require("postcss-pxtorem"); 3 | 4 | export default defineConfig({ 5 | npmClient: "npm", 6 | extraPostCSSPlugins: [ 7 | pxtorem({ 8 | rootValue: 75, //这里根据设计稿大小配置,一般是375 9 | propList: ["*"], 10 | }), 11 | ], 12 | define: { 13 | baseApi: "https://apidev.somedomain.com", 14 | "process.env": { 15 | NODE_ENV: "dev", 16 | UMI_ENV: "dev", 17 | }, 18 | }, 19 | plugins: ["@umijs/plugins/dist/dva"], 20 | dva: {}, 21 | }); 22 | -------------------------------------------------------------------------------- /src/layouts/index.less: -------------------------------------------------------------------------------- 1 | .app { 2 | height: 100vh; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .top { 8 | flex: 0; 9 | border-bottom: solid 1px var(--adm-color-border); 10 | } 11 | 12 | .bottom { 13 | z-index: 99999; 14 | position: fixed; 15 | width: 100%; 16 | bottom: 0; 17 | height: fit-content; 18 | background-color: white; 19 | border-top: solid 1px var(--adm-color-border); 20 | .tabbar { 21 | padding: 10px 0; 22 | font-size: 24px; 23 | height: 110px; 24 | .tabbarItem { 25 | height: 110px; 26 | justify-content: space-around; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/models/user.js: -------------------------------------------------------------------------------- 1 | // import { queryUsers, queryUser } from "../../services/user"; 2 | 3 | export default { 4 | state: { 5 | user: { 6 | test: "test123", 7 | }, 8 | }, 9 | 10 | effects: { 11 | *queryUser({ payload }, { call, put }) { 12 | // const { data } = yield call(queryUser, payload); 13 | // yield put({ type: "queryUserSuccess", payload: data }); 14 | }, 15 | }, 16 | 17 | reducers: { 18 | queryUserSuccess(state, { payload }) { 19 | return { 20 | ...state, 21 | user: payload, 22 | }; 23 | }, 24 | }, 25 | 26 | test(state) { 27 | console.log("test"); 28 | return state; 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "author": "Lakshya", 4 | "scripts": { 5 | "build": "umi build", 6 | "dev": "umi dev", 7 | "postinstall": "umi setup", 8 | "setup": "umi setup", 9 | "start": "npm run dev", 10 | "start:uat": "cross-env UMI_ENV=uat umi dev" 11 | }, 12 | "dependencies": { 13 | "antd-mobile": "^5.24.1", 14 | "axios": "^1.1.3", 15 | "dva": "^2.4.1", 16 | "lib-flexible": "^0.3.2", 17 | "normalize.css": "^8.0.1", 18 | "postcss-pxtorem": "^6.0.0", 19 | "redux": "^4.2.0", 20 | "umi": "^4.0.25" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^18.0.0", 24 | "@types/react-dom": "^18.0.0", 25 | "@umijs/plugins": "^4.0.26", 26 | "cross-env": "^7.0.3", 27 | "typescript": "^4.1.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | // axios 2 | import request from "@/utils/request"; 3 | 4 | //################### #使用mock时不设置baseURL ####################// 5 | import axios from "axios"; 6 | // create an axios instance 7 | const service = axios.create({ 8 | //baseURL: baseApi, // url = base api url + request url 如果使用 mock 去掉此项 9 | withCredentials: false, // send cookies when cross-domain requests 10 | crossDomain: true, 11 | timeout: 10000, // request timeout 12 | }); 13 | export function getCaptchaMock(params) { 14 | return service({ 15 | url: "/user/captcha/getCaptcha", 16 | withCredentials: false, 17 | method: "get", 18 | params, 19 | }); 20 | } 21 | //################### #使用mock时不设置baseURL ####################// 22 | 23 | export function getCaptcha(params) { 24 | return request({ 25 | url: "/user/captcha/getCaptcha", 26 | withCredentials: false, 27 | method: "get", 28 | params, 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/assets/css/home.less: -------------------------------------------------------------------------------- 1 | .indexContainer { 2 | overflow-x: hidden; 3 | padding-bottom: 125px; 4 | .indexHeader { 5 | display: flex; 6 | height: 100px; 7 | justify-content: space-between; 8 | align-items: center; 9 | padding: 32px 4.3vw 16px; 10 | position: fixed; 11 | width: 100%; 12 | background-color: #f5f5f5; 13 | } 14 | 15 | .indexBody { 16 | padding-top: 150px; 17 | .indexTop { 18 | margin: 32px; 19 | font-size: 32px; 20 | .desc { 21 | } 22 | .btn { 23 | margin: 32px 0; 24 | background-color: chocolate; 25 | color: #fff; 26 | } 27 | } 28 | .swiper { 29 | margin: 16px 32px; 30 | width: calc(100vw - 64px); 31 | .swiperItem { 32 | height: 175px; 33 | } 34 | .customIndicator { 35 | display: flex; 36 | justify-content: center; 37 | border-radius: 4px; 38 | } 39 | .indicator { 40 | width: 40px; 41 | height: 8px; 42 | background-color: #fff; 43 | } 44 | .activeIndicator { 45 | border-radius: 4px; 46 | background-color: rgb(255, 88, 20); 47 | } 48 | } 49 | .banner { 50 | border-radius: 20px; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/pages/login.jsx: -------------------------------------------------------------------------------- 1 | import { Checkbox, Input, Button, NavBar } from "antd-mobile"; 2 | import { useState } from "react"; 3 | import { getCaptcha, getCaptchaMock } from "@/api/user.js"; 4 | import { useNavigate } from "react-router-dom"; 5 | import styles from "@/assets/css/login.less"; 6 | export default function LoginPage() { 7 | const getCaptchaFunc = () => { 8 | getCaptcha(); 9 | getCaptchaMock(); 10 | }; 11 | const navigate = useNavigate(); 12 | return ( 13 | <> 14 | navigate(-1)} 20 | > 21 | 标题 22 | 23 |
24 |
25 |
26 |
27 | 37 |
38 |
39 |
40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | // create an axios instance 3 | const service = axios.create({ 4 | baseURL: baseApi, // url = base api url + request url 如果使用 mock 去掉此项 5 | withCredentials: false, 6 | crossDomain: true, 7 | timeout: 10000, // request timeout 8 | }); 9 | // request拦截器 request interceptor 10 | service.interceptors.request.use( 11 | (config) => { 12 | // 设置用户token 13 | return config; 14 | }, 15 | (error) => { 16 | // do something with request error 17 | console.log(error); // for debug 18 | return Promise.reject(error); 19 | } 20 | ); 21 | // respone拦截器 22 | service.interceptors.response.use( 23 | (response) => { 24 | const res = response.data; 25 | console.log("response-->", res); 26 | if (res.status && res.status !== 200) { 27 | } else { 28 | if (res.code === 200 || res.message === "SUCCESS") { 29 | return Promise.resolve(res); 30 | } else if (res.code === 305 || res.code === 306) { 31 | Promise.reject(res || "error"); 32 | return; 33 | } else { 34 | return Promise.reject(res || "error"); 35 | } 36 | } 37 | }, 38 | (error) => { 39 | console.log("request error" + error, typeof error); // for debug 40 | return Promise.reject(error); 41 | } 42 | ); 43 | 44 | export default service; 45 | -------------------------------------------------------------------------------- /src/pages/home.jsx: -------------------------------------------------------------------------------- 1 | import { PullToRefresh, Button, Swiper, Toast } from "antd-mobile"; 2 | import { useNavigate } from "react-router-dom"; 3 | import styles from "@/assets/css/home.less"; 4 | const colors = ["#ace0ff", "#bcffbd", "#e4fabd", "#ffcfac"]; 5 | 6 | const items = colors.map((color, index) => ( 7 | 8 |
{ 12 | Toast.show(`你点击了卡片 ${index + 1}`); 13 | }} 14 | > 15 | {index + 1} 16 |
17 |
18 | )); 19 | const Home = () => { 20 | const navigate = useNavigate(); 21 | 22 | return ( 23 |
24 |
首页Header
25 |
26 | { 28 | console.log("PullToRefresh"); 29 | }} 30 | > 31 |
32 |
33 | {"一个基于umi4 antd mobile5 的react移动端脚手架;"} 34 |
35 |
{"含dva,mock,多环境配置等;"}
36 | 49 |
50 | ( 56 |
57 | {Array(total) 58 | .fill(1) 59 | .map((el, i) => ( 60 |
66 | ))} 67 |
68 | )} 69 | loop 70 | > 71 | {items} 72 | 73 | 74 |
75 |
76 | ); 77 | }; 78 | 79 | export default Home; 80 | -------------------------------------------------------------------------------- /src/layouts/index.jsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "umi"; 2 | import { useNavigate, matchRoutes, useLocation } from "react-router-dom"; 3 | import { useEffect, useState } from "react"; 4 | import router from "@/routes"; 5 | import "lib-flexible"; 6 | import { TabBar, SafeArea } from "antd-mobile"; 7 | import { AppOutline, HeartOutline, UserOutline } from "antd-mobile-icons"; 8 | import "normalize.css/normalize.css"; //全局引入 9 | import styles from "./index.less"; 10 | export default function Layout() { 11 | const location = useLocation(); 12 | const [defaultSelectedKeys, setDefaultSelectedKeys] = useState([]); 13 | const [defaultOpenKeys, setDefaultOpenKeys] = useState([]); 14 | const [isInit, setIsInit] = useState(false); 15 | const navigate = useNavigate(); 16 | const { pathname } = location; 17 | useEffect(() => { 18 | console.log(router, location.pathname); 19 | const routes = matchRoutes(router.routes, location.pathname); // 返回匹配到的路由数组对象,每一个对象都是一个路由对象 20 | const pathArr = []; 21 | if (routes !== null) { 22 | routes.forEach((item) => { 23 | const path = item.route.path; 24 | if (path) { 25 | pathArr.push(path); 26 | } 27 | }); 28 | } 29 | setDefaultSelectedKeys(pathArr); 30 | setDefaultOpenKeys(pathArr); 31 | setIsInit(true); 32 | }, [location.pathname]); 33 | if (!isInit) { 34 | return null; 35 | } 36 | 37 | const setRouteActive = (value) => { 38 | navigate(value); 39 | }; 40 | 41 | const tabs = [ 42 | { 43 | key: "/home", 44 | title: "首页", 45 | icon: , 46 | }, 47 | { 48 | key: "/about", 49 | title: "关于", 50 | icon: , 51 | }, 52 | 53 | { 54 | key: "/my", 55 | title: "我的", 56 | icon: , 57 | }, 58 | ]; 59 | return ( 60 | <> 61 |
62 | 63 | 64 |
65 | {["/home", "/about", "/my"].includes(pathname) && ( 66 |
67 | setRouteActive(value)} 72 | > 73 | {tabs.map((item) => ( 74 | 80 | ))} 81 | 82 | 83 |
84 | )} 85 | 86 | ); 87 | } 88 | --------------------------------------------------------------------------------