├── .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 | 
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 | [](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 |
--------------------------------------------------------------------------------