├── 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 | ![项目结构](./docs/structure.png) 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 | --------------------------------------------------------------------------------