├── src
├── pages
│ ├── lab
│ │ ├── hooks.scss
│ │ ├── FormValidate.scss
│ │ ├── comp.scss
│ │ ├── index.scss
│ │ ├── hooks.tsx
│ │ ├── FormValidate.tsx
│ │ ├── index.tsx
│ │ └── comp.tsx
│ ├── user
│ │ ├── index.scss
│ │ └── index.tsx
│ ├── home
│ │ ├── index.scss
│ │ └── index.tsx
│ └── common
│ │ ├── imgPreview.scss
│ │ └── imgPreview.tsx
├── styles
│ ├── classes.scss
│ ├── theme.scss
│ ├── var.scss
│ └── mixin.scss
├── store
│ ├── index.ts
│ └── counter.ts
├── assets
│ ├── fonts
│ │ └── iconfont.ttf
│ └── images
│ │ ├── common
│ │ └── img_default_goods.png
│ │ └── icon
│ │ ├── icon_tabbar_home_default.png
│ │ ├── icon_tabbar_goods_default.png
│ │ ├── icon_tabbar_goods_selected.png
│ │ ├── icon_tabbar_home_selected.png
│ │ ├── icon_tabbar_order_default.png
│ │ └── icon_tabbar_order_selected.png
├── global_data.ts
├── enums
│ ├── userAgent.enum.ts
│ └── userPlatform.enum.ts
├── constants
│ ├── code.ts
│ └── index.ts
├── interceptors
│ ├── param.interceptor.ts
│ ├── header.interceptor.ts
│ ├── url.interceptor.ts
│ ├── del.interceptor.ts
│ └── data.interceptor.ts
├── app.scss
├── utils
│ ├── getEnv.ts
│ ├── meta.ts
│ ├── obj.ts
│ ├── img.ts
│ ├── toast.ts
│ ├── page.ts
│ ├── mp.ts
│ ├── idcard.ts
│ ├── router.ts
│ ├── validator.ts
│ └── request.ts
├── components
│ ├── Nodata
│ │ ├── Nodata.scss
│ │ └── Nodata.tsx
│ └── Tabbar
│ │ ├── Tabbar.scss
│ │ └── Tabbar.tsx
├── services
│ ├── root
│ │ └── drug.service.ts
│ ├── apisJuhe
│ │ └── mobile.service.ts
│ └── qqMap
│ │ └── ws.service.ts
├── index.html
└── app.tsx
├── .prettierignore
├── .prettierrc.js
├── docs
└── structure.png
├── .npmrc
├── .editorconfig
├── .gitignore
├── .markdownlint.json
├── config
├── dev.js
├── pro.js
├── uat.js
├── test.js
├── plugins
│ ├── getTaroVersion.js
│ ├── compareVersion.js
│ └── index.js
└── index.js
├── problems.md
├── .github
└── workflows
│ └── superlinter.yml
├── commitlint.config.js
├── tsconfig.json
├── project.config.json
├── LICENCE
├── global.d.ts
├── .eslintrc.js
├── package.json
├── .stylelintrc.js
└── README.md
/src/pages/lab/hooks.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/user/index.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .yml
2 | .yaml
3 |
--------------------------------------------------------------------------------
/src/styles/classes.scss:
--------------------------------------------------------------------------------
1 | .bold {
2 | font-weight: 700;
3 | }
4 |
--------------------------------------------------------------------------------
/src/styles/theme.scss:
--------------------------------------------------------------------------------
1 | $theme-color: #45aafa;
2 | $body-bg: #fff;
3 |
--------------------------------------------------------------------------------
/src/pages/lab/FormValidate.scss:
--------------------------------------------------------------------------------
1 | .FormValidate-page {
2 | width: 100vw;
3 | }
4 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ...require('@youtils/prettier-config-standard'),
3 | }
4 |
--------------------------------------------------------------------------------
/docs/structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexmin0412/taro2-template/HEAD/docs/structure.png
--------------------------------------------------------------------------------
/src/pages/home/index.scss:
--------------------------------------------------------------------------------
1 | .button-jsonp {
2 | font-size: 28px;
3 | color: #ff4a4a;
4 | }
5 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import counter from './counter'
2 |
3 | export default {
4 | counter
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/fonts/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexmin0412/taro2-template/HEAD/src/assets/fonts/iconfont.ttf
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | # 设置npm源
2 | registry = https://registry.npm.taobao.org
3 |
4 | # 不使用包锁定功能(不生成package-lock.json)
5 | package-lock=false
6 |
--------------------------------------------------------------------------------
/src/assets/images/common/img_default_goods.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexmin0412/taro2-template/HEAD/src/assets/images/common/img_default_goods.png
--------------------------------------------------------------------------------
/src/pages/lab/comp.scss:
--------------------------------------------------------------------------------
1 | .comp-page {
2 | width: 100vw;
3 |
4 | .comp-page-title {
5 | width: 100%;
6 | font-size: 32px;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/assets/images/icon/icon_tabbar_home_default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexmin0412/taro2-template/HEAD/src/assets/images/icon/icon_tabbar_home_default.png
--------------------------------------------------------------------------------
/src/assets/images/icon/icon_tabbar_goods_default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexmin0412/taro2-template/HEAD/src/assets/images/icon/icon_tabbar_goods_default.png
--------------------------------------------------------------------------------
/src/assets/images/icon/icon_tabbar_goods_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexmin0412/taro2-template/HEAD/src/assets/images/icon/icon_tabbar_goods_selected.png
--------------------------------------------------------------------------------
/src/assets/images/icon/icon_tabbar_home_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexmin0412/taro2-template/HEAD/src/assets/images/icon/icon_tabbar_home_selected.png
--------------------------------------------------------------------------------
/src/assets/images/icon/icon_tabbar_order_default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexmin0412/taro2-template/HEAD/src/assets/images/icon/icon_tabbar_order_default.png
--------------------------------------------------------------------------------
/src/assets/images/icon/icon_tabbar_order_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexmin0412/taro2-template/HEAD/src/assets/images/icon/icon_tabbar_order_selected.png
--------------------------------------------------------------------------------
/src/pages/lab/index.scss:
--------------------------------------------------------------------------------
1 | .sass-test {
2 | @include textOrient(1);
3 | @include wh(100%, 200px);
4 | @include setFont(36px, $theme-color);
5 | z-index: $modalZIndex;
6 | }
7 |
--------------------------------------------------------------------------------
/src/global_data.ts:
--------------------------------------------------------------------------------
1 | const globalData = {}
2 |
3 | export function set (key, val) {
4 | globalData[key] = val
5 | }
6 |
7 | export function get (key) {
8 | return globalData[key]
9 | }
10 |
--------------------------------------------------------------------------------
/src/styles/var.scss:
--------------------------------------------------------------------------------
1 | // 页面中普通的元素定位zIndex
2 | $absoluteIndex: 9;
3 |
4 | // 固定定位的元素zIndex 如底部的提交按钮 头部导航等
5 | $fixedZIndex: 99;
6 |
7 | // 弹窗元素zIndex 一般需要最高的层级
8 | $modalZIndex: 999;
9 |
10 |
--------------------------------------------------------------------------------
/src/enums/userAgent.enum.ts:
--------------------------------------------------------------------------------
1 | enum USER_SYSTEM {
2 | IOS = 'ios', // ios系统
3 | ANDROID = 'android', // 安卓
4 | WINDOWS_PHONE = 'windows_phone', // wp
5 | UNKNOWN = 'unknown', // 未知
6 | }
7 |
8 | export default USER_SYSTEM
9 |
--------------------------------------------------------------------------------
/src/constants/code.ts:
--------------------------------------------------------------------------------
1 | // 接口返回code
2 |
3 | // 成功code
4 | export const SUCC_LIST = [
5 | '0',
6 | '00000',
7 | '10000',
8 | ]
9 |
10 | // 登录失效code
11 | export const LOGIN_FAILURE_LIST = [
12 | '99999',
13 | '40000',
14 | '40001'
15 | ]
16 |
--------------------------------------------------------------------------------
/src/enums/userPlatform.enum.ts:
--------------------------------------------------------------------------------
1 | enum USER_PLATFORM {
2 | WEAPP = 'weapp', // 微信小程序
3 | WEIXIN = 'weixin', // 微信h5环境
4 | ALIPAY = 'alipay', // 支付宝小程序
5 | H5 = 'h5', // h5
6 | UNKNOWN = 'unknown', // 未知
7 | }
8 |
9 | export default USER_PLATFORM
10 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | deploy_versions/
3 | .temp/
4 | .rn_temp/
5 | node_modules/
6 | .DS_Store
7 | project.config.json
8 | package-lock.json
9 | yarn.lock
10 |
11 | # 本地local配置文件
12 | config/local.js
13 |
14 | # 以下文件打包时会自动生成
15 | # src/app.tsx
16 | src/pages/routes.js
17 | src/components/index.ts
18 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "default": true,
3 | "MD003": { "style": "atx_closed" },
4 | "MD007": { "indent": 2 },
5 | "no-hard-tabs": false,
6 | "whitespace": false,
7 | "ul-indent": false,
8 | "header-style": false,
9 | "code-block-style": false,
10 | "line-length": false,
11 | "ol-prefix": false
12 | }
13 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/config/dev.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | defineConstants: {
3 | APP_CONF: {
4 | API_HOST: JSON.stringify('https://xx.com/'),
5 | APPID: JSON.stringify('this_is_my_tourist_appid'),
6 | API_MAP_QQ: JSON.stringify('https://apis.map.qq.com'),
7 | KEY_MAP_QQ: JSON.stringify('UQPBZ-RCU36-K2YS3-EMV6Y-JI6JJ-3WBUM'),
8 | },
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/config/pro.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | defineConstants: {
3 | APP_CONF: {
4 | API_HOST: JSON.stringify('https://xx.com/'),
5 | APPID: JSON.stringify('this_is_my_tourist_appid'),
6 | API_MAP_QQ: JSON.stringify('https://apis.map.qq.com'),
7 | KEY_MAP_QQ: JSON.stringify('UQPBZ-RCU36-K2YS3-EMV6Y-JI6JJ-3WBUM'),
8 | },
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/config/uat.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | defineConstants: {
3 | APP_CONF: {
4 | API_HOST: JSON.stringify('https://xx.com/'),
5 | APPID: JSON.stringify('this_is_my_tourist_appid'),
6 | API_MAP_QQ: JSON.stringify('https://apis.map.qq.com'),
7 | KEY_MAP_QQ: JSON.stringify('UQPBZ-RCU36-K2YS3-EMV6Y-JI6JJ-3WBUM'),
8 | },
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/config/test.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | defineConstants: {
3 | APP_CONF: {
4 | API_HOST: JSON.stringify('https://xx.com/'),
5 | APPID: JSON.stringify('this_is_my_tourist_appid'),
6 | API_MAP_QQ: JSON.stringify('https://apis.map.qq.com'),
7 | KEY_MAP_QQ: JSON.stringify('UQPBZ-RCU36-K2YS3-EMV6Y-JI6JJ-3WBUM'),
8 | },
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/src/store/counter.ts:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx'
2 |
3 | const counter = observable({
4 | counter: 0,
5 | increment() {
6 | this.counter++
7 | },
8 | decrement() {
9 | this.counter--
10 | },
11 | incrementAsync() {
12 | setTimeout(() => {
13 | this.counter++
14 | }, 1000)
15 | }
16 | })
17 | export default counter
18 |
--------------------------------------------------------------------------------
/src/app.scss:
--------------------------------------------------------------------------------
1 | // @import '~taro-ui/dist/style/index.scss';
2 |
3 | // @import "~taro-ui/dist/style/components/noticeBar.scss";
4 | // @import "~taro-ui/dist/style/components/tag.scss";
5 |
6 | // iconfont引入
7 | @font-face {
8 | font-family: 'iconfont';
9 | src: url('./assets/fonts/iconfont.ttf');
10 | }
11 | .iconfont {
12 | font-family: iconfont;
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/getEnv.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 获取运行华宁
3 | */
4 |
5 | class GetEnv {
6 | /**
7 | * 是否是微信h5
8 | */
9 | isWechatH5 = () => {
10 | if (navigator) {
11 | const userAgent: any = navigator.userAgent.toLowerCase()
12 | return userAgent.match(/MicroMessenger/i) == 'micromessenger'
13 | }
14 | return false
15 | }
16 | }
17 |
18 | export default new GetEnv()
19 |
--------------------------------------------------------------------------------
/src/components/Nodata/Nodata.scss:
--------------------------------------------------------------------------------
1 | .nodata-comp {
2 | width: 100%;
3 | height: 60vh;
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | flex-direction: column;
8 |
9 | .no-data-icon {
10 | width: 236px;
11 | height: 154px;
12 | }
13 |
14 | .no-data-text {
15 | margin-top: 28px;
16 | font-size: 28px;
17 | color: #999;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/pages/lab/hooks.tsx:
--------------------------------------------------------------------------------
1 | import Taro, { Config } from '@tarojs/taro'
2 | import { View } from '@tarojs/components'
3 | import { observer } from '@tarojs/mobx'
4 |
5 | import './hooks.scss'
6 |
7 | const Index = () => hooks
8 |
9 | Index.config = {
10 | navigationBarTitleText: 'hooks',
11 | } as Config
12 |
13 | export default observer(Index)
14 |
--------------------------------------------------------------------------------
/src/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 | export default Constants
20 |
--------------------------------------------------------------------------------
/src/utils/meta.ts:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro'
2 |
3 | class Meta {
4 | /**
5 | * 设置页面标题
6 | * @param title 标题文字
7 | */
8 | setTitle(title: string) {
9 | if ( process.env.TARO_ENV === 'h5' ) {
10 | document.title = title
11 | } else {
12 | Taro.setNavigationBarTitle({
13 | title
14 | })
15 | }
16 | }
17 | }
18 |
19 | export default new Meta()
20 |
--------------------------------------------------------------------------------
/src/interceptors/header.interceptor.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 头部拦截器 处理请求头的配置
3 | */
4 |
5 | import Constants from '~/constants/index'
6 |
7 | export default function(chain) {
8 | console.log('enter header interceptor', chain)
9 | const requestParams = chain.requestParams
10 |
11 | const { header } = requestParams
12 |
13 | requestParams.header = header
14 |
15 | return chain.proceed(requestParams)
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils/obj.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 对象操作
3 | */
4 |
5 | class Obj {
6 | /**
7 | * 提取对象属性,返回新对象
8 | * @param {object} obj 提取对象
9 | * @param {Array} propArr 键值数组
10 | */
11 | pickAttrFromObj = (obj: any, propArr: Array) => {
12 | const newObj: any = {}
13 | propArr.forEach((item) => {
14 | newObj[item] = obj[item]
15 | })
16 | return newObj
17 | }
18 | }
19 |
20 | export default new Obj()
21 |
--------------------------------------------------------------------------------
/src/components/Tabbar/Tabbar.scss:
--------------------------------------------------------------------------------
1 | .tabbar-comp {
2 | position: fixed;
3 | bottom: 0;
4 | left: 0;
5 | display: flex;
6 | align-items: center;
7 | width: 100%;
8 | height: 120px;
9 | background-color: #ff4a4a;
10 |
11 | .tab-item {
12 | flex: 1;
13 | height: 120px;
14 | font-weight: 700;
15 | font-size: 28px;
16 | line-height: 120px;
17 | text-align: center;
18 | color: #333;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/config/plugins/getTaroVersion.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | /**
4 | * 获取node_modules/taro脚手架依赖路径
5 | */
6 | const getCliPath = () => {
7 | return path.resolve(__dirname, './../../node_modules/@tarojs/cli/')
8 | }
9 |
10 | /**
11 | * 获取taro version
12 | */
13 | const getTaroVersion = () => {
14 | return require(path.join(getCliPath(), 'package.json')).version
15 | }
16 |
17 | /**
18 | * 获取taro版本号
19 | */
20 | module.exports = getTaroVersion
21 |
--------------------------------------------------------------------------------
/problems.md:
--------------------------------------------------------------------------------
1 | # 常见问题及需要注意的点
2 |
3 | > 注意:以下内容均基于 Taro 1.3.36 版本
4 |
5 | ## 1. H5端
6 |
7 | ### 1.1 不能嵌套setState
8 |
9 | ```tsx
10 | this.setState({
11 | state1: []
12 | }, () => {
13 | this.setState({
14 | state2: []
15 | })
16 | })
17 | ```
18 |
19 | 以上代码中,`state2` 不能够被正确改变。
20 |
21 | ### 1.2 onReachBottom 会先于 componentDidShow 触发
22 |
23 | 在列表类的页面中,经常需要在初始化页面时请求数据,然后通过 `onReachBottom` 触发上拉加载,但是在h5环境中第一次进入页面时,`componentDidShow` 和 `onReachBottom` 生命周期都会触发。
24 |
--------------------------------------------------------------------------------
/src/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/pages/user/index.tsx:
--------------------------------------------------------------------------------
1 | import Taro, { Config } from '@tarojs/taro'
2 | import { View } from '@tarojs/components'
3 | import { observer } from '@tarojs/mobx'
4 |
5 | import Tabbar from '~/components/Tabbar/Tabbar'
6 | import './index.scss'
7 |
8 | export const UserIndex = () => {
9 | return (
10 |
11 | user index page
12 |
13 |
14 | )
15 | }
16 |
17 | UserIndex.config = {
18 | navigationBarTitleText: '我的',
19 | } as Config
20 |
21 | export default observer(UserIndex)
22 |
--------------------------------------------------------------------------------
/src/services/root/drug.service.ts:
--------------------------------------------------------------------------------
1 | import BaseRequest from '~/utils/request'
2 |
3 | /**
4 | * 莲藕相关服务
5 | */
6 | class LianouService extends BaseRequest {
7 | constructor() {
8 | super({
9 | hostKey: 'API_HOST',
10 | })
11 | }
12 |
13 | /**
14 | * 根据药品获取疾病
15 | */
16 | queryDiseaseByDrugName(payload: {
17 | ComName: string // 药品名称 多个药品用_隔开
18 | }) {
19 | return this.post({
20 | url: '/drug/queryDiseaseByDrugName',
21 | data: payload,
22 | })
23 | }
24 | }
25 |
26 | export default new LianouService()
27 |
--------------------------------------------------------------------------------
/src/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/services/apisJuhe/mobile.service.ts:
--------------------------------------------------------------------------------
1 | import BaseRequest from '~/utils/request'
2 |
3 | class MobileService extends BaseRequest {
4 | constructor() {
5 | super({
6 | hostKey: 'APIS_JUHE'
7 | })
8 | }
9 |
10 | /**
11 | * 查询手机号码归属地
12 | */
13 | queryMobile(params: {
14 | phoneNumber: number|string
15 | }): Promise {
16 | console.log('into service', params)
17 | const { phoneNumber } = params
18 | return this.get({
19 | url: `/mcang.php/Exhibition/getExhibitionBanner`,
20 | data: {}
21 | })
22 | }
23 | }
24 |
25 | export default new MobileService() as MobileService
26 |
--------------------------------------------------------------------------------
/.github/workflows/superlinter.yml:
--------------------------------------------------------------------------------
1 | name: 构建 H5 应用
2 | on:
3 | push:
4 | branches:
5 | - 2.x
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
19 | npm run build:h5-pro
20 |
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 |
--------------------------------------------------------------------------------
/config/plugins/compareVersion.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 版本号比较 如果v1>v2则返回大于0的结果
3 | * @param {*} v1
4 | * @param {*} v2
5 | */
6 | const compareVersion = (v1, v2) => {
7 | v1 = v1.split('.')
8 | v2 = v2.split('.')
9 | const len = Math.max(v1.length, v2.length)
10 |
11 | while (v1.length < len) {
12 | v1.push('0')
13 | }
14 | while (v2.length < len) {
15 | v2.push('0')
16 | }
17 |
18 | for (let i = 0; i < len; i++) {
19 | const num1 = parseInt(v1[i])
20 | const num2 = parseInt(v2[i])
21 |
22 | if (num1 > num2) {
23 | return 1
24 | } else if (num1 < num2) {
25 | return -1
26 | }
27 | }
28 |
29 | return 0
30 | }
31 |
32 | module.exports = compareVersion
33 |
--------------------------------------------------------------------------------
/src/utils/img.ts:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro'
2 |
3 | class Img {
4 | constructor() {}
5 |
6 | /**
7 | * 图片url拼接
8 | */
9 | handleImgUrl(url: any): string {
10 | // 如果url不带https
11 | if (!(url.indexOf('http') > -1)) {
12 | url = `https://xxx.com/${url}`
13 | }
14 | return url
15 | }
16 |
17 | /**
18 | * 图片预览
19 | */
20 | public preview(param: {
21 | /**
22 | * 当前索引 0开始
23 | */
24 | current: number;
25 | /**
26 | * 图片列表
27 | */
28 | list: Array;
29 | }) {
30 | Taro.navigateTo({
31 | // @ts-ignore
32 | url: `/pages/common/imgPreview?data=${encodeURIComponent(JSON.stringify(param))}`
33 | })
34 | }
35 | }
36 |
37 | export default new Img()
38 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | rules: {
4 | 'type-enum': [
5 | 2,
6 | 'always',
7 | [
8 | 'test', // 测试代码
9 | 'feat', // 新增内容
10 | 'fix', // 修复bug
11 | 'refactor', // 代码重构(不改变外部行为)
12 | 'style', // 代码格式化
13 | 'docs', // 文档更新
14 | 'conf', // 项目配置文件的更改(如.eslintrc的修改)
15 | 'revert', // 回退
16 | 'perf', // 优化(代码或性能优化)
17 | 'build', // 项目打包相关(如build文件夹下编译插件的修改)
18 | 'chore', // 除以上类型之外的其他提交
19 | ],
20 | ],
21 | 'type-case': [0],
22 | 'type-empty': [0],
23 | 'scope-empty': [0],
24 | 'scope-case': [0],
25 | 'subject-full-stop': [0, 'never'],
26 | 'subject-case': [0, 'never'],
27 | 'header-max-length': [0, 'always', 72],
28 | },
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Nodata/Nodata.tsx:
--------------------------------------------------------------------------------
1 | import Taro, { Component } from '@tarojs/taro';
2 | import { View, Image, Text } from '@tarojs/components';
3 |
4 | import defaultIcon from '~/assets/images/common/img_default_goods.png'
5 | import './Nodata.scss'
6 |
7 | /**
8 | * 组件需要的Props定义
9 | */
10 | interface IProps {
11 | height?: number; // 高度
12 | icon?: string; // 颜色
13 | text?: string; // 缺省文字
14 | }
15 |
16 | export default class Line extends Component {
17 |
18 | render() {
19 | const { text, height, icon } = this.props
20 | return (
21 |
22 |
24 | {text||'暂无相关数据'}
25 |
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/utils/toast.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * taro toast封装简化
3 | */
4 |
5 | import Taro from '@tarojs/taro'
6 |
7 | class Toast {
8 | loading(title, mask = true) {
9 | Taro.showLoading({
10 | title,
11 | mask,
12 | })
13 | }
14 |
15 | hideLoading() {
16 | Taro.hideLoading()
17 | }
18 |
19 | // toast错误
20 | error(errMsg: string) {
21 | Taro.showToast({
22 | title: errMsg,
23 | mask: true,
24 | icon: "none",
25 | duration: 1500
26 | });
27 | }
28 |
29 | show( title, mask = true, icon: 'none' | 'success' | 'loading' = 'none', duration = 1200 ) {
30 | Taro.showToast({
31 | title,
32 | mask,
33 | duration,
34 | icon
35 | })
36 | }
37 |
38 | hide() {
39 | Taro.hideToast()
40 | }
41 | }
42 |
43 | export default new Toast() as Toast
44 |
--------------------------------------------------------------------------------
/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": "preserve",
19 | "jsxFactory": "Taro.createElement",
20 | "allowJs": true,
21 | "resolveJsonModule": true,
22 | "typeRoots": ["node_modules/@types", "global.d.ts"],
23 | "paths": {
24 | "~/*": ["src/*"]
25 | }
26 | },
27 | "exclude": ["node_modules", "dist"],
28 | "compileOnSave": false
29 | }
30 |
--------------------------------------------------------------------------------
/project.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "miniprogramRoot": "./dist",
3 | "projectname": "Taro2.x项目模板",
4 | "description": "Taro2.x项目模板",
5 | "appid": "this_is_my_tourist_appid",
6 | "setting": {
7 | "urlCheck": true,
8 | "es6": false,
9 | "postcss": false,
10 | "minified": false
11 | },
12 | "compileType": "miniprogram",
13 | "condition": {
14 | "search": {
15 | "current": -1,
16 | "list": []
17 | },
18 | "conversation": {
19 | "current": -1,
20 | "list": []
21 | },
22 | "plugin": {
23 | "current": -1,
24 | "list": []
25 | },
26 | "game": {
27 | "list": []
28 | },
29 | "miniprogram": {
30 | "current": 2,
31 | "list": [
32 | {
33 | "id": -1,
34 | "name": "首页",
35 | "pathName": "pages/index/index",
36 | "query": "",
37 | "scene": null
38 | }
39 | ]
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/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: 'pages/index/index',
15 | /**
16 | * 授权页
17 | */
18 | auth: 'pages/auth/auth',
19 | /**
20 | * 个人中心页
21 | */
22 | user: 'pages/user/index'
23 | }
24 | }
25 |
26 | // 获取当前路由
27 | getCurRoute() {
28 | if ( process.env.TARO_ENV === 'weapp' ) {
29 | const curPages = Taro.getCurrentPages()
30 | return curPages[curPages.length-1].route
31 | } else {
32 | const location = window.location
33 | return location.pathname.slice(1)
34 | }
35 | }
36 |
37 | backToHome() {
38 | Taro.switchTab({
39 | url: '/pages/goods/list'
40 | })
41 | }
42 | }
43 |
44 | export default new Pages()
45 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/pages/common/imgPreview.scss:
--------------------------------------------------------------------------------
1 | .imgPreview-page {
2 | display: flex;
3 | flex-direction: column;
4 | width: 100vw;
5 | height: 100vh;
6 | background: #000;
7 |
8 | .img-preview-header {
9 | position: fixed;
10 | top: 0;
11 | left: 0;
12 | z-index: 99;
13 | display: flex;
14 | align-items: center;
15 | width: 100%;
16 | height: 80px;
17 | padding: 0 20px;
18 | line-height: 80px;
19 | color: #fff;
20 |
21 | .back {
22 | width: 80px;
23 | font-size: 32px;
24 | }
25 | .title {
26 | flex: 1;
27 | text-align: center;
28 | font-size: 32px;
29 | }
30 | .right-actions {
31 | width: 80px;
32 | }
33 | }
34 | .img-preview-swiper {
35 | width: 100%;
36 | height: 100%;
37 |
38 | .img-preview-swiper-item {
39 | // width: 100vw!important;
40 | // height: 100%;
41 |
42 | .img-preview-ele {
43 | width: 100vw;
44 | height: 100%;
45 | display: flex;
46 | align-items: center;
47 | justify-content: center;
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/services/qqMap/ws.service.ts:
--------------------------------------------------------------------------------
1 | import BaseRequest from '~/utils/request'
2 |
3 | class QQMapWebService extends BaseRequest {
4 | constructor() {
5 | super({
6 | hostKey: 'API_MAP_QQ'
7 | })
8 | }
9 |
10 | /**
11 | * 逆地址解析
12 | */
13 | geocoder(payload: {
14 | /**
15 | * 位置信息 格式 ,
16 | */
17 | location: string,
18 | /**
19 | * 是否获取poi列表
20 | */
21 | get_poi: 1 | 0,
22 | }): Promise {
23 | if (process.env.TARO_ENV === 'h5') {
24 | return this.jsonp({
25 | url: '/ws/geocoder/v1',
26 | data: {
27 | ...payload,
28 | key: APP_CONF.KEY_MAP_QQ,
29 | output: 'jsonp',
30 | callback: 'jsonhandle1'
31 | },
32 | resType: 1,
33 | })
34 | } else {
35 | return this.post({
36 | url: '/ws/geocoder/v1',
37 | data: {
38 | ...payload,
39 | key: APP_CONF.KEY_MAP_QQ,
40 | },
41 | resType: 1,
42 | })
43 | }
44 | }
45 | }
46 |
47 | export default new QQMapWebService() as QQMapWebService
48 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 lexmin0412
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/Tabbar/Tabbar.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 底部Tabbar
3 | */
4 |
5 | import Taro, { Component } from '@tarojs/taro'
6 | import { View } from '@tarojs/components'
7 |
8 | import './Tabbar.scss'
9 |
10 | /**
11 | * 组件内部属性
12 | */
13 | interface IState {
14 | tabList: Array<{
15 | id: number
16 | router: string
17 | text: string
18 | }>
19 | }
20 |
21 | class Tabbar extends Component<{}, IState> {
22 | constructor(props) {
23 | super(props)
24 | this.state = {
25 | tabList: [
26 | {
27 | id: 1,
28 | router: '/pages/home/index',
29 | text: '首页',
30 | },
31 | {
32 | id: 2,
33 | router: '/pages/lab/index',
34 | text: '实验室',
35 | },
36 | {
37 | id: 2,
38 | router: '/pages/user/index',
39 | text: '个人中心',
40 | },
41 | ],
42 | }
43 | }
44 |
45 | handleTabItemClick(item) {
46 | const { router } = item
47 | Taro.redirectTo({
48 | url: router,
49 | })
50 | }
51 |
52 | render() {
53 | const { tabList } = this.state
54 | return false ? (
55 |
56 | {tabList.map(tabItem => {
57 | return (
58 |
62 | {tabItem.text}
63 |
64 | )
65 | })}
66 |
67 | ) : (
68 |
69 | )
70 | }
71 | }
72 |
73 | export default Tabbar
74 |
--------------------------------------------------------------------------------
/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, HTMLEmbedElement>
15 | }
16 | }
17 |
18 | // @ts-ignore
19 | declare const process: {
20 | env: {
21 | /**
22 | * TARO环境变量
23 | */
24 | TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'quickapp' | 'qq'
25 | /**
26 | * node环境变量
27 | */
28 | NODE_ENV: 'dev' | 'sit' | 'uat' | 'pro'
29 | /**
30 | * 其他扩展属性
31 | */
32 | [key: string]: any
33 | }
34 | }
35 |
36 | /**
37 | * NODE环境变量 dev-开发 sit-测试 uat-预发 pro-生产
38 | */
39 | declare const NODE_ENV: 'dev' | 'sit' | 'uat' | 'pro'
40 |
41 | /**
42 | * 环境变量配置
43 | */
44 | declare const APP_CONF: {
45 | /**
46 | * 接口HOST
47 | */
48 | API_HOST: string
49 | /**
50 | * 图片oss域名
51 | */
52 | IMG_OSS_PREFIX: string
53 | /**
54 | * 腾讯地图接口服务域名
55 | */
56 | API_MAP_QQ: string
57 | /**
58 | * 腾讯地图服务key
59 | */
60 | KEY_MAP_QQ: string
61 | }
62 |
63 |
64 | /**
65 | * 微信jssdk对象
66 | */
67 | declare const wx: any
68 |
69 | /**
70 | * 微信jsbridge
71 | */
72 | declare const WeixinJSBridge: any
73 |
--------------------------------------------------------------------------------
/config/plugins/index.js:
--------------------------------------------------------------------------------
1 | const compareVersion = require('./compareVersion')
2 | const getTaroVersion = require('./getTaroVersion')
3 |
4 | const pluginList = [
5 | [
6 | // 环境变量检查插件
7 | '@tarox/plugin-check-env',
8 | {
9 | // 配置需要检查的环境变量
10 | ENV_LIST: {
11 | API_HOST: '接口API域名',
12 | APPID: '小程序APPID',
13 | API_MAP_QQ: '腾讯地图API/WebService域名',
14 | KEY_MAP_QQ: '腾讯地图Key',
15 | },
16 | taroVersion: {
17 | h5: '2.2.18',
18 | weapp: '2.2.18',
19 | },
20 | },
21 | ],
22 | [
23 | // 入口文件初始化插件
24 | '@tarox/plugin-init-app',
25 | {
26 | // 配置首页路由
27 | homeRoute: 'pages/home/index',
28 | // 需要打包的页面
29 | includePages: [
30 | 'pages/home/index',
31 | 'pages/classify/index',
32 | 'pages/classify/searchResult',
33 | 'pages/details/index',
34 | ],
35 | },
36 | ],
37 | '@tarox/plugin-generate',
38 | ]
39 |
40 | // 小程序添加 taro-plugin-mp 插件
41 | if (process.env.TARO_ENV === 'weapp') {
42 | pluginList.push(
43 | // 小程序project.config.json文件生成插件
44 | '@tarox/plugin-mp'
45 | )
46 | }
47 |
48 | // 获取当前项目的taro版本号
49 | const taroVersion = getTaroVersion()
50 |
51 | // taro2.2.8以上的版本官方将uglify/scss插件被分离出了两个插件,所以这里需要插入
52 | const shouldPushUglifyNSassPlugin = compareVersion(taroVersion, '2.2.8') >= 0
53 | console.log(
54 | '当前taro版本',
55 | taroVersion,
56 | `${shouldPushUglifyNSassPlugin ? '' : '不'}需要单独引入 uglify 和 sass 插件`
57 | )
58 |
59 | if (shouldPushUglifyNSassPlugin) {
60 | pluginList.unshift(['@tarojs/plugin-uglify'])
61 | pluginList.unshift(['@tarojs/plugin-sass'])
62 | }
63 |
64 | module.exports = pluginList
65 |
--------------------------------------------------------------------------------
/src/utils/mp.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 小程序相关工具类封装
3 | */
4 | import Taro from "@tarojs/taro";
5 | import { set as setGlobalData, get as getGlobalData } from './../global_data'
6 |
7 | /**
8 | * 监控网络状态变化
9 | */
10 | export const addNetworkListener = () => {
11 | Taro.onNetworkStatusChange(res => {
12 | console.error('网络状态改变', res)
13 | console.error('success', res.isConnected)
14 | console.error('type', res.networkType)
15 | if (res.networkType === 'none') {
16 | Taro.showModal({
17 | title: '互联网链接已断开, 请检查您的网络后重试',
18 | confirmText: '确定',
19 | confirmColor: '#576B95',
20 | showCancel: false,
21 | success: function (res) {
22 | if (res.confirm) {
23 | }
24 | }
25 | })
26 | }
27 | })
28 | }
29 |
30 | /**
31 | * 检查小程序版本更新
32 | */
33 | export const checkUpdate = () => {
34 | if (process.env.TARO_ENV === "weapp") {
35 | const updateManager = Taro.getUpdateManager();
36 | updateManager.onUpdateReady(() => {
37 | Taro.showModal({
38 | title: "更新提示",
39 | content: "新版本已经准备好,是否重启应用?",
40 | success(res) {
41 | if (res.confirm) {
42 | // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
43 | updateManager.applyUpdate();
44 | }
45 | }
46 | });
47 | });
48 | }
49 | };
50 |
51 | /**
52 | * 保存系统信息
53 | */
54 | export const saveSystemInfo = () => {
55 | const systemInfo = Taro.getSystemInfoSync()
56 | let menuBtnRect = Taro.getMenuButtonBoundingClientRect()
57 | console.error('systemInfo', systemInfo)
58 | console.error('menuBtnRect', menuBtnRect)
59 |
60 | // 胶囊信息获取失败兼容
61 | const safeArea = systemInfo.safeArea
62 | if ( !menuBtnRect ) {
63 | menuBtnRect = {
64 | height: systemInfo.statusBarHeight,
65 | top: safeArea && safeArea.top ? safeArea.top + 6 : 26,
66 | }
67 | }
68 | setGlobalData('statusBarHeight', systemInfo.statusBarHeight)
69 | setGlobalData('menuBtnRect', menuBtnRect)
70 | };
71 |
--------------------------------------------------------------------------------
/src/pages/common/imgPreview.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 图片预览页面
3 | */
4 | import Taro, { useState, useEffect, useRouter, Config } from '@tarojs/taro'
5 | import { View, Image, Swiper, SwiperItem } from '@tarojs/components'
6 | import { observer } from '@tarojs/mobx'
7 |
8 | import './imgPreview.scss'
9 |
10 | export const ImgPreview = () => {
11 | const [list, setList] = useState>([])
12 | const [current, setCurrent] = useState(0)
13 | const { params } = useRouter()
14 |
15 | useEffect(() => {
16 | document.title = '图片预览'
17 | const data = JSON.parse(decodeURIComponent(params.data))
18 | console.log('data', data)
19 | setList(data.list)
20 | setCurrent(data.current)
21 | }, [])
22 |
23 | const handleBackClick = () => {
24 | Taro.navigateBack()
25 | }
26 |
27 | const handleSwiperClick = e => {
28 | setCurrent(e.detail.current)
29 | }
30 |
31 | return list && list.length > 0 ? (
32 |
33 |
34 |
35 | 返回
36 |
37 |
38 | {current + 1}/{list.length}
39 |
40 |
41 |
42 |
49 | {list &&
50 | list.map(item => {
51 | return (
52 |
53 |
59 |
60 | )
61 | })}
62 |
63 |
64 | ) : null
65 | }
66 |
67 | ImgPreview.config = {
68 | navigationBarTitleText: '图片预览',
69 | } as Config
70 |
71 | export default observer(ImgPreview)
72 |
--------------------------------------------------------------------------------
/src/pages/lab/FormValidate.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 表单验证测试
3 | */
4 |
5 | import Taro, { Config, useState } from '@tarojs/taro'
6 | import { View, Button, Input } from '@tarojs/components'
7 | import { observer } from '@tarojs/mobx'
8 | import toast from '~/utils/toast'
9 | import _validator from '~/utils/validator'
10 |
11 | import './FormValidate.scss'
12 |
13 | const FormValidate = () => {
14 | const [phone, setPhone] = useState('')
15 | const [address, setAddress] = useState('')
16 |
17 | /**
18 | * 表单验证
19 | */
20 | const handleValidate = () => {
21 | const funcs = _validator.funcs
22 | const validResult = _validator.validate(
23 | {
24 | phone: [
25 | {
26 | errMsg: '请输入手机号',
27 | test: funcs._notEmpty,
28 | },
29 | {
30 | errMsg: '请输入正确长度的手机号',
31 | test: val => val.length === 11,
32 | },
33 | ],
34 | address: [
35 | {
36 | errMsg: '请输入地址',
37 | test: funcs._notEmpty,
38 | },
39 | ],
40 | },
41 | true,
42 | {
43 | phone,
44 | address,
45 | }
46 | )
47 | if (validResult.success) {
48 | toast.show('验证成功')
49 | } else {
50 | console.error('validResult', validResult)
51 | }
52 | }
53 |
54 | const handleInput = (type, e) => {
55 | switch (type) {
56 | case 'phone':
57 | setPhone(e.detail.value)
58 | break
59 | case 'address':
60 | setAddress(e.detail.value)
61 | break
62 | default:
63 | break
64 | }
65 | }
66 |
67 | return (
68 |
69 | 表单验证测试
70 |
71 | handleInput('phone', e)}
75 | />
76 | handleInput('address', e)}
80 | />
81 |
82 | )
83 | }
84 |
85 | FormValidate.config = {
86 | navigationBarTitleText: '表单验证测试',
87 | } as Config
88 |
89 | export default observer(FormValidate)
90 |
--------------------------------------------------------------------------------
/src/styles/mixin.scss:
--------------------------------------------------------------------------------
1 | // 宽高
2 | @mixin wh($width, $height) {
3 | width: $width;
4 | height: $height;
5 | }
6 |
7 | // 设置字体大小及颜色
8 | @mixin setFont($fontSize, $color) {
9 | font-size: $fontSize;
10 | color: $color;
11 | }
12 |
13 | // 多行截取
14 | @mixin textOrient($line) {
15 | display: -webkit-box;
16 | // 需要加上这一句autoprefixer的忽略规则 否则这一行样式加不上 导致无法展示省略号
17 | /*! autoprefixer: ignore next */
18 | -webkit-box-orient: vertical;
19 | -webkit-line-clamp: $line;
20 | text-overflow: ellipsis;
21 | overflow: hidden;
22 | word-break: break-all;
23 | // white-space: nowrap;
24 | }
25 |
26 | // 1px边框处理
27 |
28 | // 下边框
29 | @mixin border-bottom-1px($color: #eee, $type: solid) {
30 | &:after {
31 | content: " ";
32 | position: absolute;
33 | left: 0;
34 | bottom: 0;
35 | width: 100%;
36 | height: 1px;
37 | border-bottom: 1px $type $color;
38 | transform-origin: left bottom;
39 | transform: scale(1, 0.5);
40 | }
41 | }
42 |
43 | /* 上边框 */
44 | @mixin border-top-1px($color: #eee, $type: solid) {
45 | &:after {
46 | content: " ";
47 | position: absolute;
48 | left: 0;
49 | top: 0;
50 | width: 100%;
51 | height: 1px;
52 | border-top: 1px $type $color;
53 | transform-origin: left top;
54 | transform: scale(1, 0.5);
55 | }
56 | }
57 |
58 | /* 左边框 */
59 | @mixin border-left-1px($color: #eee, $type: solid) {
60 | &:after {
61 | content: " ";
62 | position: absolute;
63 | left: 0;
64 | top: 0;
65 | width: 5px;
66 | height: 100%;
67 | border-left: 1px $type $color;
68 | transform-origin: left top;
69 | transform: scale(0.5, 1);
70 | }
71 | }
72 |
73 | /* 右边框 */
74 | @mixin border-right-1px($color: #eee, $type: solid) {
75 | &:after {
76 | content: " ";
77 | position: absolute;
78 | right: 0;
79 | top: 0;
80 | width: 5px;
81 | height: 100%;
82 | border-right: 1px $type $color;
83 | transform-origin: right top;
84 | transform: scale(0.5, 1);
85 | }
86 | }
87 |
88 | /* 四边框 */
89 | @mixin border-1px($radius: 0px, $color: #eee) {
90 | &:after {
91 | content: "";
92 | position: absolute;
93 | left: 0;
94 | top: 0;
95 | width: 200%;
96 | height: 200%;
97 | border: 1px $type $color;
98 | transform-origin: 0 0;
99 | transform: scale(0.5, 0.5);
100 | box-sizing: border-box;
101 | border-radius: $radius;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/pages/lab/index.tsx:
--------------------------------------------------------------------------------
1 | import Taro, { Config } from '@tarojs/taro'
2 | import { View, Button, Text } from '@tarojs/components'
3 | import { observer } from '@tarojs/mobx'
4 | import { AtNoticebar, AtTag } from 'taro-ui'
5 |
6 | import Tabbar from '~/components/Tabbar/Tabbar'
7 | import QQMapWSService from '~/services/qqMap/ws.service'
8 | import LianouService from '~/services/root/drug.service'
9 | import './index.scss'
10 |
11 | const LabIndex = () => {
12 | const handleJSONPTest = async () => {
13 | const result = await QQMapWSService.geocoder({
14 | location: `28.2532,112.87887`,
15 | get_poi: 0,
16 | })
17 | console.log('result', result)
18 | }
19 |
20 | const handleProxyText = async () => {
21 | const result = await LianouService.queryDiseaseByDrugName({
22 | ComName: '阿莫西林胶囊',
23 | })
24 | console.log('result', result)
25 | }
26 |
27 | const handleCustomRoute = () => {
28 | console.error('into handleCustomRoute')
29 | Taro.switchTab({
30 | url: '/pages/lab/index',
31 | })
32 | }
33 |
34 | const handleCompTest = type => {
35 | Taro.navigateTo({
36 | url: `/pages/lab/comp?type=${type}`,
37 | })
38 | }
39 |
40 | return (
41 |
42 | taro-ui组件演示:通告栏
43 | taro-ui组件演示:标签
44 |
47 |
48 |
49 |
52 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
65 |
68 |
71 |
72 |
73 |
74 | )
75 | }
76 |
77 | LabIndex.config = {
78 | navigationBarTitleText: '首页',
79 | } as Config
80 |
81 | export default observer(LabIndex)
82 |
--------------------------------------------------------------------------------
/src/utils/idcard.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 身份证相关工具类
3 | */
4 |
5 | class IDCard {
6 | /**
7 | * 分析身份证,计算年龄,性别
8 | * @param {string} identityCard 身份证号码
9 | * @param {string} isEncrypt 是否脱敏 脱敏则不校验格式
10 | */
11 | getIDCardInfo = (idCardNo: string, isEncrypt?: string) => {
12 | console.log('idcard', idCardNo)
13 |
14 | /**
15 | * 解析完成的信息对象
16 | */
17 | const msgObj = {
18 | /**
19 | * 是否合法
20 | */
21 | isValid: true,
22 | /**
23 | * 性别 1-男 0-女
24 | */
25 | sex: '1',
26 | /**
27 | * 年龄 number
28 | */
29 | age: 0,
30 | /**
31 | * 出生日期 格式 YYYY-MM-DD
32 | */
33 | birthday: '',
34 | }
35 |
36 | if (
37 | !/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(idCardNo) &&
38 | !isEncrypt
39 | ) {
40 | console.warn('into reg')
41 | msgObj.isValid = false
42 | return msgObj
43 | }
44 |
45 | const getCardInfos = (idNo: string) => {
46 | const cardInfos = {
47 | yearBirth: '',
48 | monthBirth: '',
49 | dayBirth: '',
50 | }
51 | if (idNo.length === 15) {
52 | cardInfos.yearBirth = `19${idNo.substring(6, 8)}`
53 | cardInfos.monthBirth = idNo.substring(8, 10)
54 | cardInfos.dayBirth = idNo.substring(10, 12)
55 | } else {
56 | cardInfos.yearBirth = idNo.substring(6, 10)
57 | cardInfos.monthBirth = idNo.substring(10, 12)
58 | cardInfos.dayBirth = idNo.substring(12, 14)
59 | }
60 | return cardInfos
61 | }
62 |
63 | // 获取用户身份证号码
64 | const userCard = idCardNo
65 |
66 | // 获取性别
67 | if (parseInt(userCard.substr(userCard.length - 2, 1)) % 2 === 1) {
68 | msgObj.sex = '1'
69 | } else {
70 | msgObj.sex = '0'
71 | }
72 | // 获取出生年月日
73 | const cardInfos = getCardInfos(userCard)
74 | if (
75 | Number(cardInfos.yearBirth) < 1900 ||
76 | Number(cardInfos.yearBirth) > new Date().getFullYear() ||
77 | Number(cardInfos.monthBirth) > 12 ||
78 | Number(cardInfos.dayBirth) > 31
79 | ) {
80 | msgObj.isValid = false
81 | }
82 | const yearBirth = cardInfos.yearBirth
83 | const monthBirth = cardInfos.monthBirth
84 | const dayBirth = cardInfos.dayBirth
85 | // 获取当前年月日并计算年龄
86 | const myDate = new Date()
87 | const monthNow = myDate.getMonth() + 1
88 | const dayNow = myDate.getDay()
89 | let age = myDate.getFullYear() - Number(yearBirth)
90 | if (
91 | monthNow < parseInt(monthBirth) ||
92 | (monthNow === parseInt(monthBirth) && dayNow < parseInt(dayBirth))
93 | ) {
94 | age--
95 | }
96 | // 得到年龄
97 | msgObj.age = age
98 | msgObj.birthday = `${yearBirth}-${monthBirth}-${dayBirth}`
99 |
100 | // 返回解析信息对象
101 | return msgObj
102 | }
103 | }
104 |
105 | export default new IDCard()
106 |
--------------------------------------------------------------------------------
/src/app.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 注意:此文件为编译时自动生成,如需修改入口文件请前往 build/template/app.tsx
3 | */
4 | import Taro, { Component, Config } from '@tarojs/taro'
5 | import { Provider } from '@tarojs/mobx'
6 | import Index from '~/pages/home/index'
7 | import store from '~/store'
8 | import { checkUpdate } from '~/utils/mp'
9 | import './app.scss'
10 | // 如果需要在 h5 环境中开启 React Devtools
11 | // 取消以下注释:
12 | // if (process.env.NODE_ENV !== 'production' && process.env.TARO_ENV === 'h5') {
13 | // require('nerv-devtools')
14 | // }
15 | // h5非生产环境添加vconsole
16 | if (process.env.TARO_ENV === 'h5' && process.env.NODE_ENV !== 'pro') {
17 | const VConsole = require('vconsole')
18 | new VConsole()
19 | }
20 | class App extends Component {
21 | /**
22 | * 指定config的类型声明为: Taro.Config
23 | *
24 | * 由于 typescript 对于 object 类型推导只能推出 Key 的基本类型
25 | * 对于像 navigationBarTextStyle: 'black' 这样的推导出的类型是 string
26 | * 提示和声明 navigationBarTextStyle: 'black' | 'white' 类型冲突, 需要显示声明类型
27 | */
28 | config: Config = {
29 | /**
30 | * 主包页面声明开始 注释用于判断开始行 勿动
31 | */
32 | pages: [
33 | 'pages/home/index',
34 | 'pages/common/imgPreview',
35 | 'pages/lab/FormValidate',
36 | 'pages/lab/comp',
37 | 'pages/lab/hooks',
38 | 'pages/lab/index',
39 | 'pages/user/index',
40 | ],
41 | subPackages: [], // 页面声明结束 注释用于判断结束行 勿动
42 | window: {
43 | backgroundTextStyle: 'light',
44 | navigationBarBackgroundColor: '#fff',
45 | navigationBarTitleText: 'WeChat',
46 | navigationBarTextStyle: 'black',
47 | },
48 | tabBar: {
49 | color: '#969BA0',
50 | selectedColor: '#333333',
51 | backgroundColor: '#ffffff',
52 | list: [
53 | {
54 | iconPath: 'assets/images/icon/icon_tabbar_goods_default.png',
55 | selectedIconPath: 'assets/images/icon/icon_tabbar_goods_selected.png',
56 | pagePath: 'pages/home/index',
57 | text: '首页',
58 | },
59 | {
60 | iconPath: 'assets/images/icon/icon_tabbar_goods_default.png',
61 | selectedIconPath: 'assets/images/icon/icon_tabbar_goods_selected.png',
62 | pagePath: 'pages/lab/index',
63 | text: '实验室',
64 | },
65 | {
66 | iconPath: 'assets/images/icon/icon_tabbar_goods_default.png',
67 | selectedIconPath: 'assets/images/icon/icon_tabbar_goods_selected.png',
68 | pagePath: 'pages/user/index',
69 | text: '我的',
70 | },
71 | ],
72 | },
73 | }
74 | componentDidShow() {
75 | // 检查更新
76 | checkUpdate()
77 | }
78 | componentDidCatchError(err) {
79 | console.error('catch error', err)
80 | console.error('catch error', 'catch error', err)
81 | console.log(123)
82 | }
83 | // 在 App 类中的 render() 函数没有实际作用
84 | // 请勿修改此函数
85 | render() {
86 | return (
87 |
88 |
89 |
90 | )
91 | }
92 | }
93 | Taro.render(, document.getElementById('app'))
94 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [ 'taro', '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 | 'error',
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 | // 全局变量配置 "readonly"-只读, "writable"-可写, "off"-不允许
90 | globals: {
91 | APP_CONF: 'readonly',
92 | },
93 | }
94 |
--------------------------------------------------------------------------------
/src/utils/router.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 跳转方法工具类
3 | */
4 |
5 | import Taro from '@tarojs/taro'
6 |
7 | interface IRoute {
8 | /**
9 | * 页面路径
10 | */
11 | url: string
12 | /**
13 | * 页面跳转参数
14 | */
15 | query?: {
16 | [key: string]: any
17 | }
18 | }
19 |
20 | interface IBack {
21 | delta?: number
22 | }
23 |
24 | class APPRouter {
25 | /**
26 | * 前进
27 | */
28 | navigateTo = (obj: IRoute) => {
29 | const url = this.joinUrl(obj)
30 | Taro.navigateTo({
31 | url,
32 | })
33 | }
34 |
35 | /**
36 | * 重定向
37 | */
38 | redirectTo = (obj: IRoute) => {
39 | const url = this.joinUrl(obj)
40 | Taro.redirectTo({
41 | url,
42 | })
43 | }
44 |
45 | /**
46 | * 切换到tab页面
47 | */
48 | switchTab = (obj: IRoute) => {
49 | const url = this.joinUrl(obj)
50 | Taro.switchTab({
51 | url,
52 | })
53 | }
54 |
55 | /**
56 | * 返回页面栈中的页面
57 | */
58 | navigateBack(obj: IBack) {
59 | if (process.env.TARO_ENV === 'weapp') {
60 | // 微信小程序
61 | Taro.navigateBack({
62 | delta: obj.delta || 1,
63 | })
64 | } else if (process.env.TARO_ENV === 'h5') {
65 | window.history.go(-(obj.delta || 1))
66 | }
67 | }
68 |
69 | /**
70 | * 重载应用并打开指定页面
71 | */
72 | reLaunch = (obj: IRoute) => {
73 | const url = this.joinUrl(obj)
74 |
75 | if (process.env.TARO_ENV === 'weapp') {
76 | Taro.reLaunch({
77 | url,
78 | })
79 | } else {
80 | const pages = Taro.getCurrentPages()
81 |
82 | // 要重载的页面是否在页面栈中已存在
83 | const existedIndex = pages.findIndex(item => {
84 | if (item) {
85 | const path = item.props.location.path
86 | return obj.url && obj.url.startsWith(path)
87 | }
88 | })
89 | // 如果存在则直接返回
90 | if (existedIndex > -1) {
91 | Taro.navigateBack({
92 | delta: pages.length - existedIndex - 1,
93 | })
94 | } else {
95 | // 否则replace
96 | const relaunchUrl = `${window.location.origin}${url.slice(1)}`
97 | window.location.replace(relaunchUrl)
98 | }
99 | }
100 | }
101 |
102 | /**
103 | * 拼接url
104 | */
105 | joinUrl(params: IRoute) {
106 | const { url, query } = params
107 | if (query && Object.keys(query).length) {
108 | const paramsStr = this.joinParams(query)
109 | return `${url}${paramsStr}`
110 | } else {
111 | return url
112 | }
113 | }
114 |
115 | /**
116 | * 将对象形式的参数拼接成字符串形式
117 | */
118 | joinParams(paramsObj) {
119 | // 对象不为空且属性数量大于0
120 | if (paramsObj && Object.keys(paramsObj).length > 0) {
121 | let paramStr = ''
122 | for (const key in paramsObj) {
123 | if (paramsObj.hasOwnProperty(key)) {
124 | const element = paramsObj[key]
125 | paramStr = `${paramStr}${paramStr ? '&' : '?'}${key}=${element}`
126 | }
127 | }
128 | return paramStr
129 | }
130 | return ''
131 | }
132 | }
133 |
134 | export default new APPRouter()
135 |
--------------------------------------------------------------------------------
/src/utils/validator.ts:
--------------------------------------------------------------------------------
1 | import Toast from '~/utils/toast'
2 | import IDCard from '~/utils/idcard'
3 |
4 | /**
5 | * 表单验证类
6 | */
7 | class FormValidator {
8 | /**
9 | * 验证函数列表 可直接使用
10 | */
11 | funcs = {
12 | /**
13 | * 非空验证
14 | */
15 | _notEmpty: (val) => val,
16 | /**
17 | * 手机号验证
18 | */
19 | _isMobile: (value: any) => /^1[23456789]\d{9}$/.test(value),
20 | /**
21 | * 身份证合法验证
22 | */
23 | _isIDCard: (value) => IDCard.getIDCardInfo(value).isValid,
24 | }
25 |
26 | /**
27 | * 预置的规则 可直接使用
28 | * 示例:
29 | ```
30 | validateRules = {
31 | phoneNumber: [
32 | Validator.rules._isMobile
33 | ],
34 | }
35 | ```
36 | */
37 | rules = {
38 | /**
39 | * 手机号验证规则
40 | */
41 | _isMobile: {
42 | test: this.funcs._isMobile,
43 | errMsg: '请输入正确的手机号',
44 | },
45 | /**
46 | * 身份证号验证规则
47 | */
48 | _isIDCard: {
49 | test: this.funcs._isIDCard,
50 | errMsg: '请输入正确的身份证号',
51 | },
52 | }
53 |
54 | /**
55 | * 表单验证方法
56 | * @param rules 验证规则数组
57 | * @param showToast 是否弹出错误信息
58 | * @param obj 属性集的父级对象
59 | */
60 | validate(
61 | rules: {
62 | [key: string]: Array<{
63 | /**
64 | * 验证规则
65 | */
66 | test: Function
67 | /**
68 | * 错误信息
69 | */
70 | errMsg: string
71 | }>
72 | },
73 | showToast: boolean,
74 | obj: any
75 | ): {
76 | /**
77 | * 是否验证通过
78 | */
79 | success: boolean
80 | /**
81 | * 错误信息
82 | */
83 | errMsg: string
84 | /**
85 | * 获取到的表单数据对象 验证通过时返回
86 | */
87 | formData?: any
88 | } {
89 | console.log('进入验证方法', rules)
90 |
91 | if (!obj) {
92 | console.error('请传入需要进行表单验证的对象')
93 | return {
94 | success: false,
95 | errMsg: `参数缺失,obj:${obj}`,
96 | }
97 | }
98 |
99 | const returnObj = {
100 | success: true,
101 | errMsg: 'ok',
102 | formData: {},
103 | }
104 | for (const key in rules) {
105 | if (rules.hasOwnProperty(key)) {
106 | const element = rules[key]
107 |
108 | console.log('each item', element)
109 |
110 | let tempErrMsg = ''
111 | element.forEach((item) => {
112 | if (item.test(obj[key])) {
113 | console.log('test success')
114 | returnObj.formData[key] = obj[key]
115 | } else {
116 | console.log('表单验证失败', item.errMsg)
117 | if (showToast) {
118 | Toast.error(item.errMsg)
119 | }
120 | tempErrMsg = item.errMsg
121 | throw new Error(item.errMsg)
122 | }
123 | })
124 | if (tempErrMsg) {
125 | return {
126 | success: false,
127 | errMsg: tempErrMsg,
128 | }
129 | }
130 | }
131 | }
132 | console.log('test all success', returnObj)
133 | return returnObj
134 | }
135 | }
136 |
137 | export default new FormValidator()
138 |
--------------------------------------------------------------------------------
/src/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.proceed(requestParams).then((res) => {
17 | console.log('data拦截器接收到的数据', res)
18 |
19 | // 先判断状态码
20 | if (res.statusCode !== 200) {
21 | // 错误处理
22 | console.error(`接口异常: ${res.data.path}`, res.statusCode)
23 | if (showToast) {
24 | Toast.error('很抱歉,数据临时丢失,请耐心等待修复')
25 | }
26 | return Promise.resolve('很抱歉,数据临时丢失,请耐心等待修复')
27 | }
28 |
29 | let resultData = { ...res.data }
30 |
31 | // 状态码为200时的错误处理
32 | // 这里主要是兼容多后台返回结果格式不规范以及后台框架设计存在问题的情况
33 | // 1. 返回状态码200 但返回结果是空字符串 在浏览器调试工具中查看不到任何信息
34 | if (!resultData) {
35 | throw `返回数据为空:${resultData}`;
36 | }
37 |
38 | console.log('into data handle', resultData)
39 |
40 | // 返回格式统一为 code data msg
41 | // 腾讯地图webservice接口返回格式统一
42 | if ( resType === 1 ) {
43 | resultData.code = resultData.status
44 | resultData.msg = resultData.message
45 | resultData.data = resultData.result
46 | }
47 |
48 | // 2. 统一返回格式
49 | // code 返回编码 强转字符串
50 | // msg 错误信息字符串 一般用于前端错误展示
51 | // data 返回数据
52 | resultData.code = resultData.hasOwnProperty('code') ? resultData.code.toString() : resultData.code
53 |
54 | console.error('resultData', resultData)
55 |
56 | // 3. 接口返回错误code时前端错误抛出
57 | // 4. 登录失效前端逻辑处理
58 | if (LOGIN_FAILURE_LIST.includes(resultData.code)) {
59 | console.error('into login falire')
60 | // const storageTimeStamp = Taro.getStorageSync('loginFailureTimeStamp')
61 | Taro.setStorageSync(Constants.LOGIN_FAILURE_TIMESTAMP, new Date().getTime())
62 | Taro.removeStorageSync(Constants.MASK_TOKEN)
63 | Taro.showToast({
64 | title: resultData.msg,
65 | icon: 'none',
66 | duration: 800
67 | })
68 | const curPages = Taro.getCurrentPages()
69 | console.error('taro.curPages', curPages)
70 | if (Page.getCurRoute() === Page.getRoutes().home) {
71 | setTimeout(() => {
72 | Taro.navigateTo({
73 | url: `/${Page.getRoutes().auth}?from=home`
74 | })
75 | }, 800);
76 | } else {
77 | setTimeout(() => {
78 | Taro.navigateTo({
79 | url: `/${Page.getRoutes().auth}`
80 | })
81 | }, 800);
82 | }
83 | } else if (!SUCC_LIST.includes(resultData.code) && showToast) {
84 | console.log('非登录失效的失败code', resultData)
85 | if (resultData.code === '50000') {
86 | Toast.error('系统开小差了')
87 | } else {
88 | Toast.error(resultData.msg)
89 | }
90 | }
91 | console.error('返回之前的resultData', resultData)
92 | return Promise.resolve(resultData)
93 | })
94 | .catch((err) => {
95 | Taro.hideLoading()
96 | Toast.error('网络开小差了')
97 | return Promise.reject(err)
98 | })
99 | }
100 |
--------------------------------------------------------------------------------
/src/pages/home/index.tsx:
--------------------------------------------------------------------------------
1 | import Taro, { Config, useState } from '@tarojs/taro'
2 | import { View, Button, Text, Input } from '@tarojs/components'
3 | import { observer } from '@tarojs/mobx'
4 | import { AtNoticebar, AtTag } from 'taro-ui'
5 | import {
6 | HdPaging,
7 | HdBackToTop,
8 | HdCard,
9 | HdCountdown,
10 | HdModal,
11 | HdNodata,
12 | HdTabs,
13 | } from 'taro-ui-hd'
14 |
15 | import Tabbar from '~/components/Tabbar/Tabbar'
16 | import counter from '~/store/counter'
17 | import QQMapWSService from '~/services/qqMap/ws.service'
18 | import LianouService from '~/services/root/drug.service'
19 | import './index.scss'
20 |
21 | export const Index = () => {
22 | const [testState, setTestState] = useState('')
23 | const [modalVisible, setModalVisible] = useState(false)
24 |
25 | const increment = () => {
26 | setTestState(`${testState}expand`)
27 | counter.increment()
28 | }
29 |
30 | const decrement = () => {
31 | counter.decrement()
32 | }
33 |
34 | const incrementAsync = () => {
35 | counter.incrementAsync()
36 | }
37 |
38 | // 手机号输入
39 | const handleInput = (type, e) => {
40 | console.log('type', type, e)
41 | }
42 |
43 | const handleJSONPTest = async () => {
44 | const result = await QQMapWSService.geocoder({
45 | location: `28.2532,112.87887`,
46 | get_poi: 0,
47 | })
48 | console.log('result', result)
49 | }
50 |
51 | const handleProxyText = async () => {
52 | const result = await LianouService.queryDiseaseByDrugName({
53 | ComName: '阿莫西林胶囊',
54 | })
55 | console.log('result', result)
56 | }
57 |
58 | const handleBackToTop = () => {}
59 |
60 | /**
61 | * 弹窗关闭
62 | */
63 | const handleModalClose = () => {
64 | setModalVisible(false)
65 | }
66 |
67 | /**
68 | * handleTabChange
69 | */
70 | const handleTabChange = e => {
71 | console.log('handleTabChange', e)
72 | }
73 |
74 | const handleOk = () => {
75 | setModalVisible(false)
76 | }
77 |
78 | return (
79 |
80 | 这是 NoticeBar 通告栏
81 | 标签
82 | handleInput('mobile', e)}
84 | type='number'
85 | placeholder='请输入手机号'
86 | />
87 | {/* */}
88 | {/* 归属地:{mobileText} */}
89 |
92 |
93 |
94 |
95 |
96 |
97 | {counter.counter}
98 |
99 |
100 | 这是卡片内容哦
101 |
102 |
112 | 这是弹窗内容
113 |
114 |
128 |
129 |
130 |
131 | )
132 | }
133 |
134 | Index.config = {
135 | navigationBarTitleText: '首页',
136 | } as Config
137 |
138 | export default observer(Index)
139 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = require('fs')
3 | const chalk = require('chalk')
4 | const plugins = require('./plugins/index')
5 |
6 | const config = {
7 | projectName: 'Taro2.x项目模板',
8 | date: '2020-3-10',
9 | designWidth: 750,
10 | deviceRatio: {
11 | 640: 2.34 / 2,
12 | 750: 1,
13 | 828: 1.81 / 2,
14 | },
15 | defineConstants: {},
16 | // 解析alias路径
17 | alias: {
18 | '~': path.resolve(__dirname, '..', 'src'),
19 | },
20 | sourceRoot: 'src',
21 | outputRoot: 'dist',
22 | // sass配置
23 | sass: {
24 | // 全局注入scss文件
25 | resource: [
26 | 'src/styles/classes.scss',
27 | 'src/styles/mixin.scss',
28 | 'src/styles/theme.scss',
29 | 'src/styles/var.scss',
30 | ],
31 | // 指定项目根目录,这样在resource字段中就不需要重复书写path.resolve了
32 | projectDirectory: path.resolve(__dirname, '..'),
33 | },
34 | uglify: {
35 | enable: true,
36 | // config: {
37 | // // 配置项同 https://github.com/mishoo/UglifyJS2#minify-options
38 | // }
39 | config: {
40 | nameCache: null, // or specify a name cache object
41 | toplevel: false,
42 | ie8: false,
43 | warnings: false,
44 | },
45 | },
46 | plugins: plugins,
47 | babel: {
48 | sourceMap: true,
49 | presets: [
50 | [
51 | 'env',
52 | {
53 | modules: false,
54 | },
55 | ],
56 | ],
57 | plugins: [
58 | 'transform-decorators-legacy',
59 | 'transform-class-properties',
60 | 'transform-object-rest-spread',
61 | [
62 | 'transform-runtime',
63 | {
64 | // async/await支持 替代taro1.x的tarojs/await
65 | helpers: false,
66 | polyfill: false,
67 | regenerator: true,
68 | moduleName: 'babel-runtime',
69 | },
70 | ],
71 | ],
72 | },
73 | mini: {
74 | postcss: {
75 | autoprefixer: {
76 | enable: true,
77 | config: {
78 | browsers: ['last 3 versions', 'Android >= 4.1', 'ios >= 8'],
79 | },
80 | },
81 | pxtransform: {
82 | enable: true,
83 | config: {},
84 | },
85 | url: {
86 | enable: true,
87 | config: {
88 | limit: 10240, // 本地图片转base64上限(单位byte)
89 | },
90 | },
91 | cssModules: {
92 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
93 | config: {
94 | namingPattern: 'module', // 转换模式,取值为 global/module
95 | generateScopedName: '[name]__[local]___[hash:base64:5]',
96 | },
97 | },
98 | },
99 | },
100 | h5: {
101 | publicPath: process.env.NODE_ENV === 'development' ? '/' : './',
102 | staticDirectory: 'static',
103 | router: {
104 | mode: 'browser', // 或者是 'hash'
105 | basename: '/taro-template', // 添加basesname为/h5后 使用taro路由跳转后的路径为 /h5/url 但在地址栏输入 url 和 /h5/url 都可以访问到对应的页面
106 | },
107 | // js文件名添加hash
108 | output: {
109 | filename: 'js/[name].[hash:8].js',
110 | chunkFilename: 'js/[name].[chunkhash:8].js',
111 | },
112 | // css文件名添加hash
113 | miniCssExtractPluginOption: {
114 | filename: 'css/[name].[hash:8].css',
115 | chunkFilename: 'css/[id].[chunkhash:8].css',
116 | },
117 | postcss: {
118 | autoprefixer: {
119 | enable: true,
120 | config: {
121 | browsers: ['last 3 versions', 'Android >= 4.1', 'ios >= 8'],
122 | },
123 | },
124 | cssModules: {
125 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
126 | config: {
127 | namingPattern: 'module', // 转换模式,取值为 global/module
128 | generateScopedName: '[name]__[local]___[hash:base64:5]',
129 | },
130 | },
131 | },
132 | // 配置需要额外的编译的源码模块 经过这一配置之后,代码中引入的处于 `node_modules/taro-ui/` 路径下的源码文件均会经过taro的编译处理。
133 | esnextModules: ['taro-ui'],
134 | },
135 | }
136 |
137 | module.exports = function (merge) {
138 | console.log('当前编译环境', process.env.BUILD_ENV)
139 |
140 | const BUILD_ENV = process.env.BUILD_ENV
141 |
142 | if (BUILD_ENV && !fs.existsSync(`config/${BUILD_ENV}.js`)) {
143 | console.error(
144 | chalk.red(
145 | `当前运行 ${BUILD_ENV} 环境,请先创建 config/${BUILD_ENV}.js 后重试,配置文件内容请参考 https://github.com/lexmin0412/taro-template/blob/master/README.md#启动本地调试`
146 | )
147 | )
148 | return
149 | }
150 |
151 | let currentConfig = {}
152 | switch (BUILD_ENV) {
153 | case 'local':
154 | currentConfig = require('./local')
155 | break
156 | case 'dev':
157 | currentConfig = require('./dev')
158 | break
159 | case 'test':
160 | currentConfig = require('./test')
161 | break
162 | case 'uat':
163 | currentConfig = require('./uat')
164 | break
165 | default:
166 | currentConfig = require('./pro')
167 | break
168 | }
169 | return merge({}, config, currentConfig)
170 | }
171 |
--------------------------------------------------------------------------------
/src/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 | console.log('hostconfig', APP_CONF)
16 |
17 | // 添加拦截器
18 | const getInterceptors = () => {
19 | return [
20 | urlInterceptor,
21 | headerInterceptor,
22 | paramInterceptor,
23 | dataInterceptor,
24 | delInterceptor,
25 | Taro.interceptors.logInterceptor,
26 | Taro.interceptors.timeoutInterceptor,
27 | ]
28 | }
29 | getInterceptors().forEach(interceptorItem =>
30 | Taro.addInterceptor(interceptorItem)
31 | )
32 |
33 | interface IOptions {
34 | hostKey: string
35 | [key: string]: any
36 | }
37 |
38 | interface IRequestConfig {
39 | url: string
40 | data?: any
41 | method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'UPLOAD'
42 | [key: string]: any
43 | }
44 |
45 | class BaseRequest {
46 | public options: IOptions
47 |
48 | constructor(options) {
49 | console.log('options', options)
50 | this.options = options
51 | }
52 |
53 | public async request({
54 | url,
55 | data,
56 | method,
57 | header = {
58 | 'Content-Type': 'application/json',
59 | },
60 | dataType = 'json',
61 | responseType = 'text',
62 | showToast = true,
63 | jsonp = false,
64 | crossHeaderInterceptor = false,
65 | resType = 0,
66 | }: IRequestConfig) {
67 | // 添加自定义请求头,用于host和header处理
68 | const hostKey = this.options ? this.options.hostKey : ''
69 | if (!hostKey) {
70 | throw '请指定service key'
71 | }
72 | const hostUrl = APP_CONF[hostKey]
73 | header[Constants.INTERCEPTOR_HEADER] = {
74 | hostKey,
75 | hostUrl,
76 | showToast,
77 | resType,
78 | crossHeaderInterceptor,
79 | }
80 |
81 | // UPLOAD方法特殊处理
82 | if (method === 'UPLOAD') {
83 | return new Promise((resolve, reject) => {
84 | return Taro.uploadFile({
85 | url: `${APP_CONF.API_HOST}/${url}`, //仅为示例,非真实的接口地址
86 | filePath: data,
87 | name: 'file',
88 | success(res) {
89 | const resultData = res.data
90 |
91 | console.log('uploadFile success', resultData)
92 | console.log('uploadFile success', JSON.parse(resultData))
93 | const result = JSON.parse(resultData)
94 | if (SUCC_LIST.includes(result.code)) {
95 | resolve(result)
96 | } else {
97 | toast.error(result.msg)
98 | reject(result)
99 | }
100 | },
101 | fail(err) {
102 | console.log('uploadFile err', err)
103 | reject(err)
104 | },
105 | })
106 | })
107 | } else {
108 | return Taro.request({
109 | url,
110 | data,
111 | method,
112 | header,
113 | dataType,
114 | responseType,
115 | jsonp,
116 | })
117 | }
118 | }
119 |
120 | public get(payload: {
121 | url: string
122 | data: any
123 | showToast?: boolean
124 | header?: any
125 | resType?: 1 | 0
126 | crossHeaderInterceptor?: boolean
127 | }) {
128 | return this.request({
129 | method: 'GET',
130 | ...payload,
131 | })
132 | }
133 |
134 | public post(payload: {
135 | url: string
136 | data: any
137 | showToast?: boolean
138 | header?: any
139 | resType?: 1 | 0
140 | crossHeaderInterceptor?: boolean
141 | }) {
142 | return this.request({
143 | method: 'POST',
144 | ...payload,
145 | })
146 | }
147 |
148 | public put(payload: {
149 | url: string
150 | data: any
151 | showToast?: boolean
152 | header?: any
153 | resType?: 1 | 0
154 | crossHeaderInterceptor?: boolean
155 | }) {
156 | return this.request({
157 | method: 'PUT',
158 | ...payload,
159 | })
160 | }
161 |
162 | public delete(payload: {
163 | url: string
164 | data: any
165 | showToast?: boolean
166 | header?: any
167 | resType?: 1 | 0
168 | crossHeaderInterceptor?: boolean
169 | }) {
170 | return this.request({
171 | method: 'DELETE',
172 | ...payload,
173 | })
174 | }
175 |
176 | public jsonp(payload: {
177 | url: string
178 | data: any
179 | showToast?: boolean
180 | header?: any
181 | resType?: 1 | 0
182 | crossHeaderInterceptor?: boolean
183 | }) {
184 | return this.request({
185 | method: 'GET',
186 | jsonp: true,
187 | ...payload,
188 | })
189 | }
190 |
191 | /**
192 | * 上传文件
193 | */
194 | public upload(payload: {
195 | url: string
196 | data: any
197 | showToast?: boolean
198 | header?: any
199 | resType?: 1 | 0
200 | crossHeaderInterceptor?: boolean
201 | }) {
202 | return this.request({
203 | ...payload,
204 | method: 'UPLOAD',
205 | header: {
206 | 'Content-Type': 'multipart/form-data',
207 | },
208 | })
209 | }
210 | }
211 |
212 | export default BaseRequest
213 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "taro-template",
3 | "version": "2.0.0",
4 | "private": true,
5 | "description": "基于 taro 2.x 的开箱即用多端项目模版",
6 | "templateInfo": {
7 | "name": "mobx",
8 | "typescript": true,
9 | "css": "sass"
10 | },
11 | "homepage": "https://github.com/lexmin0412/taro-template",
12 | "repository": {
13 | "type": "github",
14 | "url": "https://github.com/lexmin0412/taro-template",
15 | "branch": "2.x"
16 | },
17 | "bugs": {
18 | "url": "https://github.com/lexmin0412/taro-template/issues",
19 | "email": "zhangle_media@hotmail.com"
20 | },
21 | "keywords": [
22 | "taro",
23 | "template",
24 | "taro-template",
25 | "project-template",
26 | "react",
27 | "h5",
28 | "weapp"
29 | ],
30 | "author": "lexmin0412",
31 | "maintainers": [
32 | {
33 | "name": "lexmin0412",
34 | "email": "zhangle_media@hotmail.com",
35 | "url": "https://github.com/lexmin0412"
36 | }
37 | ],
38 | "contributors": [
39 | {
40 | "name": "lexmin0412",
41 | "email": "zhangle_media@hotmail.com",
42 | "url": "https://github.com/lexmin0412"
43 | }
44 | ],
45 | "engines": {
46 | "node": "12.18.0"
47 | },
48 | "bundledDependencies": [
49 | "@tarojs/taro"
50 | ],
51 | "typings": "global.d.ts",
52 | "markdown": "github",
53 | "license": "MIT",
54 | "scripts": {
55 | "dev:mp": "npm run build:mp -- --watch",
56 | "dev:mp-dev": "npm run build:mp-dev -- --watch",
57 | "dev:mp-test": "npm run build:mp-test -- --watch",
58 | "dev:mp-uat": "npm run build:mp-uat -- --watch",
59 | "dev:mp-pro": "npm run build:mp-pro --type weapp -- --watch",
60 | "dev:mp-local": "npm run build:mp-local --type weapp -- --watch",
61 | "build:mp": "cross-env BUILD_ENV=pro taro build --type weapp",
62 | "build:mp-dev": "cross-env BUILD_ENV=dev taro build --type weapp",
63 | "build:mp-test": "cross-env BUILD_ENV=test taro build --type weapp",
64 | "build:mp-uat": "cross-env BUILD_ENV=uat taro build --type weapp",
65 | "build:mp-pro": "cross-env BUILD_ENV=pro taro build --type weapp",
66 | "build:mp-local": "cross-env BUILD_ENV=local taro build --type weapp",
67 | "dev:h5": "npm run build:h5-local -- --watch",
68 | "dev:h5-local": "npm run build:h5-local -- --watch",
69 | "dev:h5-dev": "npm run build:h5-dev -- --watch",
70 | "dev:h5-test": "npm run build:h5-test -- --watch",
71 | "dev:h5-uat": "npm run build:h5-uat -- --watch",
72 | "dev:h5-pro": "npm run build:h5-pro -- --watch",
73 | "build:h5-local": "cross-env BUILD_ENV=local taro build --type h5",
74 | "build:h5-dev": "cross-env BUILD_ENV=dev taro build --type h5",
75 | "build:h5-test": "cross-env BUILD_ENV=test taro build --type h5",
76 | "build:h5-uat": "cross-env BUILD_ENV=uat taro build --type h5",
77 | "build:h5-pro": "cross-env BUILD_ENV=pro taro build --type h5"
78 | },
79 | "husky": {
80 | "hooks": {
81 | "pre-commit": "pretty-quick --staged && lint-staged",
82 | "commit-msg": "commitlint -e $HUSKY_GIT_PARAMS"
83 | }
84 | },
85 | "lint-staged": {
86 | "**/*.js": "eslint --ext .js",
87 | "src/**/*.ts": "eslint --ext .ts",
88 | "src/**/*.tsx": "eslint --ext .tsx",
89 | "src/**/*.scss": "stylelint --syntax scss && stylelint --fix scss"
90 | },
91 | "dependencies": {
92 | "@tarojs/components": "2.2.18",
93 | "@tarojs/components-qa": "2.2.18",
94 | "@tarojs/mobx": "2.2.18",
95 | "@tarojs/mobx-h5": "2.2.18",
96 | "@tarojs/plugin-sass": "2.2.18",
97 | "@tarojs/router": "2.2.18",
98 | "@tarojs/runner-utils": "2.2.18",
99 | "@tarojs/taro": "2.2.18",
100 | "@tarojs/taro-alipay": "2.2.18",
101 | "@tarojs/taro-h5": "2.2.18",
102 | "@tarojs/taro-qq": "2.2.18",
103 | "@tarojs/taro-quickapp": "2.2.18",
104 | "@tarojs/taro-swan": "2.2.18",
105 | "@tarojs/taro-tt": "2.2.18",
106 | "@tarojs/taro-weapp": "2.2.18",
107 | "babel-runtime": "^6.26.0",
108 | "clipboard": "^2.0.6",
109 | "dayjs": "^1.8.24",
110 | "mobx": "4.8.0",
111 | "nerv-devtools": "^1.5.7",
112 | "nervjs": "^1.5.7",
113 | "qrcode": "^1.4.4",
114 | "regenerator-runtime": "0.11.1",
115 | "taro-ui": "^2.3.1",
116 | "taro-ui-hd": "0.0.2",
117 | "wtils": "^0.2.0"
118 | },
119 | "devDependencies": {
120 | "@commitlint/cli": "^8.3.5",
121 | "@commitlint/config-conventional": "^8.3.4",
122 | "@tarojs/cli": "2.2.18",
123 | "@tarojs/mini-runner": "2.2.18",
124 | "@tarojs/plugin-uglify": "2.2.18",
125 | "@tarojs/webpack-runner": "2.2.18",
126 | "@tarox/plugin-check-env": "1.0.0-alpha.4",
127 | "@tarox/plugin-generate": "^0.0.1-alpha.1",
128 | "@tarox/plugin-init-app": "1.0.0-alpha.5",
129 | "@tarox/plugin-mp": "1.0.0-alpha.4",
130 | "@types/react": "^16.4.6",
131 | "@types/webpack-env": "^1.13.6",
132 | "@typescript-eslint/eslint-plugin": "^2.13.0",
133 | "@typescript-eslint/parser": "^2.13.0",
134 | "@youtils/prettier-config-standard": "^1.0.0-alpha.1",
135 | "babel-eslint": "^8.2.3",
136 | "babel-plugin-transform-class-properties": "^6.24.1",
137 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
138 | "babel-plugin-transform-jsx-stylesheet": "^0.6.5",
139 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
140 | "babel-plugin-transform-runtime": "^6.23.0",
141 | "babel-preset-env": "^1.6.1",
142 | "chalk": "^4.0.0",
143 | "cross-env": "^7.0.2",
144 | "eslint": "^6.8.0",
145 | "eslint-config-taro": "2.2.18",
146 | "eslint-plugin-import": "^2.12.0",
147 | "eslint-plugin-react": "^7.8.2",
148 | "eslint-plugin-react-hooks": "^1.6.1",
149 | "eslint-plugin-taro": "2.2.18",
150 | "husky": "^4.2.5",
151 | "lint-staged": "^10.0.9",
152 | "node-plop": "^0.26.0",
153 | "plop": "^2.6.0",
154 | "prettier": "^2.0.5",
155 | "pretty-quick": "^2.0.1",
156 | "standard": "^14.3.3",
157 | "stylelint": "^9.3.0",
158 | "stylelint-config-standard": "^20.0.0",
159 | "stylelint-config-taro-rn": "2.2.18",
160 | "stylelint-order": "^4.0.0",
161 | "stylelint-scss": "^3.17.1",
162 | "stylelint-taro-rn": "2.2.18",
163 | "typescript": "^3.0.1",
164 | "vconsole": "^3.3.4",
165 | "webpack-bundle-analyzer": "^3.6.1"
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/.stylelintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'stylelint-config-standard',
3 | plugins: [ 'stylelint-order' ],
4 | rules: {
5 | 'font-family-no-missing-generic-family-keyword': [
6 | true,
7 | {
8 | ignoreFontFamilies: [ 'iconfont' ],
9 | },
10 | ],
11 | 'no-descending-specificity': [
12 | true,
13 | [
14 | '.marketing-wheel-page .marketing-wheel-wheel .prize-list.prize-list-8 .prize-item',
15 | '.marketing-wheel-page .marketing-wheel-wheel .prize-list.prize-list-8 .prize-item .prize-item-inner',
16 | ],
17 | ], // 权重顺序
18 | 'at-rule-no-unknown': [
19 | // 不允许未知的@符号开头的规则 如 @test {}
20 | true, // 这里只能是true
21 | {
22 | ignoreAtRules: [ 'mixin', 'include', 'extend' ], // 忽略关键字 可以写正则
23 | },
24 | ],
25 | 'property-no-vendor-prefix': [
26 | true,
27 | {
28 | // 不允许兼容前缀
29 | ignoreProperties: [ 'box-orient', 'background-clip' ], // 忽略 因为没有名为box-orient的属性
30 | },
31 | ],
32 | 'order/order': [
33 | 'declarations',
34 | 'custom-properties',
35 | 'dollar-variables',
36 | 'rules',
37 | 'at-rules',
38 | ],
39 | 'order/properties-order': [
40 | 'position',
41 | 'z-index',
42 | 'top',
43 | 'bottom',
44 | 'left',
45 | 'right',
46 | 'float',
47 | 'clear',
48 | 'columns',
49 | 'columns-width',
50 | 'columns-count',
51 | 'column-rule',
52 | 'column-rule-width',
53 | 'column-rule-style',
54 | 'column-rule-color',
55 | 'column-fill',
56 | 'column-span',
57 | 'column-gap',
58 | 'display',
59 | 'grid',
60 | 'grid-template-rows',
61 | 'grid-template-columns',
62 | 'grid-template-areas',
63 | 'grid-auto-rows',
64 | 'grid-auto-columns',
65 | 'grid-auto-flow',
66 | 'grid-column-gap',
67 | 'grid-row-gap',
68 | 'grid-template',
69 | 'grid-template-rows',
70 | 'grid-template-columns',
71 | 'grid-template-areas',
72 | 'grid-gap',
73 | 'grid-row-gap',
74 | 'grid-column-gap',
75 | 'grid-area',
76 | 'grid-row-start',
77 | 'grid-row-end',
78 | 'grid-column-start',
79 | 'grid-column-end',
80 | 'grid-column',
81 | 'grid-column-start',
82 | 'grid-column-end',
83 | 'grid-row',
84 | 'grid-row-start',
85 | 'grid-row-end',
86 | 'flex',
87 | 'flex-grow',
88 | 'flex-shrink',
89 | 'flex-basis',
90 | 'flex-flow',
91 | 'flex-direction',
92 | 'flex-wrap',
93 | 'justify-content',
94 | 'align-content',
95 | 'align-items',
96 | 'align-self',
97 | 'order',
98 | 'table-layout',
99 | 'empty-cells',
100 | 'caption-side',
101 | 'border-collapse',
102 | 'border-spacing',
103 | 'list-style',
104 | 'list-style-type',
105 | 'list-style-position',
106 | 'list-style-image',
107 | 'ruby-align',
108 | 'ruby-merge',
109 | 'ruby-position',
110 | 'box-sizing',
111 | 'width',
112 | 'min-width',
113 | 'max-width',
114 | 'height',
115 | 'min-height',
116 | 'max-height',
117 | 'padding',
118 | 'padding-top',
119 | 'padding-right',
120 | 'padding-bottom',
121 | 'padding-left',
122 | 'margin',
123 | 'margin-top',
124 | 'margin-right',
125 | 'margin-bottom',
126 | 'margin-left',
127 | 'border',
128 | 'border-width',
129 | 'border-top-width',
130 | 'border-right-width',
131 | 'border-bottom-width',
132 | 'border-left-width',
133 | 'border-style',
134 | 'border-top-style',
135 | 'border-right-style',
136 | 'border-bottom-style',
137 | 'border-left-style',
138 | 'border-color',
139 | 'border-top-color',
140 | 'border-right-color',
141 | 'border-bottom-color',
142 | 'border-left-color',
143 | 'border-image',
144 | 'border-image-source',
145 | 'border-image-slice',
146 | 'border-image-width',
147 | 'border-image-outset',
148 | 'border-image-repeat',
149 | 'border-top',
150 | 'border-top-width',
151 | 'border-top-style',
152 | 'border-top-color',
153 | 'border-top',
154 | 'border-right-width',
155 | 'border-right-style',
156 | 'border-right-color',
157 | 'border-bottom',
158 | 'border-bottom-width',
159 | 'border-bottom-style',
160 | 'border-bottom-color',
161 | 'border-left',
162 | 'border-left-width',
163 | 'border-left-style',
164 | 'border-left-color',
165 | 'border-radius',
166 | 'border-top-right-radius',
167 | 'border-bottom-right-radius',
168 | 'border-bottom-left-radius',
169 | 'border-top-left-radius',
170 | 'outline',
171 | 'outline-width',
172 | 'outline-color',
173 | 'outline-style',
174 | 'outline-offset',
175 | 'overflow',
176 | 'overflow-x',
177 | 'overflow-y',
178 | 'resize',
179 | 'visibility',
180 | 'font',
181 | 'font-style',
182 | 'font-variant',
183 | 'font-weight',
184 | 'font-stretch',
185 | 'font-size',
186 | 'font-family',
187 | 'font-synthesis',
188 | 'font-size-adjust',
189 | 'font-kerning',
190 | 'line-height',
191 | 'text-align',
192 | 'text-align-last',
193 | 'vertical-align',
194 | 'text-overflow',
195 | 'text-justify',
196 | 'text-transform',
197 | 'text-indent',
198 | 'text-emphasis',
199 | 'text-emphasis-style',
200 | 'text-emphasis-color',
201 | 'text-emphasis-position',
202 | 'text-decoration',
203 | 'text-decoration-color',
204 | 'text-decoration-style',
205 | 'text-decoration-line',
206 | 'text-underline-position',
207 | 'text-shadow',
208 | 'white-space',
209 | 'overflow-wrap',
210 | 'word-wrap',
211 | 'word-break',
212 | 'line-break',
213 | 'hyphens',
214 | 'letter-spacing',
215 | 'word-spacing',
216 | 'quotes',
217 | 'tab-size',
218 | 'orphans',
219 | 'writing-mode',
220 | 'text-combine-upright',
221 | 'unicode-bidi',
222 | 'text-orientation',
223 | 'direction',
224 | 'text-rendering',
225 | 'font-feature-settings',
226 | 'font-language-override',
227 | 'image-rendering',
228 | 'image-orientation',
229 | 'image-resolution',
230 | 'shape-image-threshold',
231 | 'shape-outside',
232 | 'shape-margin',
233 | 'color',
234 | 'background',
235 | 'background-image',
236 | 'background-position',
237 | 'background-size',
238 | 'background-repeat',
239 | 'background-origin',
240 | 'background-clip',
241 | 'background-attachment',
242 | 'background-color',
243 | 'background-blend-mode',
244 | 'isolation',
245 | 'clip-path',
246 | 'mask',
247 | 'mask-image',
248 | 'mask-mode',
249 | 'mask-position',
250 | 'mask-size',
251 | 'mask-repeat',
252 | 'mask-origin',
253 | 'mask-clip',
254 | 'mask-composite',
255 | 'mask-type',
256 | 'filter',
257 | 'box-shadow',
258 | 'opacity',
259 | 'transform-style',
260 | 'transform',
261 | 'transform-box',
262 | 'transform-origin',
263 | 'perspective',
264 | 'perspective-origin',
265 | 'backface-visibility',
266 | 'transition',
267 | 'transition-property',
268 | 'transition-duration',
269 | 'transition-timing-function',
270 | 'transition-delay',
271 | 'animation',
272 | 'animation-name',
273 | 'animation-duration',
274 | 'animation-timing-function',
275 | 'animation-delay',
276 | 'animation-iteration-count',
277 | 'animation-direction',
278 | 'animation-fill-mode',
279 | 'animation-play-state',
280 | 'scroll-behavior',
281 | 'scroll-snap-type',
282 | 'scroll-snap-destination',
283 | 'scroll-snap-coordinate',
284 | 'cursor',
285 | 'touch-action',
286 | 'caret-color',
287 | 'ime-mode',
288 | 'object-fit',
289 | 'object-position',
290 | 'content',
291 | 'counter-reset',
292 | 'counter-increment',
293 | 'will-change',
294 | 'pointer-events',
295 | 'all',
296 | 'page-break-before',
297 | 'page-break-after',
298 | 'page-break-inside',
299 | 'widows',
300 | ],
301 | 'no-empty-source': null,
302 | 'number-leading-zero': 'never',
303 | 'number-no-trailing-zeros': true,
304 | 'length-zero-no-unit': true,
305 | 'value-list-comma-space-after': 'always',
306 | 'declaration-colon-space-after': 'always',
307 | 'value-list-max-empty-lines': 0,
308 | 'shorthand-property-no-redundant-values': true,
309 | 'declaration-block-no-duplicate-properties': true,
310 | 'declaration-block-semicolon-newline-after': 'always',
311 | 'block-closing-brace-newline-after': 'always',
312 | 'media-feature-colon-space-after': 'always',
313 | 'media-feature-range-operator-space-after': 'always',
314 | 'at-rule-name-space-after': 'always',
315 | indentation: 2,
316 | 'no-eol-whitespace': true,
317 | 'string-no-newline': null,
318 | },
319 | // ignoreFiles: ['**/mixin.scss'],
320 | }
321 |
--------------------------------------------------------------------------------
/src/pages/lab/comp.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 底层基础组件,用于其他页面和组件继承
3 | */
4 |
5 | import { ComponentType } from 'react'
6 | import Taro, { Component } from '@tarojs/taro'
7 | import { View, Button, Input } from '@tarojs/components'
8 | import { observer, inject } from '@tarojs/mobx'
9 |
10 | import Meta from '~/utils/meta'
11 | import Validator from '~/utils/validator'
12 | import {
13 | HdCard,
14 | HdNodata,
15 | HdPaging,
16 | HdModal,
17 | HdCountdown,
18 | HdTabs,
19 | } from 'taro-ui-hd'
20 |
21 | import './comp.scss'
22 | import toast from '~/utils/toast'
23 |
24 | /**
25 | * 页面props
26 | */
27 | type PageStateProps = {
28 | /**
29 | * 子元素
30 | */
31 | children?: any
32 | counter: any
33 | }
34 |
35 | /**
36 | * 页面state
37 | */
38 | type PageState = {
39 | type: string
40 | imageList: Array
41 | hasMore: boolean
42 | showPaging: boolean
43 | modalVisible: boolean
44 | modalType: 'center' | 'bottom'
45 | }
46 |
47 | interface Comp {
48 | props: PageStateProps
49 | state: PageState
50 | }
51 |
52 | @inject('counter')
53 | @observer
54 | class Comp extends Component {
55 | constructor(props) {
56 | super(props)
57 | Meta.setTitle('底层基础组件,用于其他页面和组件继承')
58 | this.state = {
59 | type: 'image',
60 | imageList: [
61 | 'https://ss0.bdstatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1584433500&di=b0d1428f12e1cdea17f4d8e667298aad&src=http://cdn2.image.apk.gfan.com/asdf/PImages/2014/12/26/211610_2d6bc9db3-77eb-4d80-9330-cd5e95fa091f.png',
62 | 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1584537201012&di=50279a8b6a931992f1610cac5653c469&imgtype=0&src=http%3A%2F%2Fb.hiphotos.baidu.com%2Fzhidao%2Fpic%2Fitem%2Fc75c10385343fbf233e9732cb27eca8064388ffc.jpg',
63 | 'https://ss0.bd2sdsdfsdtatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1584433500&di=b0d1428f12e1cdea17f4d8e667298aad&src=http://cdn2.image.apk.gfan.com/asdf/PImages/2014/12/26/211610_2d6bc9db3-77eb-4d80-9330-cd5e95fa091f.png',
64 | 'https://ss0.bdstatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1584433500&di=b0d1428f12e1cdea17f4d8e667298aad&src=http://cdn2.image.apk.gfan.com/asdf/PImages/2014/12/26/211610_2d6bc9db3-77eb-4d80-9330-cd5e95fa091f.png',
65 | 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1584537201012&di=50279a8b6a931992f1610cac5653c469&imgtype=0&src=http%3A%2F%2Fb.hiphotos.baidu.com%2Fzhidao%2Fpic%2Fitem%2Fc75c10385343fbf233e9732cb27eca8064388ffc.jpg',
66 | 'https://ss0.bd2sdsdfsdtatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1584433500&di=b0d1428f12e1cdea17f4d8e667298aad&src=http://cdn2.image.apk.gfan.com/asdf/PImages/2014/12/26/211610_2d6bc9db3-77eb-4d80-9330-cd5e95fa091f.png',
67 | 'https://ss0.bdstatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1584433500&di=b0d1428f12e1cdea17f4d8e667298aad&src=http://cdn2.image.apk.gfan.com/asdf/PImages/2014/12/26/211610_2d6bc9db3-77eb-4d80-9330-cd5e95fa091f.png',
68 | 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1584537201012&di=50279a8b6a931992f1610cac5653c469&imgtype=0&src=http%3A%2F%2Fb.hiphotos.baidu.com%2Fzhidao%2Fpic%2Fitem%2Fc75c10385343fbf233e9732cb27eca8064388ffc.jpg',
69 | 'https://ss0.bd2sdsdfsdtatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1584433500&di=b0d1428f12e1cdea17f4d8e667298aad&src=http://cdn2.image.apk.gfan.com/asdf/PImages/2014/12/26/211610_2d6bc9db3-77eb-4d80-9330-cd5e95fa091f.png',
70 | ],
71 | hasMore: true,
72 | showPaging: true,
73 | modalVisible: false,
74 | modalType: 'center',
75 | }
76 | }
77 |
78 | // 监听mobx状态变化
79 | componentWillReact() {
80 | console.log('componentWillReact', this.props)
81 | }
82 |
83 | componentWillMount() {
84 | this.setState({
85 | type: this.$router.params.type,
86 | })
87 | }
88 |
89 | onReachBottom() {
90 | console.log('reachBottom')
91 | const { showPaging } = this.state
92 | if (!showPaging) {
93 | this.setState({
94 | showPaging: true,
95 | })
96 | }
97 | this.queryData()
98 | }
99 |
100 | queryData() {
101 | const { imageList } = this.state
102 | setTimeout(() => {
103 | if (imageList.length <= 20) {
104 | this.setState({
105 | imageList: this.state.imageList.concat([
106 | 'https://ss0.bdstatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1584433500&di=b0d1428f12e1cdea17f4d8e667298aad&src=http://cdn2.image.apk.gfan.com/asdf/PImages/2014/12/26/211610_2d6bc9db3-77eb-4d80-9330-cd5e95fa091f.png',
107 | 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1584537201012&di=50279a8b6a931992f1610cac5653c469&imgtype=0&src=http%3A%2F%2Fb.hiphotos.baidu.com%2Fzhidao%2Fpic%2Fitem%2Fc75c10385343fbf233e9732cb27eca8064388ffc.jpg',
108 | 'https://ss0.bd2sdsdfsdtatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1584433500&di=b0d1428f12e1cdea17f4d8e667298aad&src=http://cdn2.image.apk.gfan.com/asdf/PImages/2014/12/26/211610_2d6bc9db3-77eb-4d80-9330-cd5e95fa091f.png',
109 | ]),
110 | })
111 | } else {
112 | this.setState({
113 | hasMore: false,
114 | })
115 | }
116 | }, 1000)
117 | }
118 |
119 | /**
120 | * 展示弹窗
121 | * @param type 类型
122 | */
123 | showModal(type) {
124 | this.setState({
125 | modalVisible: true,
126 | modalType: type,
127 | })
128 | }
129 |
130 | /**
131 | * 图片数组变更回调
132 | */
133 | handleImgListChange(list) {
134 | console.log('comp page list', list)
135 | this.setState({
136 | imgList: list,
137 | })
138 | }
139 |
140 | /**
141 | * 测试变表单验证方法
142 | */
143 | handleValidate() {
144 | const funcs = Validator.funcs
145 | const validResult = Validator.validate(
146 | {
147 | phone: [
148 | {
149 | errMsg: '请输入手机号',
150 | test: funcs._notEmpty,
151 | },
152 | {
153 | errMsg: '测试单字段多验证规则提示',
154 | test: val => val.length === 11,
155 | },
156 | ],
157 | address: [
158 | {
159 | errMsg: '请输入地址',
160 | test: funcs._notEmpty,
161 | },
162 | ],
163 | },
164 | true,
165 | this.state
166 | )
167 | if (validResult.success) {
168 | toast.show('验证成功')
169 | } else {
170 | console.error('validResult', validResult)
171 | }
172 | }
173 |
174 | handleInput(type, e) {
175 | this.setState({
176 | [type]: e.detail.value,
177 | })
178 | }
179 |
180 | // tab标签页切换
181 | handleTabChange(e) {
182 | console.log('handleTabChange', e)
183 | }
184 |
185 | render() {
186 | const { type, imageList, hasMore, modalVisible, modalType } = this.state
187 | return (
188 |
189 | 组件演示
190 |
191 | {type === 'card' ||
192 | (type === 'paging' && (
193 |
194 | 卡片组件
195 | {imageList.map(item => {
196 | return {item}
197 | })}
198 |
199 | ))}
200 |
201 | {type === 'default' && (
202 |
203 | 缺省组件
204 |
205 |
206 | )}
207 |
208 | {type === 'paging' && (
209 |
210 | 缺省组件
211 |
212 |
213 | )}
214 |
215 | {type === 'modal' && (
216 |
217 | 弹窗组件
218 |
221 |
224 |
229 |
230 | 这是内容这是内容这是内容这是内
231 | 容这是内容这是内容这是内容这是内容这是内容这是内容
232 |
233 |
234 |
235 | )}
236 |
237 | {type === 'countdown' && (
238 |
239 | 倒计时组件
240 |
245 |
246 | )}
247 |
248 | {type === 'formValidate' && (
249 |
250 | 表单验证演示
251 |
252 |
257 |
262 |
263 | )}
264 |
265 | {type === 'tabs' && (
266 |
267 | 标签页组件演示
268 |
282 |
283 | )}
284 |
285 | {/* */}
286 | {/* */}
287 |
288 | )
289 | }
290 | }
291 |
292 | export default Comp as ComponentType
293 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Taro 2.x 项目模版
2 |
3 | > 说明:当前仓库长期维护基于 Taro2.x 的模版,同时还提供了 1.x 和 3.x 版本(基于React)可供选择。
4 | >
5 | > [1.x 版本模版点此前往](https://github.com/lexmin0412/taro-template/tree/1.x/)
6 | >
7 | > 3.x 版本为独立仓库,[点此前往](https://github.com/lexmin0412/taro3-react-template)
8 |
9 | ## 相关项目
10 |
11 | 基于这个模版,开发了 [taro-xui](https://github.com/lexmin0412/taro-xui) 这个 UI 库,而在这个模版中又引用了 taro-xui 作为首页的演示 demo,emm...
12 |
13 | 禁止套娃...
14 |
15 | ## 导航
16 |
17 | - [功能列表](#功能列表)
18 | - [项目结构](#项目结构)
19 | - [开始](#开始)
20 | - [开发](#开发)
21 | - [编译](#编译)
22 | - [预置功能](#预置功能)
23 | - [基础开发](#基础开发)
24 | - [请求数据](#请求数据)
25 | - [创建 service](#创建service)
26 | - [直接调用 service 获取数据](#直接调用service获取数据)
27 | - [组件](#组件)
28 | - [1.定义组件](#1.定义组件)
29 | - [2.在页面中引用](#在页面中引用)
30 | - [开发规范](#开发规范)
31 | - [ESLint](#ESLint)
32 | - [静态资源导入规范](#静态资源导入规范)
33 | - [类名规范](#类名规范)
34 | - [代码/性能优化](#代码/性能优化)
35 | - [部署](#部署)
36 | - [技术栈](#技术栈)
37 | - [后续工作](#后续工作)
38 | - [问题记录](#问题记录)
39 | - [Taro 升级问题](#Taro升级问题)
40 | - [其他](#其他)
41 | - [优化](#优化)
42 | - [taro-ui 样式引入](#taro-ui样式引入)
43 | - [官方优化指南](#官方优化指南)
44 |
45 | ## 功能列表
46 |
47 | - 基础功能支持
48 | - [x] TypeScript
49 | - [x] Sass,全局注入公用样式文件
50 | - [x] UI 库(taro-ui)
51 | - [x] 状态管理(mobx)
52 | - [x] 异步编程(async/await)
53 | - [x] 引入字体(iconfont)
54 | - 接口请求
55 | - [x] request 类
56 | - [x] 拦截器
57 | - [x] url 拦截器
58 | - [x] header 拦截器
59 | - [x] param 拦截器
60 | - [x] data 拦截器
61 | - [x] 开发环境本地代理(h5 端)
62 | - [x] jsonp 支持(h5 端)
63 | - 调试
64 | - [x] vconsole(h5 端)
65 | - 工程化
66 | - [x] 全局变量
67 | - [x] 插件
68 | - [x] [环境变量检查 - taro-plugin-check-env](https://github.com/lexmin0412/taro-plugin-check-env)
69 | - [x] [pages/components 文件扫描,入口文件初始化 - taro-plugin-init-app](https://github.com/lexmin0412/taro-plugin-init-app)
70 | - [x] [根据不同的环境变量生成不同的 project.config.json(小程序端) - taro-plugin-mp](https://github.com/lexmin0412/taro-plugin-mp)
71 | - [x] 通过 plop 插件一键生成模版文件(页面、组件、样式、服务类、mobx 状态管理)
72 | - [x] 底层组件,用于页面和组件继承,实现类似 vue 原型绑定的功能
73 | - [x] git hooks
74 | - [x] eslint
75 | - [x] stylelint
76 | - [x] prettier
77 | - [x] commit lint
78 | - [x] 引入自建组件库([taro-xui](https://github.com/lexmin0412/taro-xui))
79 | - [x] 引入自建工具类库(wtils)
80 | - [ ] 接入 Taro 模版源
81 | - [ ] 提交 Taro 物料市场
82 | - 组件
83 | - [x] Card 卡片组件 提供圆角、阴影功能,可自定义类名、样式(圆角及内外边距)
84 | - [x] Countdown 倒计时组件,可自定义结束时间、自定义倒计时长、是否展示天,自定义 item 样式
85 | - [x] Divider 分割线,可自定义高度
86 | - [x] Nodata 缺省组件 可自定义图片、文字、宽高
87 | - [x] Paging 分页提示组件 将 scrollerLoader, scrollerEndMessage 合并成一个组件,减少判断
88 | - [x] Modal 基础弹窗组件,可选择弹窗位置,包括中间弹窗、底部弹窗,抛出关闭回调
89 | - [x] Tabs 标签页
90 | - [x] TButton 按钮组件,可自定义类名、自定义宽高、背景色、圆角、positionType
91 | - [x] TImage 图片组件 提供错误处理、loading 过渡、查看大图等功能
92 | - [x] TImageUploader 图片上传组件 基于 image 提供上传图片、图片数量限制、删除图片、查看大图等功能
93 | - 工具类
94 | - [x] img.ts 图片处理类(如拼接 url、预览等)
95 | - [x] mp.ts 小程序独有 api 封装(如检查更新)
96 | - [x] page.ts 页面工具类,实现获取页面路由、跳转等功能
97 | - [x] toast.ts loading/toast api 封装简化
98 | - [x] validator.ts 表单验证
99 | - [x] meta.ts meta 相关功能
100 | - 体验工程
101 | - [ ] 骨架屏
102 |
103 | ## 项目结构
104 |
105 | 以下是项目结构的缩略图
106 |
107 | 
108 |
109 | ## 开始
110 |
111 | ```zsh
112 | # 获取模版
113 | git clone https://github.com/lexmin0412/taro-template.git
114 | # 进入项目文件夹
115 | cd taro_template
116 | # 安装依赖
117 | yarn
118 | # 本地浏览器运行
119 | yarn dev:h5
120 | # 本地小程序运行
121 | yarn dev:mp
122 | ```
123 |
124 | ## 开发
125 |
126 | ### 编译
127 |
128 | 为了提高开发体验、调整了默认 taro 模版的部分编译命令,也为不同服务器环境(包括 dev/sit/uat/pro)、不同编译模式(开发/打包)、不同运行环境(h5/小程序)提供了统一的命令。
129 |
130 | 新增的服务器环境参数,主要是考虑到在处理线上问题时,为了复现问题,经常需要在本地请求非开发环境的接口,这时候习惯的操作是去更改配置文件,而更改配置文件的风险是很高的,不仅操作繁琐,更容易在多人开发时造成冲突,甚至可能将测试的变量提交到生产环境,造成不必要的线上问题。
131 |
132 | 编译命令格式如下:
133 |
134 | ```shell
135 | yarn :-
136 | ```
137 |
138 | `mode` ,编译模式:
139 |
140 | - dev 本地开发
141 | - build 服务器部署
142 |
143 | `platform` ,运行环境
144 |
145 | - mp 微信小程序
146 | - h5 h5
147 |
148 | `env` ,服务器环境标识,不同标识对应着不同的配置项,如接口 host
149 |
150 | - sit 测试环境
151 | - uat 预发环境
152 | - pro 生产环境
153 | - 空 开发环境
154 |
155 | 示例:
156 |
157 | ```shell
158 | yarn dev:mp # 本地开发 小程序 开发环境
159 | yarn dev:mp-sit # 本地开发 小程序 测试环境
160 | yarn build:mp # 部署 小程序 开发环境
161 | yarn build-mo-pro # 部署 小程序 生产环境
162 | ```
163 |
164 | 完整的编译命令列表详见 [package.json](./package.json) 中的 `scripts` 配置项。
165 |
166 | ### 启动本地调试
167 |
168 | 启动本地调试模式需要新建 config/local.js,参考如下内容:
169 |
170 | ```js
171 | /**
172 | * 本地调试配置
173 | * 默认使用开发环境
174 | * 运行 yarn dev:h5命令
175 | */
176 |
177 | module.exports = {
178 | defineConstants: {
179 | APP_CONF: {
180 | API_HOST: JSON.stringify('https://xx.com/'),
181 | APPID: JSON.stringify('this_is_my_tourist_appid'),
182 | API_MAP_QQ: JSON.stringify('https://apis.map.qq.com'),
183 | KEY_MAP_QQ: JSON.stringify('UQPBZ-RCU36-K2YS3-EMV6Y-JI6JJ-3WBUM'),
184 | },
185 | },
186 | }
187 | ```
188 |
189 | ### 预置功能
190 |
191 | 在开发阶段,为了减少一些重复且枯燥,还有可能造成报错的代码,做了以下几个工作:
192 |
193 | - 通过命令生成文件
194 | - pages 和 components 文件夹的扫描
195 | - 公用 sass 文件的全局注册
196 |
197 | 1. **通过命令生成文件**
198 |
199 | 对于文件的新建操作,在项目中也预置了命令,开发者只需在命令行中输入 `yarn template` ,然后根据相关提示输入对应的配置项,即可生成对应的文件,目前支持以下四种文件的快捷创建:
200 |
201 | - 页面(同时生成对应的 scss 和 ts 类型生命文件)
202 | - 组件(同时生成对应的 scss 文件)
203 | - mobx 模块
204 | - service 类
205 |
206 | 2. pages 和 components 文件夹的扫描
207 |
208 | 在平常的项目开发中,存在以下问题:
209 |
210 | - 每新建一个页面文件,就需要在 app.tsx 中的 pages 配置项中追加一行;
211 | - 每一个组件的引用,都需要另起一行,引用到具体的组件存放路径
212 |
213 | pages 文件夹的扫描,是基于 pages 目录及其所有文件夹及文件夹下的文件名,生成一个路由文件 routes.js,再读取这个路由文件,追加到 app.tsx 模版文件的 pages 配置项中去。
214 |
215 | components 文件夹的扫描,跟 pages 目录同理,但生成的是一个 index.ts 文件,自动引入了 components 文件夹下的所有组件并导出,这样做的目的是在多个组件引用时,不需要每一个组件的引用都另起一行,而可以通过如下方式书写:
216 |
217 | ```tsx
218 | import { Line, TImage } from '~/components
219 | ```
220 |
221 | 3. 公用 sass 文件的全局注册
222 |
223 | 在 config/index.js 中预置了如下内容:
224 |
225 | ```js
226 | // config/index.js
227 | {
228 | sass: {
229 | // 全局注入scss文件
230 | resource: [
231 | 'src/styles/classes.scss',
232 | 'src/styles/mixin.scss',
233 | 'src/styles/theme.scss',
234 | 'src/styles/var.scss'
235 | ],
236 | // 指定项目根目录,这样在resource字段中就不需要重复书写path.resolve了
237 | projectDirectory: path.resolve(__dirname, '..')
238 | },
239 | }
240 | ```
241 |
242 | 作用是全局注入了 mixin.scss 和 theme.scss,这样做之后,在项目内的所有 scss 文件中,可以直接使用这两个文件中的所有特性而不需要引入对应的文件,如果有更多的公用文件注入,只需要修改这里的配置项即可(注意:修改后需要重启项目才能生效)。(**TODO:后续需要在编译插件中扫描 styles 文件夹,省去配置项追加的操作**)
243 |
244 | ```scss
245 | // pages/index/index.scss
246 | .index-page {
247 | background-color: $body-bg; // 变量来自styles/theme.scss
248 |
249 | .block-title {
250 | @include textOrient(1); // textOrient来自styles/
251 | }
252 | }
253 | ```
254 |
255 | ### 基础开发
256 |
257 | #### 请求数据
258 |
259 | 在页面中请求数据,需要先做一个判断:当前这个接口的数据需不需要跨页面共享,如果不需要,那么就没有必要经过 dva,直接调用 service 即可;反之则需要定义 model , 在页面上发起 action, 走 dva 的流程。
260 |
261 | ##### 创建 service
262 |
263 | service, 也就是我们的服务模块,用于统一存放后端接口定义,供页面调用。
264 |
265 | ** `service` 文件设计规范**
266 |
267 | 由于同一个接口被不同页面调用调用的可能性非常高,服务模块的结构需要依照后端接口来设计,如项目内既包含了公司后端项目的接口请求,又需要请求第三方接口,那么 service 模块就要分成两个大的模块,大的模块下面再根据接口模块划分来划分小的 service 文件。
268 |
269 | 如一个接口路径为 `https://xxx.normal.com/webapi/account/queryBalanceAccount` , 用途是查询用户账户余额,那么这个接口在 service 模块的结构就应该表现为:
270 |
271 | 首先分为两个大的模块,下一层是后台的项目,最后根据后台接口模块命名一个 `xxx.service.ts` , xxx 是后台的模块名称。只要一个接口是在后台接口项目中的这个子模块,那么在前端就应该定义在相应的 service 文件下。
272 |
273 | 上面的示例接口设计结构如下:
274 |
275 | ```bash
276 | ├── services 服务根文件夹
277 | | ├── inside 内部服务
278 | | ├── qqMap 腾讯地图api接口
279 | | ├── ws.service.ts webservice服务
280 | ```
281 |
282 | ##### 直接调用 service 获取数据
283 |
284 | ```tsx
285 | import QQMapWSService from '~/services/qqMap/ws.service'
286 |
287 | class Index extends Component {
288 | state = {}
289 |
290 | componentDidMount() {
291 | this.handleJSONPTest()
292 | }
293 |
294 | // 直接调用service
295 | async handleJSONPTest() {
296 | const result = await QQMapWSService.geocoder({
297 | location: `28.2532,112.87887`,
298 | get_poi: 0,
299 | })
300 | this.setState({
301 | locationData: result.data,
302 | })
303 | console.log('result', result)
304 | }
305 |
306 | render() {
307 | const { locationData } = this.state
308 | return (
309 |
310 | {locationData && (
311 |
312 | 当前位置:{locationData.latitude},{locationData.longitude}
313 |
314 | )}
315 |
316 | )
317 | }
318 | }
319 | ```
320 |
321 | #### 组件
322 |
323 | 在业务开发的过程中,我们常需要复用一些相同的结构,如商品轮播图,订单 item 等,如果每个页面都复制粘贴一遍,不仅不美观,更难以维护,这时候就需要开发组件了。
324 |
325 | 组件分为展示型组件和容器型组件。展示型组件只需要接收父组件传递的属性并渲染页面,容器型组件则会涉及到数据处理等复杂的逻辑,难以重用,所以平常我们开发的一般都是展示型的组件。
326 |
327 | 在项目模板中已经包含了数个常用的基础组件,可直接使用,引用方式:
328 |
329 | ```tsx
330 | import { Card, TImage } from '~/components'
331 | ```
332 |
333 | 在编译前已经进行了 components 文件夹的扫描操作,自动生成了 components/index.ts,而 `~/components` 会指向 src/components/index.ts 文件,所以可以直接通过以上方式引用。
334 |
335 | ##### 1. 定义组件
336 |
337 | 通过 yarn template 命令新建组件,会生成如下模版:
338 |
339 | ```tsx
340 | /**
341 | * ComponentDesc
342 | */
343 |
344 | import { ComponentClass } from 'react'
345 | import Taro, { Component } from '@tarojs/taro'
346 | import { View } from '@tarojs/components'
347 |
348 | import './ComponentName.scss'
349 |
350 | /**
351 | * props属性
352 | */
353 | interface IProps {
354 | /**
355 | * 子元素
356 | */
357 | children?: any
358 | }
359 |
360 | /**
361 | * 组件内部属性
362 | */
363 | interface IState {}
364 |
365 | interface ComponentName {
366 | props: IProps
367 | state: IState
368 | }
369 |
370 | class ComponentName extends Component {
371 | static defaultProps: IProps = {}
372 |
373 | render() {
374 | return ComponentDesc
375 | }
376 | }
377 |
378 | export default ComponentName as ComponentClass
379 | ```
380 |
381 | 基于以上模版,我们就可以开始组件的具体逻辑开发了。
382 |
383 | ##### 2. 在页面中引用
384 |
385 | ```tsx
386 | import { ComponentName } from '~/components'
387 | ```
388 |
389 | ### 开发规范
390 |
391 | #### ESLint
392 |
393 | 代码书写规范请遵循 [Taro 规范](http://taro-docs.jd.com/taro/docs/spec-for-taro.html),后续会有更完善的规范补充。
394 |
395 | #### 静态资源导入规范
396 |
397 | 一个页面文件导入模块时应该按照如下规范:
398 |
399 | 1. 先导入第三方模块,如第三方UI库等
400 | 2. 再导入项目内部模块,如组件、工具类等
401 | 3. 导入静态文件,图片在前,其他资源次之,样式文件最后
402 |
403 | 示例:
404 |
405 | ```tsx
406 | // 导入第三方库
407 | import Taro, { Component, Config } from '@tarojs/taro'
408 | import { View, Text } from '@tarojs/components'
409 | import { connect } from '@tarojs/redux'
410 | import { ComponentClass } from 'react'
411 |
412 | // 导入项目内部模块
413 | import Line from '~/components/Line'
414 | import Toast from '~/utils/toast'
415 |
416 | // 导入静态文件和样式
417 | import './index.scss'
418 | ```
419 |
420 | #### 类名规范
421 |
422 | - 页面容器应以模块-文件名-容器类型命名,如 home-index-page, line-comp 等
423 |
424 | - 样式
425 |
426 | ```scss
427 | .index-page {
428 | background: $theme-color;
429 | }
430 | ```
431 |
432 | ### 代码/性能优化
433 |
434 | 请基于以下文档优化:
435 |
436 | - [Taro 最佳实践](http://taro-docs.jd.com/taro/docs/best-practice.html)
437 | - [Render Props](http://taro-docs.jd.com/taro/docs/render-props.html)
438 | - [Taro 性能优化实践](http://taro-docs.jd.com/taro/docs/optimized-practice.html)
439 |
440 | ## 部署
441 |
442 | 根据 [package.json](./package.json) 文件中的 scripts 配置项,在对应的自动化部署工具(如 jenkins)中进行相应的配置即可。编译命令说明 [详见](#编译)
443 |
444 | ## 技术栈
445 |
446 | - [taro](https://nervjs.github.io/taro/docs/README.html)
447 | - [taro-ui](https://taro-ui.aotu.io/)
448 | - [mobx](https://cn.mobx.js.org/)
449 | - [typescript](https://www.tslang.cn/docs/handbook/basic-types.html)
450 | - [scss](https://www.sass.hk/)
451 |
452 | ## 问题记录
453 |
454 | ### Taro 升级问题
455 |
456 | #### 使用 async/await 在小程序中报 `regeneratorRuntime is not defined`
457 |
458 | 原因:没有 async await 支持
459 |
460 | 解决方案:
461 |
462 | -
463 | -
464 |
465 | ### 其他
466 |
467 | #### 小程序中无法识别类型声明独立文件的操作
468 |
469 | 解决方案:不将类型声明独立文件,此问题需要后续观察。
470 |
471 | #### h5 中 调用 chooseImage,点击取消按钮无法不会进入 success fail complete 中的任何一个回调
472 |
473 | 解决方案:loading 放在 success 回调中 上传之前开始展示
474 |
475 | ## 优化
476 |
477 | ### taro-ui 样式引入
478 |
479 | ```scss
480 | // app.scss
481 |
482 | // 方式1: 一次性引入所有样式
483 | @import '~taro-ui/dist/style/index.scss';
484 |
485 | // 方式2: 在使用到新的组件时才引入
486 | @import '~taro-ui/dist/style/components/noticeBar.scss';
487 | @import '~taro-ui/dist/style/components/tag.scss';
488 | ```
489 |
490 | 对于上面的情况,如果在项目中只使用到了 taro-ui 中的 Button 和 Tag 组件,打包后的 app.css 体积从 210kb 减少到 53kb,只要打包后生成的 app.css 文件小于 210kb,那么这种引入方式就是值得的。
491 |
492 | ## 项目文档
493 |
494 | - [常见问题记录](./problems.md)
495 |
--------------------------------------------------------------------------------