├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .markdownlint.yml ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── README.md ├── babel.config.js ├── commitlint.config.js ├── config ├── index.js ├── local.js └── plugins │ └── index.js ├── docs └── images │ └── webpack-bundle-analyzer.png ├── generators ├── component │ ├── component.hbs │ ├── index.js │ └── scss.hbs ├── index.js ├── page │ ├── config.hbs │ ├── index.js │ ├── page.hbs │ └── scss.hbs ├── service │ ├── index.js │ └── service.hbs └── store │ ├── index.js │ └── store.hbs ├── global.d.ts ├── package.json ├── pnpm-lock.yaml ├── project.config.json ├── src ├── app.config.ts ├── app.scss ├── app.tsx ├── custom-tab-bar │ ├── index.scss │ └── index.tsx ├── default │ ├── 404.config.ts │ ├── 404.scss │ └── 404.tsx ├── demo │ ├── form │ │ ├── form.config.ts │ │ ├── form.scss │ │ └── form.tsx │ └── router │ │ ├── router.config.ts │ │ ├── router.scss │ │ ├── router.tsx │ │ ├── routerTarget.config.ts │ │ ├── routerTarget.scss │ │ └── routerTarget.tsx ├── index.html ├── index │ ├── index.config.ts │ ├── index.scss │ └── index.tsx ├── shared │ ├── assets │ │ └── icons │ │ │ ├── back.svg │ │ │ ├── home.png │ │ │ ├── home.svg │ │ │ ├── home_selected.png │ │ │ ├── user.png │ │ │ └── user_selected.png │ ├── components │ │ ├── navigation_header │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── page_container │ │ │ └── index.tsx │ │ └── rich_text_renderer │ │ │ ├── index.scss │ │ │ └── index.tsx │ ├── constants │ │ ├── code.ts │ │ └── index.ts │ ├── interceptors │ │ ├── data.interceptor.ts │ │ ├── del.interceptor.ts │ │ ├── header.interceptor.ts │ │ ├── param.interceptor.ts │ │ └── url.interceptor.ts │ ├── services │ │ └── root │ │ │ └── demo.service.ts │ ├── store │ │ ├── app.ts │ │ └── index.ts │ ├── styles │ │ ├── mixin.scss │ │ └── var.scss │ └── utils │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── page.ts │ │ ├── request.ts │ │ ├── route.ts │ │ ├── toast.ts │ │ └── validator.ts ├── tabbar.config.ts ├── user │ ├── index.config.ts │ ├── index.scss │ └── index.tsx └── webview │ ├── index.config.ts │ ├── index.scss │ └── index.tsx └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 'taro/react', 'plugin:@typescript-eslint/recommended' ], 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ '@typescript-eslint' ], 5 | rules: { 6 | 'arrow-parens': [ 'error', 'as-needed' ], // 箭头函数单参数时不使用括号 多参数时使用括号 7 | 'object-curly-newline': [ 8 | // 强制在花括号内使用一致的换行符 9 | 'off', 10 | { 11 | minProperties: 2, // 属性数量超过2时强制使用换行符 12 | }, 13 | ], 14 | 'object-property-newline': [ 15 | // 强制将对象的属性放在不同的行上 16 | 'off', 17 | { 18 | allowAllPropertiesOnSameLine: true, // // 禁止所有的属性都放在同一行 19 | }, 20 | ], 21 | 22 | 'object-curly-spacing': [ 'error', 'always' ], // 要求大括号与内容间总是有空格 23 | 'dot-location': [ 'error', 'property' ], // 强制在点号之后换行 object-跟随对象 property-跟随属性 24 | curly: 'error', // 强制所有控制语句使用一致的括号风格 25 | 'import/no-commonjs': [ 'off' ], // 禁止commonjs写法 如module.exports 26 | complexity: [ 'off', 16 ], // 限制圈复杂度 阈值3 如if else if else语句最多嵌套三层 TODO: 需要放开 27 | 'react/jsx-indent-props': 0, // 不验证jsx缩进 28 | 'no-unused-vars': [ 29 | // 不允许未使用的变量 30 | 'warn', 31 | { 32 | varsIgnorePattern: 'Taro', // Taro框架要求在使用class组件的时候必须在文件中声明Taro 但是不是所有文件都会显式使用到 所以忽略 33 | }, 34 | ], 35 | 'arrow-spacing': [ 36 | // 要求箭头函数的箭头之前或之后有空格 37 | 'error', 38 | { 39 | before: true, 40 | after: true, 41 | }, 42 | ], 43 | 'prefer-arrow-callback': [ 'error' ], // 要求使用箭头函数作为回调 44 | 'react/no-string-ref': 0, // 不允许字符串ref 45 | 'react/jsx-filename-extension': [ 46 | // 识别jsx的文件扩展名 47 | 1, 48 | { 49 | extensions: [ '.js', '.jsx', '.tsx' ], 50 | }, 51 | ], 52 | '@typescript-eslint/no-unused-vars': [ 53 | // 禁止未使用的变量 54 | 'error', 55 | { 56 | varsIgnorePattern: 'Taro', // 忽略正则 57 | }, 58 | ], 59 | '@typescript-eslint/member-delimiter-style': [ 60 | 'error', 61 | { 62 | multiline: { 63 | delimiter: 'none', 64 | requireLast: false, 65 | }, 66 | singleline: { 67 | delimiter: 'semi', 68 | requireLast: false, 69 | }, 70 | }, 71 | ], 72 | '@typescript-eslint/explicit-function-return-type': [ 'off' ], // function和class的方法必须有明确的返回值 73 | '@typescript-eslint/no-empty-function': [ 'warn' ], // 禁止空函数体 74 | '@typescript-eslint/no-var-requires': 0, // 在import引用之外禁止require引用 75 | 'import/first': 0, // import必须位于文件头部 76 | '@typescript-eslint/no-explicit-any': 0, // 禁止any声明 77 | '@typescript-eslint/interface-name-prefix': 0, // interface名必须以大写字母I开头 78 | 'import/newline-after-import': 0, // import之后必须隔行 79 | '@typescript-eslint/camelcase': 0, // 变量必须使用驼峰命名 80 | '@typescript-eslint/no-this-alias': 0, // 禁止将this赋值给其他变量 81 | }, 82 | parserOptions: { 83 | ecmaFeatures: { 84 | jsx: true, 85 | }, 86 | useJSXTextNode: true, 87 | project: './tsconfig.json', 88 | }, 89 | } 90 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 构建部署H5应用 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 拉取仓库代码 12 | uses: actions/checkout@v2 13 | with: 14 | persist-credentials: false 15 | 16 | - name: 安装依赖 17 | run: | 18 | npm install pnpm@8.9.2 -g 19 | pnpm i 20 | pnpm build:h5 21 | - name: 部署应用 22 | uses: JamesIves/github-pages-deploy-action@releases/v3 23 | with: 24 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 25 | BRANCH: gh-pages 26 | FOLDER: dist 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | deploy_versions/ 3 | .temp/ 4 | .rn_temp/ 5 | node_modules/ 6 | .DS_Store 7 | 8 | package-lock.json 9 | yarn-error.log 10 | yarn.lock 11 | 12 | src/pages.js 13 | src/subPackages.js 14 | src/components/index.ts 15 | 16 | .env 17 | 18 | .swc 19 | -------------------------------------------------------------------------------- /.markdownlint.yml: -------------------------------------------------------------------------------- 1 | { 'default': true } 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com/ 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.hbs 2 | *.yml 3 | *.yaml 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("@lexmin0412/prettier-config-base") 3 | }; 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # taro3-react-template 2 | 3 | 基于 Taro3 + React 的开箱即用多端项目模板。 4 | 5 | ## 相关仓库 6 | 7 | - [taro2-template](https://github.com/lexmin0412/taro2-template/tree/2.x) - 基于 Taro2 的项目模板。 8 | 9 | ## Repo Activity 10 | 11 | ![Repo Activity](https://repobeats.axiom.co/api/embed/e39e5816e00d2a9627dca894852446b7f7c83463.svg "Repobeats analytics image") 12 | 13 | ## 技术栈 14 | 15 | - Taro 16 | - React Hooks 17 | - TypeScript 18 | - NutUI 19 | - SCSS 20 | 21 | ## 运行环境 22 | 23 | - Node 16+ 24 | - Pnpm 8+ 25 | 26 | ## 适配平台 27 | 28 | - [x] H5 29 | - [x] 微信小程序 30 | 31 | 由于部分平台的小程序注册门槛较高,如抖音、京东等平台均无法通过个人开发者身份注册,目前此模板只在 h5 / 微信小程序端经过验证。 32 | 33 | ## 开始 34 | 35 | 基于 [上述原因](#适配平台), 此模板只提供了 H5/微信小程序的调试/构建命令。 36 | 37 | ### 本地开发 38 | 39 | #### 环境变量构建 40 | 41 | 在根目录新建 .env 文件,写入环境变量 42 | 43 | ```bash 44 | TARO_API_BASE=/api 45 | ``` 46 | 47 | #### 启动调试 48 | 49 | ```shell 50 | pnpm dev:h5 # h5 51 | pnpm dev:weapp # 微信小程序 52 | pnpm preview:weapp-local-watch # 启动小程序本地调试并打开微信开发者工具 53 | ``` 54 | 55 | ### 构建产物 56 | 57 | ```shell 58 | pnpm build:h5 # h5 59 | pnpm build:weapp # 微信小程序 60 | ``` 61 | 62 | ## 支持特性 63 | 64 | - 🏠 基于 Taro3 65 | - 📦 支持 React 66 | - 🐑 CSS 预处理器( SCSS ) 67 | - 🥣 完全使用 TypeScript 开发 68 | - 🔛 企业级的 request 类及拦截器封装 69 | - 👮 `eslint`+`stylelint`+`markdownlint`+`prettier`+`commitlint`+`editorConfig` 实现的全方位代码规范体系 70 | - 🔥 自定义 tabbar 71 | - 🌩️ 使用多核心及缓存提升编译速度 72 | - 💰 更多特性持续迭代中... 73 | 74 | ## 目录结构 75 | 76 | ```bash 77 | - .github # Github 相关配置 78 | - config # Taro 配置 79 | - generators # Plop 模版配置 80 | - src #源码目录 81 | - custom-tab-bar # 自定义 tabbar 82 | - default # 缺省页面 83 | - demo # 演示页面 84 | - index # 首页 85 | - shared # 公用代码 86 | - assets # 静态资源 87 | - icons # 图标 88 | - components # 公共组件 89 | - constants # 常量 90 | - interceptors # 拦截器 91 | - services # 服务类 92 | - store # 状态管理 93 | - styles # 公共样式 94 | - utils # 工具类 95 | - user # 用户相关页面 96 | - webview # webview 功能演示 97 | - .editorConfig # 编辑器编码风格配置 98 | - .env # 本地环境配置 99 | - .eslintrc.js # eslint 配置 100 | - .gitignore # git 忽略配置 101 | - .markdownlint.yml # markdownlint 配置 102 | - .npmrc # npm 相关配置 103 | - .prettierignore # prettier 忽略配置 104 | - babel.config.js # babel 配置文件 105 | - commitlint.config.js # commitlint 配置 106 | - global.d.ts # ts 全局类型声明 107 | - package.json # 你懂的 108 | - pnpm-lock.yaml # pnpm 依赖锁文件 109 | - project.config.json # 微信小程序配置 110 | - README.md # 项目说明文档 111 | - tsconfig.json # ts 配置 112 | ``` 113 | 114 | 说明:与一般的项目结构划分不同,这里将所有静态资源、公用组件、状态管理等功能统一放到了 `src/shared` 目录下,而将页面目录直接平铺到了 src 目录,是出于以下原因: 115 | 116 | - 在 src 下直接按照模块组织能够更直观地展示整个系统的业务组成及模块划分 117 | - 没有规范限制页面文件一定要放在 `src/pages` 目录下,在遵循小程序规范设计配置文件的大前提下,它只会带来书写的负担 118 | - 一个文件,它要么是全局共享,要么是专为某个页面(模块)服务,`{shared, xxx, yyy, zzz}` 的目录结构会促使我在新增一个文件时更加谨慎地考虑它的作用范围 119 | 120 | 当然,这纯属主观偏好,如果你想将页面文件放在 pages 目录下,那也无妨。 121 | 122 | ## Star History 123 | 124 | [![Star History Chart](https://api.star-history.com/svg?repos=lexmin0412/taro3-react-template&type=Timeline)](https://star-history.com/#lexmin0412/taro3-react-template&Timeline) 125 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel-preset-taro 更多选项和默认值: 2 | // https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md 3 | module.exports = { 4 | presets: [ 5 | ['taro', { 6 | framework: 'react', 7 | ts: true 8 | }] 9 | ], 10 | plugins: [ 11 | [ 12 | "import", 13 | { 14 | "libraryName": "@nutui/nutui-react-taro", 15 | "libraryDirectory": "dist/esm", 16 | "style": true, 17 | "camel2DashComponentName": false 18 | }, 19 | 'nutui-react-taro' 20 | ] 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@youtils/commitlint-plugin-standard'], 3 | } 4 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const plugins = require('./plugins') 3 | 4 | const config = { 5 | projectName: 'taro3-react-template', 6 | date: '2021-12-10', 7 | designWidth(input) { 8 | // nutui 组件库特殊处理 9 | if (input?.file && input.file.replace(/\\+/g, '/').indexOf('@nutui/nutui-react-taro') > -1) { 10 | return 375 11 | } 12 | return 750 13 | }, 14 | deviceRatio: { 15 | 375: 2 / 1, 16 | 640: 2.34 / 2, 17 | 750: 1, 18 | 828: 1.81 / 2, 19 | }, 20 | sourceRoot: 'src', 21 | outputRoot: 'dist', 22 | // 解析alias路径 23 | alias: { 24 | '@': path.resolve(__dirname, '..', 'src/shared'), 25 | }, 26 | plugins, 27 | sass: { 28 | data: `@import "@nutui/nutui-react-taro/dist/styles/variables.scss";` 29 | }, 30 | defineConstants: {}, 31 | copy: { 32 | patterns: [], 33 | options: {}, 34 | }, 35 | compiler: { 36 | type: 'webpack5', 37 | prebundle: { 38 | // exclude 掉第三方库,规避 prebundle 模式下可能出现的报错 39 | exclude: ['@nutui/nutui-react-taro'] 40 | }, 41 | }, 42 | framework: 'react', 43 | mini: { 44 | postcss: { 45 | pxtransform: { 46 | enable: true, 47 | config: {}, 48 | }, 49 | url: { 50 | enable: true, 51 | config: { 52 | limit: 1024, // 设定转换尺寸上限 53 | }, 54 | }, 55 | cssModules: { 56 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true 57 | config: { 58 | namingPattern: 'module', // 转换模式,取值为 global/module 59 | generateScopedName: '[name]__[local]___[hash:base64:5]', 60 | }, 61 | }, 62 | }, 63 | }, 64 | h5: { 65 | publicPath: '/taro3-react-template', 66 | staticDirectory: 'static', 67 | router: { 68 | mode: 'browser', // 使用history模式 69 | basename: '', 70 | }, 71 | postcss: { 72 | autoprefixer: { 73 | enable: true, 74 | config: {}, 75 | }, 76 | cssModules: { 77 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true 78 | config: { 79 | namingPattern: 'module', // 转换模式,取值为 global/module 80 | generateScopedName: '[name]__[local]___[hash:base64:5]', 81 | }, 82 | }, 83 | }, 84 | }, 85 | } 86 | 87 | module.exports = function (merge) { 88 | // 如果是本地调试 则合并开发变量 89 | if (process.env.NODE_ENV === 'development') { 90 | return merge({}, config, require('./local')) 91 | } 92 | return config 93 | } 94 | -------------------------------------------------------------------------------- /config/local.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | h5: { 3 | devServer: { 4 | port: '9001', 5 | open: false, 6 | https: false, 7 | proxy: { 8 | '/api': { 9 | target: 'https://api.cellerchan.top', 10 | changeOrigin: true, 11 | ws: false, 12 | pathRewrite: { 13 | '^/api': ``, 14 | }, 15 | }, 16 | }, 17 | }, 18 | }, 19 | } 20 | 21 | module.exports = config 22 | -------------------------------------------------------------------------------- /config/plugins/index.js: -------------------------------------------------------------------------------- 1 | const plugins = [ 2 | '@tarojs/plugin-html' 3 | ] 4 | 5 | if (process.env.TARO_ENV === 'weapp') { 6 | plugins.push('taro-plugin-compiler-optimization') 7 | } 8 | plugins.push('taro-plugin-dotenv') // 注入.env环境变量 9 | 10 | module.exports = plugins 11 | -------------------------------------------------------------------------------- /docs/images/webpack-bundle-analyzer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexmin0412/taro3-react-template/d5955632ff2a083fee54ea9c535b75713de8674d/docs/images/webpack-bundle-analyzer.png -------------------------------------------------------------------------------- /generators/component/component.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * {{FILE_DESC}} 3 | */ 4 | 5 | import React from 'react' 6 | import { View } from '@tarojs/components' 7 | 8 | import './{{camelCase FILE_NAME}}.scss' 9 | 10 | /** 11 | * 页面 props 12 | */ 13 | type {{pascalCase FILE_NAME}}Props = { 14 | 15 | } 16 | 17 | export default function {{pascalCase FILE_NAME}} (props: 18 | {{pascalCase FILE_NAME}}Props 19 | ): JSX.Element { 20 | 21 | return ( 22 | 23 | {{FILE_DESC}} 24 | 25 | ) 26 | } 27 | 28 | export default {{pascalCase FILE_NAME}} 29 | -------------------------------------------------------------------------------- /generators/component/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: '组件模版', // 这里是对该plop的功能描述 3 | // 问题询问 4 | prompts: [ 5 | { 6 | type: 'input', // 问题类型 此处为输入 7 | name: 'IS_SUB_PACKAGE', // actions 和 hbs 模板文件中可使用该变量 8 | message: '是否在分包中创建组件(Y/N)?', // 问题 9 | default: 'N', // 问题的默认答案 10 | }, 11 | { 12 | type: 'input', // 问题类型 此处为输入 13 | name: 'DIR_NAME', // actions 和 hbs 模板文件中可使用该变量 14 | message: '请输入所在文件夹名称', // 问题 15 | default: 'DirName', // 问题的默认答案 16 | }, 17 | { 18 | type: 'input', // 问题类型 此处为输入 19 | name: 'FILE_NAME', // actions 和 hbs 模板文件中可使用该变量 20 | message: '请输入组件名称', // 问题 21 | default: 'FileName', // 问题的默认答案 22 | }, 23 | { 24 | type: 'input', // 问题类型 此处为输入 25 | name: 'FILE_DESC', // actions 和 hbs 模板文件中可使用该变量 26 | message: '请输入组件描述', // 问题 27 | default: 'FileDesc', // 问题的默认答案 28 | }, 29 | ], 30 | // 操作行为 31 | actions: function (data) { 32 | let actions = [] 33 | 34 | // 区分是否分包 35 | if (data.IS_SUB_PACKAGE.toUpperCase() === 'Y') { 36 | actions = actions.concat([ 37 | { 38 | type: 'add', // 操作类型,这里是添加文件 39 | path: 40 | '../src/{{pascalCase DIR_NAME}}/Components/{{pascalCase FILE_NAME}}.tsx', // 添加的文件的路径 41 | templateFile: './component/component.hbs', // 模板文件的路径 42 | }, 43 | { 44 | type: 'add', // 操作类型,这里是添加文件 45 | path: 46 | '../src/{{pascalCase DIR_NAME}}/Components/{{pascalCase FILE_NAME}}.scss', // 添加的文件的路径 47 | templateFile: './component/scss.hbs', // 模板文件的路径 48 | }, 49 | ]) 50 | } else { 51 | actions = actions.concat([ 52 | { 53 | type: 'add', // 操作类型,这里是添加文件 54 | path: 55 | '../src/components/{{pascalCase DIR_NAME}}/{{pascalCase FILE_NAME}}.tsx', // 添加的文件的路径 56 | templateFile: './component/component.hbs', // 模板文件的路径 57 | }, 58 | { 59 | type: 'add', // 操作类型,这里是添加文件 60 | path: 61 | '../src/components/{{pascalCase DIR_NAME}}/{{pascalCase FILE_NAME}}.scss', // 添加的文件的路径 62 | templateFile: './component/scss.hbs', // 模板文件的路径 63 | }, 64 | ]) 65 | } 66 | 67 | return actions 68 | }, 69 | } 70 | -------------------------------------------------------------------------------- /generators/component/scss.hbs: -------------------------------------------------------------------------------- 1 | .{{camelCase FILE_NAME}}-comp { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /generators/index.js: -------------------------------------------------------------------------------- 1 | // 引入各模块配置文件 2 | const componentGenerrator = require('./component/index.js') 3 | const pageGenerrator = require('./page/index.js') 4 | const storeGenerrator = require('./store/index.js') 5 | const serviceGenerrator = require('./service/index.js') 6 | 7 | module.exports = plop => { 8 | // 欢迎语 9 | plop.setWelcomeMessage('欢迎使用~ 请选择需要创建的模版:') 10 | // component 相关 11 | plop.setGenerator('component', componentGenerrator) 12 | // views 相关 13 | plop.setGenerator('page', pageGenerrator) 14 | // vuex 相关 15 | plop.setGenerator('mobx', storeGenerrator) 16 | // api 相关 17 | plop.setGenerator('service', serviceGenerrator) 18 | } 19 | -------------------------------------------------------------------------------- /generators/page/config.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * {{FILE_DESC}} 配置文件 3 | */ 4 | export default definePageConfig({ 5 | navigationBarTitleText: '{{FILE_DESC}}' 6 | }) 7 | -------------------------------------------------------------------------------- /generators/page/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: '页面模版', // 这里是对该plop的功能描述 3 | // 问题询问 4 | prompts: [ 5 | { 6 | type: 'input', // 问题类型 此处为输入 7 | name: 'IS_SUB_PACKAGE', // actions 和 hbs 模板文件中可使用该变量 8 | message: '是否在分包中创建页面(Y/N)?', // 问题 9 | default: 'N', // 问题的默认答案 10 | }, 11 | { 12 | type: 'input', // 问题类型 此处为输入 13 | name: 'DIR_NAME', // actions 和 hbs 模板文件中可使用该变量 14 | message: '请输入所在文件夹名称', // 问题 15 | default: 'DirName', // 问题的默认答案 16 | }, 17 | { 18 | type: 'input', // 问题类型 此处为输入 19 | name: 'FILE_NAME', // actions 和 hbs 模板文件中可使用该变量 20 | message: '请输入页面名称', // 问题 21 | default: 'FileName', // 问题的默认答案 22 | }, 23 | { 24 | type: 'input', // 问题类型 此处为输入 25 | name: 'FILE_DESC', // actions 和 hbs 模板文件中可使用该变量 26 | message: '请输入页面描述', // 问题 27 | default: 'FileDesc', // 问题的默认答案 28 | }, 29 | ], 30 | // 操作行为 31 | actions: function (data) { 32 | let actions = [] 33 | if (data.IS_SUB_PACKAGE.toUpperCase() === 'Y') { 34 | actions = actions.concat([ 35 | { 36 | type: 'add', // 操作类型,这里是添加文件 37 | path: '../src/{{camelCase DIR_NAME}}/{{camelCase FILE_NAME}}.tsx', // 添加的文件的路径 38 | templateFile: './page/page.hbs', // 模板文件的路径 39 | }, 40 | { 41 | type: 'add', // 操作类型,这里是添加文件 42 | path: '../src/{{camelCase DIR_NAME}}/{{camelCase FILE_NAME}}.scss', // 添加的文件的路径 43 | templateFile: './page/scss.hbs', // 模板文件的路径 44 | }, 45 | { 46 | type: 'add', // 操作类型,这里是添加文件 47 | path: '../src/{{camelCase DIR_NAME}}/{{camelCase FILE_NAME}}.config.ts', // 添加的文件的路径 48 | templateFile: './page/config.hbs', // 模板文件的路径 49 | }, 50 | ]) 51 | } else { 52 | actions = actions.concat([ 53 | { 54 | type: 'add', // 操作类型,这里是添加文件 55 | path: '../src/{{camelCase DIR_NAME}}/{{camelCase FILE_NAME}}.tsx', // 添加的文件的路径 56 | templateFile: './page/page.hbs', // 模板文件的路径 57 | }, 58 | { 59 | type: 'add', // 操作类型,这里是添加文件 60 | path: '../src/{{camelCase DIR_NAME}}/{{camelCase FILE_NAME}}.scss', // 添加的文件的路径 61 | templateFile: './page/scss.hbs', // 模板文件的路径 62 | }, 63 | { 64 | type: 'add', // 操作类型,这里是添加文件 65 | path: '../src/{{camelCase DIR_NAME}}/{{camelCase FILE_NAME}}.config.ts', // 添加的文件的路径 66 | templateFile: './page/config.hbs', // 模板文件的路径 67 | }, 68 | ]) 69 | } 70 | return actions 71 | }, 72 | } 73 | -------------------------------------------------------------------------------- /generators/page/page.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * {{FILE_DESC}} 3 | */ 4 | 5 | import React from 'react' 6 | import { View } from '@tarojs/components' 7 | import './{{camelCase FILE_NAME}}.scss' 8 | 9 | const {{pascalCase FILE_NAME}} = (): JSX.Element => { 10 | return ( 11 | 12 | {{FILE_DESC}} 13 | 14 | ) 15 | } 16 | 17 | export default {{pascalCase FILE_NAME}} 18 | -------------------------------------------------------------------------------- /generators/page/scss.hbs: -------------------------------------------------------------------------------- 1 | .{{camelCase DIR_NAME}}-{{camelCase FILE_NAME}}-page { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /generators/service/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: '服务类模版', // 这里是对该plop的功能描述 3 | // 问题询问 4 | prompts: [ 5 | { 6 | type: 'input', // 问题类型 此处为输入 7 | name: 'DIR_NAME', // actions 和 hbs 模板文件中可使用该变量 8 | message: '请输入所在文件夹名称', // 问题 9 | default: 'root', // 问题的默认答案 10 | }, 11 | { 12 | type: 'input', // 问题类型 此处为输入 13 | name: 'FILE_NAME', // actions 和 hbs 模板文件中可使用该变量 14 | message: '请输入服务类名称', // 问题 15 | default: 'default', // 问题的默认答案 16 | }, 17 | { 18 | type: 'input', // 问题类型 此处为输入 19 | name: 'FILE_DESC', // actions 和 hbs 模板文件中可使用该变量 20 | message: '请输入服务类描述', // 问题 21 | default: '默认描述', // 问题的默认答案 22 | }, 23 | ], 24 | // 操作行为 25 | actions: [ 26 | { 27 | type: 'add', // 操作类型,这里是添加文件 28 | path: 29 | '../src/services/{{kebabCase DIR_NAME}}/{{kebabCase FILE_NAME}}.service.ts', // 添加的文件的路径 30 | templateFile: './service/service.hbs', // 模板文件的路径 31 | }, 32 | ], 33 | } 34 | -------------------------------------------------------------------------------- /generators/service/service.hbs: -------------------------------------------------------------------------------- 1 | import BaseRequest from '~/utils/request'; 2 | import { HOSTS } from '~/constants/index' 3 | 4 | /** 5 | * {{FILE_DESC}} 6 | */ 7 | class {{pascalCase FILE_NAME}}Service extends BaseRequest { 8 | constructor() { 9 | super({ 10 | hostKey: HOSTS.API_BASE 11 | }); 12 | } 13 | 14 | public queryData(payload: any): Promise { 15 | return this.post({ 16 | url: '', 17 | data: payload 18 | }); 19 | } 20 | } 21 | 22 | export default new {{pascalCase FILE_NAME}}Service(); 23 | -------------------------------------------------------------------------------- /generators/store/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: 'mobx store模版', // 这里是对该plop的功能描述 3 | // 问题询问 4 | prompts: [ 5 | { 6 | type: 'input', // 问题类型 此处为输入 7 | name: 'FILE_NAME', // actions 和 hbs 模板文件中可使用该变量 8 | message: '请输入文件名称', // 问题 9 | default: 'default', // 问题的默认答案 10 | }, 11 | { 12 | type: 'input', // 问题类型 此处为输入 13 | name: 'FILE_DESC', // actions 和 hbs 模板文件中可使用该变量 14 | message: '请输入文件描述', // 问题 15 | default: 'default', // 问题的默认答案 16 | }, 17 | ], 18 | // 操作行为 19 | actions: [ 20 | { 21 | type: 'add', // 操作类型,这里是添加文件 22 | path: '../src/store/{{camelCase FILE_NAME}}.ts', // 添加的文件的路径 23 | templateFile: './store/store.hbs', // 模板文件的路径 24 | }, 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /generators/store/store.hbs: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx' 2 | 3 | /** 4 | * {{FILE_DESC}} 5 | */ 6 | const {{FILE_NAME}} = observable({ 7 | counter: 0, 8 | addCount() { 9 | this.counter++ 10 | }, 11 | }) 12 | 13 | export default {{FILE_NAME}} 14 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' 2 | declare module '*.gif' 3 | declare module '*.jpg' 4 | declare module '*.jpeg' 5 | declare module '*.svg' 6 | declare module '*.css' 7 | declare module '*.less' 8 | declare module '*.scss' 9 | declare module '*.sass' 10 | declare module '*.styl' 11 | 12 | declare namespace JSX { 13 | interface IntrinsicElements { 14 | import: React.DetailedHTMLProps< 15 | React.EmbedHTMLAttributes, 16 | HTMLEmbedElement 17 | > 18 | } 19 | } 20 | 21 | // @ts-ignore 22 | declare const process: { 23 | env: { 24 | TARO_ENV: 25 | | 'weapp' 26 | | 'swan' 27 | | 'alipay' 28 | | 'h5' 29 | | 'rn' 30 | | 'tt' 31 | | 'quickapp' 32 | | 'qq' 33 | | 'jd' 34 | /** 35 | * 构建环境 36 | */ 37 | BUILD_ENV: 'local' | 'dev' | 'test' | 'uat' | 'pro' 38 | [key: string]: any 39 | } 40 | } 41 | 42 | /** 43 | * 接口baseurl 44 | */ 45 | declare const TARO_API_BASE: string 46 | 47 | declare const wx: { 48 | [key: string]: any 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "taro3-react-template", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "基于 Taro3 + React Hooks + TypeScript + Scss 的开箱即用的多端项目模版。", 6 | "keywords": [ 7 | "taro", 8 | "taro3", 9 | "react" 10 | ], 11 | "author": { 12 | "name": "lexmin0412", 13 | "email": "zhangle_dev@ouutlook.com", 14 | "url": "https://github.com/lexmin0412" 15 | }, 16 | "templateInfo": { 17 | "name": "mobx", 18 | "typescript": true, 19 | "css": "sass" 20 | }, 21 | "engines": { 22 | "node": ">=16.14.0", 23 | "pnpm": ">=8.0.0" 24 | }, 25 | "packageManager": "pnpm@8.9.2", 26 | "scripts": { 27 | "build:weapp": "taro build --type weapp", 28 | "dev:weapp": "taro build --type weapp --watch", 29 | "build:h5": "taro build --type h5", 30 | "dev:h5": "pnpm run build:h5 --watch", 31 | "new": "plop --plopfile generators/index.js", 32 | "preview:weapp": "npx mpa", 33 | "preview:weapp-local-watch": "pnpm preview:weapp && pnpm dev:weapp" 34 | }, 35 | "husky": { 36 | "hooks": { 37 | "pre-commit": "pretty-quick --staged && lint-staged", 38 | "commit-msg": "commitlint -e $HUSKY_GIT_PARAMS" 39 | } 40 | }, 41 | "lint-staged": { 42 | "*.{cjs,mjs,js,ts,tsx}": [ 43 | "eslint", 44 | "prettier --write" 45 | ], 46 | "*.{css,less}": [ 47 | "stylelint --fix", 48 | "prettier --write" 49 | ], 50 | "*.{json,jsonc,html,yml,yaml,md}": [ 51 | "markdownlint-cli2 .md", 52 | "prettier --write" 53 | ] 54 | }, 55 | "browserslist": [ 56 | "last 3 versions", 57 | "Android >= 4.1", 58 | "ios >= 8" 59 | ], 60 | "license": "MIT", 61 | "dependencies": { 62 | "@babel/runtime": "^7.7.7", 63 | "@nutui/nutui-react-taro": "^2.5.2", 64 | "@tarojs/components": "3.6.32", 65 | "@tarojs/helper": "3.6.32", 66 | "@tarojs/plugin-framework-react": "3.6.32", 67 | "@tarojs/plugin-html": "3.6.32", 68 | "@tarojs/plugin-platform-h5": "3.6.32", 69 | "@tarojs/plugin-platform-weapp": "3.6.32", 70 | "@tarojs/react": "3.6.32", 71 | "@tarojs/runtime": "3.6.32", 72 | "@tarojs/shared": "3.6.32", 73 | "@tarojs/taro": "3.6.32", 74 | "evil-eval": "^0.0.1", 75 | "mobx": "^4.8.0", 76 | "mobx-react": "^6.1.4", 77 | "nervjs": "^1.5.7", 78 | "postcss": "^8.4.23", 79 | "react": "18.2.0", 80 | "react-dom": "18.2.0", 81 | "wtils": "^0.2.0" 82 | }, 83 | "devDependencies": { 84 | "@babel/core": "^7.8.0", 85 | "@commitlint/cli": "^11.0.0", 86 | "@commitlint/config-conventional": "^11.0.0", 87 | "@commitlint/lint": "^17.0.3", 88 | "@lexmin0412/prettier-config-base": "^0.0.10", 89 | "@pmmmwh/react-refresh-webpack-plugin": "0.5.4", 90 | "@tarojs/cli": "3.6.32", 91 | "@tarojs/webpack5-runner": "3.6.32", 92 | "@tarox/plugin-init-app": "^1.0.0-alpha.16", 93 | "@types/react": "18.0.15", 94 | "@types/webpack-env": "^1.13.6", 95 | "@typescript-eslint/parser": "^6.2.0", 96 | "@typescript-eslint/eslint-plugin": "^6.2.0", 97 | "@youtils/commitlint-plugin-standard": "^0.0.1", 98 | "babel-loader": "^8.2.5", 99 | "babel-plugin-import": "^1.13.6", 100 | "babel-preset-taro": "3.6.32", 101 | "cache-loader": "^4.1.0", 102 | "cross-env": "^7.0.2", 103 | "eslint": "^8.12.0", 104 | "eslint-config-taro": "3.6.32", 105 | "eslint-plugin-react": "^7.8.2", 106 | "eslint-plugin-import": "^2.12.0", 107 | "eslint-plugin-react-hooks": "^4.2.0", 108 | "husky": "^4.3.0", 109 | "lint-staged": "^10.4.0", 110 | "markdownlint": "^0.25.1", 111 | "markdownlint-cli2": "^0.4.0", 112 | "miniprogram-automator": "^0.11.0", 113 | "miniprogram-automator-scripts": "0.0.2", 114 | "plop": "^2.7.4", 115 | "prettier": "^3.3.2", 116 | "pretty-quick": "^3.0.2", 117 | "react-refresh": "0.11.0", 118 | "stylelint": "9.3.0", 119 | "taro-plugin-compiler-optimization": "^1.0.1", 120 | "taro-plugin-dotenv": "^1.0.1", 121 | "thread-loader": "^3.0.4", 122 | "typescript": "^5.1.0", 123 | "webpack": "^5.74.0" 124 | } 125 | } -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "miniprogramRoot": "./dist", 3 | "projectname": "taro3-react-template", 4 | "description": "taro3-react多端项目模板", 5 | "appid": "wxfc18f27dc0b3851a", 6 | "setting": { 7 | "urlCheck": true, 8 | "es6": false, 9 | "postcss": false, 10 | "minified": false 11 | }, 12 | "compileType": "miniprogram" 13 | } 14 | -------------------------------------------------------------------------------- /src/app.config.ts: -------------------------------------------------------------------------------- 1 | const tabbarConfig = require('./tabbar.config') 2 | 3 | export default defineAppConfig({ 4 | entryPagePath: 'index/index', 5 | pages: ['index/index', 'user/index'], 6 | subpackages: [ 7 | { 8 | root: 'default', 9 | pages: ['404'], 10 | }, 11 | { 12 | root: 'demo', 13 | pages: ['router/router', 'router/routerTarget', 'form/form'], 14 | }, 15 | { 16 | root: 'webview', 17 | pages: ['index'], 18 | }, 19 | ], 20 | tabBar: tabbarConfig, 21 | window: { 22 | navigationStyle: 'custom', 23 | }, 24 | // 页面切换动画 25 | animation: { 26 | duration: 196, // 动画切换时间,单位毫秒 27 | delay: 50, // 切换延迟时间,单位毫秒 28 | }, 29 | }) 30 | -------------------------------------------------------------------------------- /src/app.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexmin0412/taro3-react-template/d5955632ff2a083fee54ea9c535b75713de8674d/src/app.scss -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Provider } from 'mobx-react' 3 | 4 | import store from './shared/store' 5 | import Router from './shared/utils/route' 6 | 7 | import './app.scss' 8 | 9 | class App extends Component { 10 | // 页面404处理 11 | onPageNotFound(object: unknown): void { 12 | console.log('on page not found', object) 13 | Router.redirectTo({ 14 | url: '/default/404', 15 | }) 16 | } 17 | 18 | // this.props.children 就是要渲染的页面 19 | render(): JSX.Element { 20 | return {this.props.children} 21 | } 22 | } 23 | 24 | export default App 25 | -------------------------------------------------------------------------------- /src/custom-tab-bar/index.scss: -------------------------------------------------------------------------------- 1 | .tabbar-container { 2 | display: flex; 3 | align-items: center; 4 | height: 100px; 5 | box-shadow: 0px -2px 20px 1px #eff0f5; 6 | background-color: #fff; 7 | 8 | .tabbar-item { 9 | flex: 1; 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | justify-content: center; 14 | 15 | &-selected { 16 | .tabbar-item-text { 17 | color: #ff0036; 18 | } 19 | } 20 | 21 | .tabbar-item-text { 22 | text-align: center; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/custom-tab-bar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import Taro from '@tarojs/taro' 3 | import { View } from '@tarojs/components' 4 | import Route from '@/utils/route' 5 | import './index.scss' 6 | 7 | interface TabbarItem { 8 | pagePath: string 9 | text: string 10 | } 11 | 12 | const list: TabbarItem[] = [ 13 | { 14 | pagePath: 'index/index', 15 | text: '首页', 16 | }, 17 | { 18 | pagePath: 'user/index', 19 | text: '我的', 20 | }, 21 | ] 22 | 23 | const CustomTabBar = (): JSX.Element => { 24 | const [currentTab, setCurrentTab] = useState('') 25 | 26 | const handleSwtich = (item: any) => { 27 | Taro.switchTab({ 28 | url: `/${item.pagePath}`, 29 | }) 30 | } 31 | 32 | useEffect(() => { 33 | // 默认选中当前页面 34 | const currentRoute = Route.getCurrentRoute() 35 | if (currentRoute) { 36 | setCurrentTab(currentRoute) 37 | } 38 | 39 | // 监听变化 40 | wx.onAppRoute((res: { path: string }) => { 41 | if (res.path) { 42 | setCurrentTab(res.path) 43 | } 44 | }) 45 | }, []) 46 | 47 | return ( 48 | 49 | {list.map(item => { 50 | return ( 51 | handleSwtich(item)} 53 | key={item.pagePath} 54 | className={`tabbar-item ${ 55 | item.pagePath === currentTab ? 'tabbar-item-selected' : '' 56 | }`} 57 | > 58 | {item.text} 59 | 60 | ) 61 | })} 62 | 63 | ) 64 | } 65 | 66 | export default CustomTabBar 67 | -------------------------------------------------------------------------------- /src/default/404.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 404页面 配置文件 3 | */ 4 | 5 | export default definePageConfig({ 6 | navigationBarTitleText: '404页面', 7 | }) 8 | -------------------------------------------------------------------------------- /src/default/404.scss: -------------------------------------------------------------------------------- 1 | .common-notFound-page { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/default/404.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 404页面 3 | */ 4 | 5 | import React from 'react' 6 | import { View } from '@tarojs/components' 7 | import CustomNavigationHeader from '@/components/navigation_header' 8 | 9 | import './404.scss' 10 | 11 | const NotFound = (): JSX.Element => { 12 | return ( 13 | 14 | 15 | 404页面 16 | 17 | ) 18 | } 19 | 20 | export default NotFound 21 | -------------------------------------------------------------------------------- /src/demo/form/form.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 表单 demo 校验文件 3 | */ 4 | 5 | export default definePageConfig({ 6 | navigationBarTitleText: '表单', 7 | }) 8 | -------------------------------------------------------------------------------- /src/demo/form/form.scss: -------------------------------------------------------------------------------- 1 | .form-item { 2 | display: flex; 3 | align-items: center; 4 | padding: 20px; 5 | 6 | &-label { 7 | width: 100px; 8 | font-size: 28px; 9 | } 10 | 11 | &-input { 12 | margin-left: 12px; 13 | font-size: 28px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/demo/form/form.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { View, Button, Input } from '@tarojs/components' 3 | import CustomNavigationHeader from '@/components/navigation_header' 4 | import validator from '@/utils/validator' 5 | import './form.scss' 6 | 7 | export default function Form() { 8 | // 按照规范定义数据结构 9 | const [formValues, setFormValues] = useState({ 10 | name: '', 11 | mobile: '', 12 | }) 13 | 14 | // 按照规范定义规则 15 | const rules = { 16 | // 1. 自行实现规则 17 | name: [ 18 | { 19 | test: value => !!value, 20 | errMsg: '姓名不能为空', 21 | }, 22 | ], 23 | // 2. 使用 validator.funcs 24 | age: [ 25 | { 26 | test: validator.funcs._notEmpty, 27 | errMsg: '年龄不能为空', 28 | }, 29 | ], 30 | // 3. 使用 validator.rules 31 | mobile: [ 32 | { 33 | test: validator.funcs._notEmpty, 34 | errMsg: '手机号不能为空', 35 | }, 36 | validator.rules._isMobile, 37 | ], 38 | } 39 | 40 | // 按照规范存储数据 41 | const handleChange = (fieldName: string, event) => { 42 | console.log('handleChange', fieldName, event) 43 | setFormValues({ 44 | ...formValues, 45 | [fieldName]: event.target.value, 46 | }) 47 | } 48 | 49 | // 按照规范校验数据 50 | const handleSubmit = () => { 51 | console.log('formValues', formValues) 52 | // 使用 validator.validate 方法校验 53 | const validateRes = validator.validate(rules, true, formValues) 54 | console.log('validate', validateRes) 55 | } 56 | 57 | return ( 58 | 59 | 60 | 61 | 62 | 姓名 63 | 64 | handleChange('name', event)} 68 | /> 69 | 70 | 71 | 72 | 年龄 73 | 74 | handleChange('age', event)} 78 | /> 79 | 80 | 81 | 82 | 手机号 83 | 84 | handleChange('mobile', event)} 88 | /> 89 | 90 | 91 | 92 | 93 | 94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /src/demo/router/router.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 路由跳转 配置文件 3 | */ 4 | 5 | export default definePageConfig({ 6 | navigationBarTitleText: '路由跳转', 7 | }) 8 | -------------------------------------------------------------------------------- /src/demo/router/router.scss: -------------------------------------------------------------------------------- 1 | .demo-router-page { 2 | width: 100%; 3 | padding: 0 20px; 4 | box-sizing: border-box; 5 | 6 | .nut-button { 7 | margin-top: 10px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/demo/router/router.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 路由跳转 3 | */ 4 | 5 | import React from 'react' 6 | import { View } from '@tarojs/components' 7 | import { Button } from '@nutui/nutui-react-taro' 8 | import CustomNavigationHeader from '@/components/navigation_header' 9 | 10 | import Router from '@/utils/route' 11 | 12 | import './router.scss' 13 | 14 | const RouterDemo = (): JSX.Element => { 15 | const handleRouterTest = ( 16 | navType: 'navigateTo' | 'redirectTo' | 'relaunch' | 'navigateBack', 17 | validateType?: 'noUrl' | 'invalidUrl' 18 | ) => { 19 | switch (validateType) { 20 | case 'noUrl': 21 | Router.navigateTo({ 22 | url: '', 23 | }) 24 | break 25 | case 'invalidUrl': 26 | Router.navigateTo({ 27 | url: 'demo/router/routerTarget', 28 | }) 29 | break 30 | default: 31 | break 32 | } 33 | switch (navType) { 34 | case 'redirectTo': 35 | Router.redirectTo({ 36 | url: '/demo/router/routerTarget', 37 | query: { 38 | param1: 'value1', 39 | param2: 'value2', 40 | }, 41 | }) 42 | break 43 | case 'relaunch': 44 | Router.relaunch({ 45 | url: '/demo/router/routerTarget', 46 | query: { 47 | param1: 'value1', 48 | param2: 'value2', 49 | }, 50 | }) 51 | break 52 | case 'navigateBack': 53 | Router.navigateBack() 54 | break 55 | default: 56 | Router.navigateTo({ 57 | url: '/demo/router/routerTarget', 58 | query: { 59 | param1: 'value1', 60 | param2: 'value2', 61 | }, 62 | }) 63 | break 64 | } 65 | } 66 | 67 | return ( 68 | 69 | 70 | 78 | 86 | 94 | 102 | 103 | ) 104 | } 105 | 106 | export default RouterDemo 107 | -------------------------------------------------------------------------------- /src/demo/router/routerTarget.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 路由目标页面 配置文件 3 | */ 4 | 5 | export default definePageConfig({ 6 | navigationBarTitleText: '路由目标页面', 7 | }) 8 | -------------------------------------------------------------------------------- /src/demo/router/routerTarget.scss: -------------------------------------------------------------------------------- 1 | .demoRouter-routerTarget-page { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/demo/router/routerTarget.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 路由目标页面 3 | */ 4 | 5 | import React from 'react' 6 | import { View } from '@tarojs/components' 7 | import CustomNavigationHeader from '@/components/navigation_header' 8 | 9 | import './routerTarget.scss' 10 | 11 | const RouterTarget = (): JSX.Element => { 12 | return ( 13 | 14 | 15 | 路由目标页面 16 | 17 | ) 18 | } 19 | 20 | export default RouterTarget 21 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/index/index.config.ts: -------------------------------------------------------------------------------- 1 | export default definePageConfig({ 2 | navigationBarTitleText: '首页', 3 | }) 4 | -------------------------------------------------------------------------------- /src/index/index.scss: -------------------------------------------------------------------------------- 1 | @import './../shared/styles/var.scss'; 2 | 3 | .test { 4 | color: $baseFontSize; 5 | font-size: $baseFontColor; 6 | box-sizing: border-box; 7 | } 8 | 9 | .index { 10 | padding: 0 20px; 11 | 12 | .nut-button { 13 | margin-top: 10px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/index/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import PageContainer from '@/components/page_container' 3 | import { Button } from '@nutui/nutui-react-taro' 4 | import Router from '@/utils/route' 5 | 6 | import './index.scss' 7 | import toast from '@/utils/toast' 8 | 9 | const Index = (): JSX.Element => { 10 | useEffect(() => { 11 | try { 12 | console.log('已配置环境变量✅') 13 | console.log('process.env', process.env.TARO_ENV) 14 | console.log('TARO_API_BASE', process.env.TARO_API_BASE) 15 | } catch (error) { 16 | const msg = '读取环境变量异常,请先阅读 README.md, 配置环境变量后重新启动' 17 | console.error(msg) 18 | toast.info(msg) 19 | } 20 | }, []) 21 | 22 | /** 23 | * 跳转demo页面 24 | */ 25 | const jumpToDemo = (demoType: 'router'|'form') => { 26 | let url = '' 27 | switch (demoType) { 28 | case 'router': 29 | url = '/demo/router/router'; 30 | break 31 | case 'form': 32 | url = '/demo/form/form'; 33 | break 34 | default: 35 | toast.info('不存在的 Demo 页面,请检查') 36 | return 37 | } 38 | Router.navigateTo({ url }) 39 | } 40 | 41 | return ( 42 | 46 | 54 | 62 | 63 | ) 64 | } 65 | 66 | export default Index 67 | -------------------------------------------------------------------------------- /src/shared/assets/icons/back.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/shared/assets/icons/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexmin0412/taro3-react-template/d5955632ff2a083fee54ea9c535b75713de8674d/src/shared/assets/icons/home.png -------------------------------------------------------------------------------- /src/shared/assets/icons/home.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/shared/assets/icons/home_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexmin0412/taro3-react-template/d5955632ff2a083fee54ea9c535b75713de8674d/src/shared/assets/icons/home_selected.png -------------------------------------------------------------------------------- /src/shared/assets/icons/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexmin0412/taro3-react-template/d5955632ff2a083fee54ea9c535b75713de8674d/src/shared/assets/icons/user.png -------------------------------------------------------------------------------- /src/shared/assets/icons/user_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexmin0412/taro3-react-template/d5955632ff2a083fee54ea9c535b75713de8674d/src/shared/assets/icons/user_selected.png -------------------------------------------------------------------------------- /src/shared/components/navigation_header/index.scss: -------------------------------------------------------------------------------- 1 | @import './../../styles/mixin.scss'; 2 | 3 | .nav-header { 4 | position: sticky; 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | box-sizing: border-box; 9 | height: 88px; 10 | line-height: 88px; 11 | 12 | &-content { 13 | display: flex; 14 | align-items: center; 15 | 16 | &-left { 17 | &-icon-group { 18 | width: 160px; 19 | display: flex; 20 | align-items: center; 21 | border-radius: 16px; 22 | border: 1px solid #fff; 23 | background-color: rgba($color: #333, $alpha: 0.4); 24 | } 25 | 26 | &-icon { 27 | width: 40px; 28 | height: 100%; 29 | padding: 0 20px; 30 | display: flex; 31 | align-items: center; 32 | 33 | &-divider { 34 | height: 36px; 35 | width: 2px; 36 | background-color: #fff; 37 | } 38 | } 39 | } 40 | 41 | &-center { 42 | flex: 1; 43 | text-align: center; 44 | @include textOrient(1); 45 | font-size: 32px; 46 | } 47 | 48 | &-right { 49 | width: 20%; 50 | height: 100%; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/shared/components/navigation_header/index.tsx: -------------------------------------------------------------------------------- 1 | import Taro from '@tarojs/taro' 2 | import React from 'react' 3 | import { View, Image } from '@tarojs/components' 4 | import { useNavigationBarInfo, usePageInfo } from '@/utils/hooks' 5 | import icon_back from '@/assets/icons/back.svg' 6 | import icon_home from '@/assets/icons/home.svg' 7 | import Router from '@/utils/route' 8 | import './index.scss' 9 | 10 | type NavigationHeaderProps = { 11 | title?: string 12 | } 13 | 14 | export default function NavigationHeader( 15 | props: NavigationHeaderProps 16 | ): JSX.Element { 17 | const { title } = props 18 | 19 | const { 20 | statusBarHeight, 21 | navigationBarHeight, 22 | navigationContentHeight, 23 | menuButtonHeight, 24 | navigationPaddding, 25 | menuButtonWidth, 26 | } = useNavigationBarInfo() 27 | 28 | const { stackLength, isTabbar } = usePageInfo() 29 | console.log('pageInfo', stackLength, isTabbar) 30 | 31 | const handleBack = () => { 32 | Router.navigateBack() 33 | } 34 | 35 | const handleBackToHome = () => { 36 | Router.backToHome() 37 | } 38 | 39 | const iconBoxStyle = 40 | process.env.TARO_ENV === 'weapp' 41 | ? { 42 | width: menuButtonWidth, 43 | } 44 | : { 45 | width: Taro.pxTransform(200), 46 | } 47 | 48 | return ( 49 | 56 | 62 | 68 | 69 | {!isTabbar ? ( 70 | <> 71 | 82 | 87 | 88 | 93 | 94 | 95 | ) : null} 96 | 97 | {title || '页面标题'} 98 | 99 | 100 | 101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /src/shared/components/page_container/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { View } from '@tarojs/components' 3 | import NavHeader from '../navigation_header' 4 | 5 | type PageContainerProps = { 6 | title: string 7 | containerClass: string 8 | children: React.ReactNode 9 | } 10 | 11 | export const PageContainer = (props: PageContainerProps): JSX.Element => { 12 | const { title, containerClass, children } = props 13 | return ( 14 | 15 | 16 | {children} 17 | 18 | ) 19 | } 20 | 21 | export default PageContainer 22 | -------------------------------------------------------------------------------- /src/shared/components/rich_text_renderer/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexmin0412/taro3-react-template/d5955632ff2a083fee54ea9c535b75713de8674d/src/shared/components/rich_text_renderer/index.scss -------------------------------------------------------------------------------- /src/shared/components/rich_text_renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import Taro from '@tarojs/taro' 3 | import { View } from '@tarojs/components' 4 | 5 | interface RichTextRendererProps { 6 | /** 7 | * 容器id 全局唯一 8 | */ 9 | id: string 10 | /** 11 | * 需要渲染的字符串 12 | */ 13 | data: string 14 | /** 15 | * 渲染类型,支持 markdown/html 默认html 16 | */ 17 | datatype?: 'markdown' | 'html' 18 | /** 19 | * 图片点击事件 20 | */ 21 | onImageClick?: (src: string) => void 22 | /** 23 | * 链接点击事件 24 | */ 25 | onLinkClick?: (src: string) => void 26 | } 27 | 28 | const RichTextRenderer: React.FC = props => { 29 | const { id, data, onImageClick, onLinkClick } = props 30 | 31 | /** 32 | * 给特殊元素绑定事件 33 | * @param arr 34 | */ 35 | const addEventListeners = (arr: NodeListOf) => { 36 | arr.forEach(item => { 37 | switch (item.h5tagName) { 38 | case 'img': 39 | item.addEventListener('tap', () => { 40 | if (onImageClick) { 41 | onImageClick(item.props.src) 42 | } else { 43 | Taro.previewImage({ 44 | urls: [item.props.src], 45 | }) 46 | } 47 | }) 48 | break 49 | case 'a': 50 | item.addEventListener('tap', () => { 51 | if (onLinkClick) { 52 | onLinkClick(item.props.href) 53 | } 54 | }) 55 | default: 56 | break 57 | } 58 | 59 | if (item.childNodes) { 60 | addEventListeners(item.childNodes) 61 | } 62 | }) 63 | } 64 | 65 | useEffect(() => { 66 | const container = document.querySelector(`#${id}`) 67 | if (container && container.childNodes) { 68 | addEventListeners(container.childNodes) 69 | } 70 | }, []) 71 | 72 | return ( 73 | 79 | ) 80 | } 81 | 82 | export default RichTextRenderer 83 | -------------------------------------------------------------------------------- /src/shared/constants/code.ts: -------------------------------------------------------------------------------- 1 | // 接口返回code 2 | 3 | // 成功code 4 | export const SUCC_LIST = ['0', '00000', '10000'] 5 | 6 | // 登录失效code 7 | export const LOGIN_FAILURE_LIST = ['99999', '40000', '40001'] 8 | -------------------------------------------------------------------------------- /src/shared/constants/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 导出常量 3 | */ 4 | const Constants = { 5 | /** 6 | * token字段 7 | */ 8 | MASK_TOKEN: 'maskToken', 9 | /** 10 | * 最后一次登录失效的时间戳 11 | */ 12 | LOGIN_FAILURE_TIMESTAMP: 'loginFailureTimeStamp', 13 | /** 14 | * 拦截器自定义头部key 15 | */ 16 | INTERCEPTOR_HEADER: 'interceptor-custom-header', 17 | } 18 | 19 | /** 20 | * 网络链接 21 | */ 22 | export const HOSTS = { 23 | /** 24 | * 接口请求base 25 | */ 26 | TARO_API_BASE: 'TARO_API_BASE', 27 | } 28 | 29 | export default Constants 30 | -------------------------------------------------------------------------------- /src/shared/interceptors/data.interceptor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * data拦截器 处理数据格式 接口错误等 3 | */ 4 | 5 | import Taro from '@tarojs/taro' 6 | import { SUCC_LIST, LOGIN_FAILURE_LIST } from '@/constants/code' 7 | import Toast from '@/utils/toast' 8 | import Page from '@/utils/page' 9 | import Constants from '@/constants/index' 10 | 11 | export default function (chain) { 12 | console.log('enter data interceptor', chain) 13 | const requestParams = chain.requestParams 14 | const { header } = requestParams 15 | const { showToast, resType } = header[Constants.INTERCEPTOR_HEADER] 16 | return chain 17 | .proceed(requestParams) 18 | .then(res => { 19 | console.log('data拦截器接收到的数据', res) 20 | 21 | // 先判断状态码 22 | if (res.statusCode !== 200) { 23 | // 错误处理 24 | console.error(`接口异常: ${res.data.path}`, res.statusCode) 25 | if (showToast) { 26 | Toast.info('很抱歉,数据临时丢失,请耐心等待修复') 27 | } 28 | return Promise.resolve('很抱歉,数据临时丢失,请耐心等待修复') 29 | } 30 | 31 | const resultData = { ...res.data } 32 | 33 | // 状态码为200时的错误处理 34 | // 这里主要是兼容多后台返回结果格式不规范以及后台框架设计存在问题的情况 35 | // 1. 返回状态码200 但返回结果是空字符串 在浏览器调试工具中查看不到任何信息 36 | if (!resultData) { 37 | throw `返回数据为空:${resultData}` 38 | } 39 | 40 | console.log('into data handle', resultData) 41 | 42 | // 返回格式统一为 code data msg 43 | // 腾讯地图webservice接口返回格式统一 44 | if (resType === 1) { 45 | resultData.code = resultData.status 46 | resultData.msg = resultData.message 47 | resultData.data = resultData.result 48 | } 49 | 50 | // 2. 统一返回格式 51 | // code 返回编码 强转字符串 52 | // msg 错误信息字符串 一般用于前端错误展示 53 | // data 返回数据 54 | resultData.code = resultData.hasOwnProperty('code') 55 | ? resultData.code.toString() 56 | : resultData.code 57 | 58 | console.error('resultData', resultData) 59 | 60 | // 3. 接口返回错误code时前端错误抛出 61 | // 4. 登录失效前端逻辑处理 62 | if (LOGIN_FAILURE_LIST.includes(resultData.code)) { 63 | console.error('into login failure') 64 | Taro.setStorageSync( 65 | Constants.LOGIN_FAILURE_TIMESTAMP, 66 | new Date().getTime() 67 | ) 68 | Taro.removeStorageSync(Constants.MASK_TOKEN) 69 | Taro.showToast({ 70 | title: resultData.msg, 71 | icon: 'none', 72 | duration: 800, 73 | }) 74 | const curPages = Taro.getCurrentPages() 75 | console.error('taro.curPages', curPages) 76 | if (Page.getCurRoute() === Page.getRoutes().home) { 77 | setTimeout(() => { 78 | Taro.navigateTo({ 79 | url: `/${Page.getRoutes().auth}?from=home`, 80 | }) 81 | }, 800) 82 | } else { 83 | setTimeout(() => { 84 | Taro.navigateTo({ 85 | url: `/${Page.getRoutes().auth}`, 86 | }) 87 | }, 800) 88 | } 89 | } else if (!SUCC_LIST.includes(resultData.code) && showToast) { 90 | console.log('非登录失效的失败code', resultData) 91 | if (resultData.code === '50000') { 92 | Toast.info('系统开小差了') 93 | } else { 94 | Toast.info(resultData.msg) 95 | } 96 | } 97 | console.error('返回之前的resultData', resultData) 98 | return Promise.resolve(resultData) 99 | }) 100 | .catch(err => { 101 | Taro.hideLoading() 102 | Toast.info('网络开小差了') 103 | return Promise.reject(err) 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /src/shared/interceptors/del.interceptor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 删除自定义请求头拦截器 3 | */ 4 | 5 | import Constants from '@/constants/index' 6 | 7 | export default function (chain) { 8 | console.log('enter del interceptor', chain) 9 | const requestParams = chain.requestParams 10 | 11 | const { header } = requestParams 12 | const { crossHeaderInterceptor } = header[Constants.INTERCEPTOR_HEADER] 13 | 14 | // 删除自定义请求头参数 15 | if (!crossHeaderInterceptor) { 16 | delete header[Constants.INTERCEPTOR_HEADER] 17 | requestParams.header = header 18 | } 19 | 20 | return chain.proceed(requestParams) 21 | } 22 | -------------------------------------------------------------------------------- /src/shared/interceptors/header.interceptor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 头部拦截器 处理请求头的配置 3 | */ 4 | 5 | export default function (chain) { 6 | const requestParams = chain.requestParams 7 | 8 | const { header } = requestParams 9 | 10 | requestParams.header = header 11 | 12 | return chain.proceed(requestParams) 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/interceptors/param.interceptor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 参数拦截器 必传参数验证等 3 | */ 4 | 5 | export default function (chain) { 6 | const requestParams = chain.requestParams 7 | const { data } = requestParams 8 | 9 | // 这里做接口入参相关的处理 10 | requestParams.data = data 11 | 12 | return chain.proceed(requestParams) 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/interceptors/url.interceptor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * host拦截器 处理url拼接等 3 | */ 4 | 5 | import Constants from '@/constants/index' 6 | 7 | export default function (chain) { 8 | const requestParams = chain.requestParams 9 | const { header, url } = requestParams 10 | 11 | // 如果传入url自带域名则不做处理 否则加上对应的域名 12 | if (!(url.startsWith('https://') || url.startsWith('http://'))) { 13 | requestParams.url = `${header[Constants.INTERCEPTOR_HEADER].hostUrl}${url}` 14 | } 15 | return chain.proceed(requestParams) 16 | } 17 | -------------------------------------------------------------------------------- /src/shared/services/root/demo.service.ts: -------------------------------------------------------------------------------- 1 | import BaseRequest from '@/utils/request' 2 | import { HOSTS } from '@/constants/index' 3 | 4 | /** 5 | * 服务类示例 6 | */ 7 | class DemoService extends BaseRequest { 8 | constructor() { 9 | super({ 10 | hostKey: HOSTS.TARO_API_BASE, 11 | }) 12 | } 13 | 14 | /** 15 | * 一个获取某项数据的 get 请求 16 | */ 17 | getSomething(payload: { 18 | paramName: string // 参数 19 | }) { 20 | return this.post({ 21 | url: '/demo/getSomething', 22 | data: payload, 23 | }) 24 | } 25 | } 26 | 27 | export default new DemoService() 28 | -------------------------------------------------------------------------------- /src/shared/store/app.ts: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx' 2 | 3 | const counterStore = observable({ 4 | counter: 0, 5 | counterStore() { 6 | this.counter++ 7 | }, 8 | increment() { 9 | this.counter++ 10 | }, 11 | decrement() { 12 | this.counter-- 13 | }, 14 | incrementAsync() { 15 | setTimeout(() => { 16 | this.counter++ 17 | }, 1000) 18 | }, 19 | }) 20 | 21 | export default counterStore 22 | -------------------------------------------------------------------------------- /src/shared/store/index.ts: -------------------------------------------------------------------------------- 1 | import app from './app' 2 | 3 | export default { 4 | app, 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | // 设置字体大小及颜色 2 | @mixin setFont($fontSize, $color) { 3 | font-size: $fontSize; 4 | color: $color; 5 | } 6 | 7 | // 多行截取 8 | @mixin textOrient($line) { 9 | // 单行 10 | @if $line==1 { 11 | overflow: hidden; 12 | text-overflow: ellipsis; 13 | white-space: nowrap; 14 | } 15 | 16 | // 多行 17 | @else { 18 | display: -webkit-box; 19 | overflow: hidden; 20 | text-overflow: ellipsis; 21 | word-break: break-all; 22 | // 需要加上这一句autoprefixer的忽略规则 否则这一行样式加不上 导致无法展示省略号 23 | 24 | /*! autoprefixer: ignore next */ 25 | -webkit-box-orient: vertical; 26 | -webkit-line-clamp: $line; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/shared/styles/var.scss: -------------------------------------------------------------------------------- 1 | // 定位元素层级 2 | $absoluteIndex: 9; // 页面中普通的元素定位zIndex 3 | $fixedZIndex: 99; // 固定定位的元素zIndex 如底部的提交按钮 头部导航等 4 | $tabbarZIndex: 998; // tabbar层级 需高于一般的定位元素 但低于全屏弹窗的mask 5 | $modalMaskZIndex: 999; // 弹窗元素遮罩zIndex 6 | $modalContentZIndex: 1000; // 弹窗元素内容 一般需要最高的层级 7 | 8 | // 字体大小 9 | $baseFontSize: 28px; 10 | 11 | // 颜色 12 | $baseFontColor: #222; 13 | -------------------------------------------------------------------------------- /src/shared/utils/hooks.ts: -------------------------------------------------------------------------------- 1 | import Taro from '@tarojs/taro' 2 | const tabbarConfig = require('./../../tabbar.config') 3 | 4 | interface UseNavigationBarInfoPresets { 5 | menuButtonInfo: Taro.getMenuButtonBoundingClientRect.Rect 6 | systemInfo: Taro.getSystemInfoSync.Result 7 | } 8 | 9 | interface INavigationBarInfo { 10 | navigationBarHeight: number 11 | navigationContentHeight: number 12 | menuButtonHeight: number 13 | navigationPaddding: number 14 | menuButtonWidth: number 15 | statusBarHeight: number 16 | } 17 | 18 | /** 19 | * 获取导航栏相关信息 20 | */ 21 | export const useNavigationBarInfo = ( 22 | presets: UseNavigationBarInfoPresets = {} as any 23 | ): INavigationBarInfo => { 24 | const systemMenuButtonInfo = 25 | process.env.TARO_ENV === 'weapp' 26 | ? Taro.getMenuButtonBoundingClientRect() 27 | : {} 28 | const menuButtonInfo = presets.menuButtonInfo || systemMenuButtonInfo 29 | const systemInfo = presets.systemInfo || Taro.getSystemInfoSync() 30 | const { statusBarHeight } = systemInfo 31 | let navigationContentHeight = 40 32 | navigationContentHeight = 33 | (menuButtonInfo.top - systemInfo.statusBarHeight) * 2 + 34 | menuButtonInfo.height 35 | return { 36 | navigationBarHeight: statusBarHeight + navigationContentHeight, 37 | navigationContentHeight, 38 | menuButtonHeight: menuButtonInfo.height, 39 | navigationPaddding: systemInfo.windowWidth - menuButtonInfo.right, 40 | statusBarHeight: systemInfo.statusBarHeight, 41 | menuButtonWidth: menuButtonInfo.width, 42 | } 43 | } 44 | 45 | /** 46 | * 判断是否tabbar页面 47 | * @param route 48 | * @returns 49 | */ 50 | const isTabbar = (route: string) => { 51 | let validRoute = route.includes('?') 52 | ? route.slice(0, route.indexOf('?')) 53 | : route 54 | 55 | if (process.env.TARO_ENV === 'h5') { 56 | validRoute = validRoute.slice(1) 57 | } 58 | 59 | const { list } = tabbarConfig 60 | 61 | return list.some(item => item.pagePath === validRoute) 62 | } 63 | 64 | /** 65 | * 获取当前页面信息 66 | */ 67 | export const usePageInfo = () => { 68 | const pages = Taro.getCurrentPages() 69 | const currentPage = pages[pages.length - 1] 70 | return { 71 | navigationBarTitleText: currentPage.config.navigationBarTitleText, 72 | route: currentPage.route, 73 | isTabbar: isTabbar(currentPage.route), 74 | stackLength: pages.length, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/shared/utils/index.ts: -------------------------------------------------------------------------------- 1 | export default 'sd' 2 | -------------------------------------------------------------------------------- /src/shared/utils/page.ts: -------------------------------------------------------------------------------- 1 | import Taro from '@tarojs/taro' 2 | 3 | class Pages { 4 | constructor() {} 5 | 6 | /** 7 | * 页面枚举 8 | */ 9 | getRoutes() { 10 | return { 11 | /** 12 | * 首页 13 | */ 14 | home: 'index/index', 15 | } 16 | } 17 | 18 | // 获取当前路由 19 | getCurRoute() { 20 | if (process.env.TARO_ENV === 'weapp') { 21 | const curPages = Taro.getCurrentPages() 22 | return curPages[curPages.length - 1].route 23 | } else { 24 | const location = window.location 25 | return location.pathname.slice(1) 26 | } 27 | } 28 | 29 | backToHome() { 30 | Taro.switchTab({ 31 | url: '/index/index', 32 | }) 33 | } 34 | } 35 | 36 | export default new Pages() 37 | -------------------------------------------------------------------------------- /src/shared/utils/request.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 请求基类 3 | */ 4 | 5 | import Taro from '@tarojs/taro' 6 | import Constants from '@/constants/index' 7 | import { SUCC_LIST } from '@/constants/code' 8 | import urlInterceptor from '@/interceptors/url.interceptor' 9 | import headerInterceptor from '@/interceptors/header.interceptor' 10 | import paramInterceptor from '@/interceptors/param.interceptor' 11 | import dataInterceptor from '@/interceptors/data.interceptor' 12 | import delInterceptor from '@/interceptors/del.interceptor' 13 | import toast from '@/utils/toast' 14 | 15 | // 添加拦截器 16 | const getInterceptors = () => { 17 | return [ 18 | urlInterceptor, 19 | headerInterceptor, 20 | paramInterceptor, 21 | dataInterceptor, 22 | delInterceptor, 23 | Taro.interceptors.logInterceptor, 24 | Taro.interceptors.timeoutInterceptor, 25 | ] 26 | } 27 | getInterceptors().forEach(interceptorItem => 28 | Taro.addInterceptor(interceptorItem) 29 | ) 30 | 31 | interface IOptions { 32 | hostKey: string 33 | [key: string]: any 34 | } 35 | 36 | interface IRequestConfig { 37 | url: string 38 | data?: any 39 | method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'UPLOAD' 40 | [key: string]: any 41 | } 42 | 43 | interface Response { 44 | code: string 45 | data: any 46 | } 47 | 48 | class BaseRequest { 49 | public options: IOptions 50 | 51 | constructor(options) { 52 | console.log('options', options) 53 | this.options = options 54 | } 55 | 56 | public async request({ 57 | url, 58 | data, 59 | method, 60 | header = { 61 | 'Content-Type': 'application/json', 62 | }, 63 | dataType = 'json', 64 | responseType = 'text', 65 | showToast = true, 66 | jsonp = false, 67 | crossHeaderInterceptor = false, 68 | resType = 0, 69 | }: IRequestConfig): Promise { 70 | // 添加自定义请求头,用于host和header处理 71 | const hostKey = this.options ? this.options.hostKey : '' 72 | if (!hostKey) { 73 | throw '请指定service key' 74 | } 75 | const hostUrl = TARO_API_BASE // 通过hostKey去配置文件中寻找对应的host 76 | header[Constants.INTERCEPTOR_HEADER] = { 77 | hostKey, 78 | hostUrl, 79 | showToast, 80 | resType, 81 | crossHeaderInterceptor, 82 | } 83 | 84 | // UPLOAD方法特殊处理 85 | if (method === 'UPLOAD') { 86 | return new Promise((resolve, reject) => { 87 | return Taro.uploadFile({ 88 | url: `${hostUrl}/${url}`, 89 | filePath: data, 90 | name: 'file', 91 | success(res) { 92 | const resultData = res.data 93 | 94 | console.log('uploadFile success', resultData) 95 | console.log('uploadFile success', JSON.parse(resultData)) 96 | const result = JSON.parse(resultData) 97 | if (SUCC_LIST.includes(result.code)) { 98 | resolve(result) 99 | } else { 100 | toast.info(result.msg) 101 | reject(result) 102 | } 103 | }, 104 | fail(err) { 105 | console.log('uploadFile err', err) 106 | reject(err) 107 | }, 108 | }) 109 | }) 110 | } else { 111 | return Taro.request({ 112 | url, 113 | data, 114 | method, 115 | header, 116 | dataType, 117 | responseType, 118 | jsonp, 119 | }) 120 | } 121 | } 122 | 123 | public get(payload: { 124 | url: string 125 | data: any 126 | showToast?: boolean 127 | header?: any 128 | resType?: 1 | 0 129 | crossHeaderInterceptor?: boolean 130 | }): Promise { 131 | return this.request({ 132 | method: 'GET', 133 | ...payload, 134 | }) 135 | } 136 | 137 | public post(payload: { 138 | url: string 139 | data: any 140 | showToast?: boolean 141 | header?: any 142 | resType?: 1 | 0 143 | crossHeaderInterceptor?: boolean 144 | }): Promise { 145 | return this.request({ 146 | method: 'POST', 147 | ...payload, 148 | }) 149 | } 150 | 151 | public put(payload: { 152 | url: string 153 | data: any 154 | showToast?: boolean 155 | header?: any 156 | resType?: 1 | 0 157 | crossHeaderInterceptor?: boolean 158 | }): Promise { 159 | return this.request({ 160 | method: 'PUT', 161 | ...payload, 162 | }) 163 | } 164 | 165 | public delete(payload: { 166 | url: string 167 | data: any 168 | showToast?: boolean 169 | header?: any 170 | resType?: 1 | 0 171 | crossHeaderInterceptor?: boolean 172 | }): Promise { 173 | return this.request({ 174 | method: 'DELETE', 175 | ...payload, 176 | }) 177 | } 178 | 179 | public jsonp(payload: { 180 | url: string 181 | data: any 182 | showToast?: boolean 183 | header?: any 184 | resType?: 1 | 0 185 | crossHeaderInterceptor?: boolean 186 | }): Promise { 187 | return this.request({ 188 | method: 'GET', 189 | jsonp: true, 190 | ...payload, 191 | }) 192 | } 193 | 194 | /** 195 | * 上传文件 196 | */ 197 | public upload(payload: { 198 | url: string 199 | data: any 200 | showToast?: boolean 201 | header?: any 202 | resType?: 1 | 0 203 | crossHeaderInterceptor?: boolean 204 | }): Promise { 205 | return this.request({ 206 | ...payload, 207 | method: 'UPLOAD', 208 | header: { 209 | 'Content-Type': 'multipart/form-data', 210 | }, 211 | }) 212 | } 213 | } 214 | 215 | export default BaseRequest 216 | -------------------------------------------------------------------------------- /src/shared/utils/route.ts: -------------------------------------------------------------------------------- 1 | import Taro, { getCurrentPages } from '@tarojs/taro' 2 | const wtils = require('wtils') 3 | const tabbarConfig = require('./../../tabbar.config') 4 | 5 | /** 6 | * 路由配置对象 7 | */ 8 | interface IRoute { 9 | /** 10 | * 页面路径 11 | */ 12 | url: string 13 | /** 14 | * query参数 15 | */ 16 | query?: { 17 | [key: string]: any 18 | } 19 | } 20 | 21 | class Route { 22 | /** 23 | * 返回上一页面 24 | */ 25 | navigateBack() { 26 | const curPages = getCurrentPages() 27 | if (curPages.length <= 1) { 28 | console.error('已无上层页面,无法返回') 29 | return 30 | } 31 | Taro.navigateBack() 32 | } 33 | 34 | /** 35 | * 页面push 36 | */ 37 | navigateTo(params: IRoute) { 38 | this.jump({ 39 | type: 'navigateTo', 40 | config: params, 41 | }) 42 | } 43 | 44 | /** 45 | * 重定向 46 | */ 47 | redirectTo(params: IRoute) { 48 | this.jump({ 49 | type: 'redirectTo', 50 | config: params, 51 | }) 52 | } 53 | 54 | /** 55 | * 重定向 56 | */ 57 | relaunch(params: IRoute) { 58 | this.jump({ 59 | type: 'relaunch', 60 | config: params, 61 | }) 62 | } 63 | 64 | /** 65 | * 切换tabbar 66 | * @param params 67 | */ 68 | switchTab(params: IRoute) { 69 | this.jump({ 70 | type: 'switchTab', 71 | config: params, 72 | }) 73 | } 74 | 75 | /** 76 | * 跳转页面 77 | */ 78 | jump(params: { 79 | type: 'navigateTo' | 'redirectTo' | 'relaunch' | 'switchTab' 80 | config: IRoute 81 | }) { 82 | const { 83 | type, 84 | config: { url, query }, 85 | } = params 86 | 87 | // url校验 88 | if (!url) { 89 | throw new Error('jump方法参数校验失败:缺少url') 90 | } 91 | if (!url.startsWith('/')) { 92 | throw new Error('jump方法参数校验失败:url必须以“/”开头') 93 | } 94 | 95 | let suffix = '' 96 | if (query && Object.keys(query).length > 0) { 97 | suffix = wtils.transParams(JSON.stringify(query)) 98 | } 99 | const finalUrl = `${url}${suffix}` 100 | switch (type) { 101 | case 'redirectTo': 102 | Taro.redirectTo({ 103 | url: finalUrl, 104 | }) 105 | break 106 | case 'relaunch': 107 | Taro.reLaunch({ 108 | url: finalUrl, 109 | }) 110 | break 111 | case 'switchTab': 112 | Taro.switchTab({ 113 | url: finalUrl, 114 | }) 115 | break 116 | default: 117 | Taro.navigateTo({ 118 | url: finalUrl, 119 | }) 120 | break 121 | } 122 | } 123 | 124 | /** 125 | * 获取当前路由 126 | */ 127 | getCurrentRoute() { 128 | const currentPages = getCurrentPages() 129 | console.log('当前页面', currentPages) 130 | 131 | return currentPages.length 132 | ? currentPages[currentPages.length - 1].route 133 | : '' 134 | } 135 | 136 | /** 137 | * 返回首页 138 | */ 139 | backToHome() { 140 | const tabBar = tabbarConfig 141 | if (tabBar.list && tabBar.list.length) { 142 | this.switchTab({ 143 | url: `/${tabBar.list[0].pagePath}`, 144 | }) 145 | } 146 | } 147 | } 148 | 149 | export default new Route() 150 | -------------------------------------------------------------------------------- /src/shared/utils/toast.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * taro toast封装简化 3 | */ 4 | 5 | import Taro from '@tarojs/taro' 6 | 7 | class Toast { 8 | /** 9 | * 展示加载动画 10 | * @param title 加载文字 11 | * @param mask 是否展示遮罩 12 | */ 13 | loading(title, mask = true) { 14 | Taro.showLoading({ 15 | title, 16 | mask, 17 | }) 18 | } 19 | 20 | /** 21 | * 隐藏loading 22 | */ 23 | hideLoading() { 24 | Taro.hideLoading() 25 | } 26 | 27 | /** 28 | * 弹出成功信息 29 | * @param title 标题 30 | */ 31 | success(title: string) { 32 | Taro.showToast({ 33 | title, 34 | mask: true, 35 | duration: 1800, 36 | icon: 'success', 37 | }) 38 | } 39 | 40 | /** 41 | * 弹出错误信息 无图标 42 | */ 43 | info( 44 | title, 45 | mask = true, 46 | icon: 'none' | 'success' | 'loading' = 'none', 47 | duration = 1800 48 | ) { 49 | Taro.showToast({ 50 | title, 51 | mask, 52 | duration, 53 | icon, 54 | }) 55 | } 56 | 57 | /** 58 | * 隐藏toast 59 | */ 60 | hide() { 61 | Taro.hideToast() 62 | } 63 | } 64 | 65 | export default new Toast() as Toast 66 | -------------------------------------------------------------------------------- /src/shared/utils/validator.ts: -------------------------------------------------------------------------------- 1 | import Toast from './../utils/toast' 2 | 3 | /** 4 | * 表单验证类 5 | */ 6 | class FormValidator { 7 | /** 8 | * 验证函数列表 可直接使用 9 | */ 10 | funcs = { 11 | /** 12 | * 非空验证 13 | */ 14 | _notEmpty: val => val, 15 | /** 16 | * 手机号验证 17 | */ 18 | _isMobile: (value: any) => /^1[23456789]\d{9}$/.test(value), 19 | } 20 | 21 | /** 22 | * 预置的规则 可直接使用 23 | * 示例: 24 | ``` 25 | validateRules = { 26 | phoneNumber: [ 27 | Validator.rules._isMobile 28 | ], 29 | } 30 | ``` 31 | */ 32 | rules = { 33 | /** 34 | * 手机号验证规则 35 | */ 36 | _isMobile: { 37 | test: this.funcs._isMobile, 38 | errMsg: '请输入正确的手机号', 39 | }, 40 | } 41 | 42 | /** 43 | * 表单验证方法 44 | * @param rules 验证规则数组 45 | * @param showToast 是否弹出错误信息 46 | * @param obj 属性集的父级对象 47 | */ 48 | validate( 49 | rules: { 50 | [key: string]: Array<{ 51 | /** 52 | * 验证规则 53 | */ 54 | test: (value: unknown) => boolean 55 | /** 56 | * 错误信息 57 | */ 58 | errMsg: string 59 | }> 60 | }, 61 | showToast: boolean, 62 | obj: any 63 | ): { 64 | /** 65 | * 是否验证通过 66 | */ 67 | success: boolean 68 | /** 69 | * 错误信息 70 | */ 71 | errMsg: string 72 | /** 73 | * 获取到的表单数据对象 验证通过时返回 74 | */ 75 | formData?: any 76 | } { 77 | console.log('进入验证方法', rules) 78 | 79 | if (!obj) { 80 | console.error('请传入需要进行表单验证的对象') 81 | return { 82 | success: false, 83 | errMsg: `参数缺失,obj:${obj}`, 84 | } 85 | } 86 | 87 | const returnObj = { 88 | success: true, 89 | errMsg: 'ok', 90 | formData: {}, 91 | } 92 | for (const key in rules) { 93 | if (rules.hasOwnProperty(key)) { 94 | const element = rules[key] 95 | 96 | console.log('each item', element) 97 | 98 | let tempErrMsg = '' 99 | element?.forEach(item => { 100 | if (item.test(obj[key])) { 101 | console.log('test success') 102 | returnObj.formData[key] = obj[key] 103 | } else { 104 | console.log('表单验证失败', item.errMsg) 105 | if (showToast) { 106 | Toast.info(item.errMsg) 107 | } 108 | tempErrMsg = item.errMsg 109 | throw new Error(item.errMsg) 110 | } 111 | }) 112 | if (tempErrMsg) { 113 | return { 114 | success: false, 115 | errMsg: tempErrMsg, 116 | } 117 | } 118 | } 119 | } 120 | console.log('test all success', returnObj) 121 | return returnObj 122 | } 123 | } 124 | 125 | export default new FormValidator() 126 | -------------------------------------------------------------------------------- /src/tabbar.config.ts: -------------------------------------------------------------------------------- 1 | const tabbarConfig = { 2 | custom: true, 3 | selectedColor: '#FF0000', 4 | list: [ 5 | { 6 | pagePath: 'index/index', 7 | text: '首页', 8 | iconPath: './shared/assets/icons/home.png', 9 | selectedIconPath: './shared/assets/icons/home_selected.png', 10 | }, 11 | { 12 | pagePath: 'user/index', 13 | text: '我的', 14 | iconPath: './shared/assets/icons/user.png', 15 | selectedIconPath: './shared/assets/icons/user_selected.png', 16 | }, 17 | ], 18 | } 19 | 20 | module.exports = tabbarConfig 21 | -------------------------------------------------------------------------------- /src/user/index.config.ts: -------------------------------------------------------------------------------- 1 | export default definePageConfig({ 2 | navigationBarTitleText: '个人中心', 3 | }) 4 | -------------------------------------------------------------------------------- /src/user/index.scss: -------------------------------------------------------------------------------- 1 | .user-index-page { 2 | width: 100vw; 3 | height: 60vh; 4 | } 5 | 6 | .user-index-content { 7 | padding: 0 20px; 8 | font-size: 28px; 9 | color: #333; 10 | } 11 | -------------------------------------------------------------------------------- /src/user/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 个人中心页面 3 | */ 4 | 5 | import React from 'react' 6 | import { View } from '@tarojs/components' 7 | import PageContainer from '@/components/page_container' 8 | 9 | import './index.scss' 10 | 11 | const UserIndex = (): JSX.Element => { 12 | return ( 13 | 14 | 15 | I'm Lexmin, a FrontEnd Engineer. 16 | 17 | 18 | ) 19 | } 20 | 21 | export default UserIndex 22 | -------------------------------------------------------------------------------- /src/webview/index.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * webview 配置文件 3 | */ 4 | export default definePageConfig({ 5 | navigationBarTitleText: 'webview', 6 | }) 7 | -------------------------------------------------------------------------------- /src/webview/index.scss: -------------------------------------------------------------------------------- 1 | .webview-index-page { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/webview/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from '@tarojs/taro' 2 | import React from 'react' 3 | import { WebView } from '@tarojs/components' 4 | import toast from '@/utils/toast' 5 | 6 | type WebviewParams = { 7 | src: string 8 | } 9 | 10 | const Index = (): JSX.Element => { 11 | const { 12 | params: { src }, 13 | } = useRouter() as { params: WebviewParams } 14 | 15 | if (!src) { 16 | toast.info('src不能为空!') 17 | } 18 | 19 | return 20 | } 21 | 22 | export default Index 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "removeComments": false, 6 | "preserveConstEnums": true, 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "noImplicitAny": false, 10 | "allowSyntheticDefaultImports": true, 11 | "outDir": "lib", 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "strictNullChecks": true, 15 | "sourceMap": true, 16 | "baseUrl": ".", 17 | "rootDir": ".", 18 | "jsx": "react", 19 | "jsxFragmentFactory": "React.Fragment", 20 | "jsxFactory": "React.createElement", 21 | "allowJs": true, 22 | "resolveJsonModule": true, 23 | "typeRoots": ["node_modules/@types", "global.d.ts"], 24 | "paths": { 25 | "@/*": ["src/shared/*"] 26 | }, 27 | "noUncheckedIndexedAccess": true 28 | }, 29 | "exclude": ["node_modules", "dist"], 30 | "compileOnSave": false 31 | } 32 | --------------------------------------------------------------------------------