├── .editorconfig
├── .env.development
├── .env.production
├── .env.staging
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .gitlab-ci.yml
├── .npmrc
├── README.md
├── babel.config.js
├── config
└── env.js
├── gulpfile.js
├── gulp-px2rpx
│ └── index.js
├── gulp-wx-babel
│ ├── babel-plugins.js
│ └── index.js
└── index.js
├── package.json
├── project.config.json.example
├── script
├── upload.js
└── upload.key
├── src
├── app.js
├── app.json
├── app.scss
├── constants
│ └── index.js
├── models
│ ├── record.d.ts
│ ├── record.js
│ └── user.js
├── pages
│ └── root
│ │ └── home
│ │ ├── home.js
│ │ ├── home.json
│ │ ├── home.scss
│ │ └── home.wxml
├── static
│ ├── empty.png
│ ├── logo.png
│ └── refresh.png
├── stores
│ ├── auth-store.js
│ ├── helper
│ │ ├── collection.js
│ │ ├── observable.js
│ │ └── simple-store.js
│ ├── index.js
│ ├── observer.js
│ └── ui-store.js
├── styles
│ ├── colors.scss
│ ├── flex.scss
│ └── mixins.scss
└── utils
│ ├── check-api-auth.js
│ ├── decoder.js
│ ├── index.js
│ ├── merge-share-message.js
│ ├── nav.js
│ ├── prompt.js
│ ├── random.js
│ ├── request.js
│ ├── save-files.js
│ ├── sleep.js
│ ├── to-json-deep.js
│ ├── upload-files.js
│ └── wxp.js
├── webpack.config.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
2 | APP_ENV=development
3 | APP_API_HOST=https://fondwell.beansmile-dev.com
4 |
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | NODE_ENV=production
2 | APP_ENV=production
3 | APP_API_HOST=https://fondwell.beansmile-dev.com
4 |
--------------------------------------------------------------------------------
/.env.staging:
--------------------------------------------------------------------------------
1 | NODE_ENV=production
2 | APP_ENV=staging
3 | APP_API_HOST=https://fondwell.beansmile-dev.com
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # eslint 白名单
2 | dist/*
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "beansmile/weapp"
4 | ],
5 | "globals": {
6 | "Behavior": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # start of template https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
2 | *.DS_Store
3 |
4 | node_modules/
5 | dist/
6 | .idea/
7 | jsconfig.json
8 | project.config.json
9 | .env.local
10 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | stages:
2 | - test
3 | - build
4 |
5 | cache:
6 | key: node_modules
7 | paths:
8 | - node_modules/
9 |
10 | before_script:
11 | - nodenv shell 11.13.0
12 | - yarn install
13 | - cp project.config.json.example project.config.json
14 |
15 | test:
16 | script:
17 | - yarn run lint
18 | except:
19 | - develop
20 | - staging
21 | - master
22 |
23 | deploy_staging:
24 | stage: build # 对应stages上的job名称
25 | script:
26 | - yarn build:staging
27 | - APP_ENV=staging node script/upload.js
28 | only:
29 | - staging
30 | tags:
31 | - mac-shell-runner
32 |
33 | deploy_production:
34 | stage: build # 对应stages上的job名称
35 | script:
36 | - yarn build:production
37 | - APP_ENV=production node script/upload.js
38 | only:
39 | - master
40 | tags:
41 | - mac-shell-runner
42 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
2 | registry=https://registry.npm.taobao.org
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 微信小程序开发脚手架
2 |
3 | ## Installation
4 |
5 | ```
6 | # 克隆仓库到指定的文件夹
7 | $ git clone git@github.com:jamieYou/weapp-starter-kit.git
8 | $ cd weapp-starter-kit
9 | $ cp project.config.json.example project.config.json
10 | ```
11 |
12 | ## Develop
13 | * 在项目根目录运行 `yarn`
14 | * 在项目根目录运行 `yarn start`
15 | * 打开微信Web开放者工具,将项目的跟目录导入进去,填写或选择相应信息
16 |
17 | ## Script
18 | ```
19 | yarn start //开发模式
20 | NODE_ENV=xxx yarn build //部署 # NODE_ENV 默认为 production
21 | yarn lint // 用于 ci 的 esling 检查
22 | ```
23 |
24 | ## css
25 | 1. 使用 scss 作为开发语言
26 | 2. 页面、组件以外的 scss 文件,请以 _ 开头或者放在 styles 目录
27 |
28 | ## npm 支持
29 | 可以在 js 中引入 npm 的包
30 |
31 | ## alias
32 | 通过 `babel-plugin-module-resolver` 支持 require 的路径别名配置
33 |
34 | `require('@/store')` `@`指 src 的目录
35 |
36 | ## wxp
37 | 全局变量 wxp,对 wx 的 api 进行 Promise 封装
38 |
39 | ## 环境变量
40 | 支持通过 `.env` 文件修改环境变量,[参考](https://www.npmjs.com/package/node-env-file)
41 |
42 | ## Observer
43 | 让页面和 mobx 结合使用
44 |
45 | ```javascript
46 | Observer.Page({
47 | state() {
48 | return {
49 | show: true,
50 | authStore,
51 | list: new Collection,
52 | }
53 | },
54 | computed: {
55 | get nickname() {
56 | return authStore.user.nickname
57 | }
58 | },
59 | onLoad() {
60 | // state 和 computed 设置到 this 上,并同步到 data
61 | this.authStore;
62 | this.nickname;
63 | }
64 | })
65 | ```
66 | 支持页面、组件(Observer.Component)、Behavior(Observer.Behavior),其他参数按照官方文档即可
67 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash')
2 | const { appENV } = require('./config/env')
3 |
4 | module.exports = {
5 | presets: [
6 | [
7 | '@babel/preset-env',
8 | {
9 | 'useBuiltIns': 'usage',
10 | 'corejs': 3,
11 | 'targets': {
12 | 'chrome': '49',
13 | 'safari': '10'
14 | }
15 | }
16 | ]
17 | ],
18 | plugins: [
19 | 'lodash',
20 | [
21 | 'transform-define',
22 | _.mapKeys(appENV, (v, key) => `process.env.${key}`)
23 | ],
24 | [
25 | '@babel/plugin-transform-runtime',
26 | {
27 | 'corejs': false,
28 | 'helpers': true,
29 | 'regenerator': false,
30 | 'useESModules': false
31 | }
32 | ],
33 | '@babel/plugin-syntax-dynamic-import',
34 | '@babel/plugin-syntax-import-meta',
35 | ['@babel/plugin-proposal-decorators', { 'legacy': true }],
36 | ['@babel/plugin-proposal-class-properties', { 'loose': true }],
37 | '@babel/plugin-proposal-export-namespace-from',
38 | '@babel/plugin-proposal-throw-expressions',
39 | '@babel/plugin-proposal-export-default-from',
40 | ['@babel/plugin-proposal-pipeline-operator', { 'proposal': 'minimal' }],
41 | '@babel/plugin-proposal-do-expressions',
42 | '@babel/plugin-proposal-function-bind',
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/config/env.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = require('fs')
3 | const env = require('node-env-file')
4 | const _ = require('lodash')
5 |
6 | const APP_ENV = process.env.APP_ENV || 'development'
7 | const localEnv = path.resolve('.env.local')
8 |
9 | env(path.resolve(`.env.${APP_ENV}`))
10 | if (fs.existsSync(localEnv)) env(localEnv)
11 |
12 | module.exports = {
13 | appENV: _.pickBy(process.env, (v, key) => {
14 | return /^APP_/.test(key) || key === 'NODE_ENV'
15 | })
16 | }
17 |
--------------------------------------------------------------------------------
/gulpfile.js/gulp-px2rpx/index.js:
--------------------------------------------------------------------------------
1 | const through = require('through2')
2 |
3 | // 默认参数
4 | const opts = {
5 | baseDpr: 1,
6 | viewportUnit: 'rpx',
7 | minPixelValue: 1,
8 | precision: 2,
9 | }
10 | const ZPXRegExp = /(\d+)px/
11 |
12 | function createPxReplace({ minPixelValue, baseDpr, viewportUnit, precision }) {
13 | return function ($0, $1) {
14 | if (!$1) return
15 | const pixels = parseFloat($1)
16 | if (pixels <= minPixelValue) return $1 + 'px'
17 | return toFixed(pixels / baseDpr * 2, precision) + viewportUnit
18 | }
19 | }
20 |
21 | function toFixed(number, precision) {
22 | const multiplier = Math.pow(10, precision + 1),
23 | wholeNumber = Math.floor(number * multiplier)
24 | return Math.round(wholeNumber / 10) * 10 / multiplier
25 | }
26 |
27 | function px2rpx() {
28 | return through.obj(
29 | function (file, encoding, cb) {
30 | if (file.isNull()) return cb(null, file)
31 |
32 | let _source = file.contents.toString()
33 | let pxGlobalRegExp = new RegExp(ZPXRegExp.source, 'g')
34 | if (pxGlobalRegExp.test(_source)) {
35 | _source = _source.replace(pxGlobalRegExp, createPxReplace(opts))
36 | }
37 |
38 | file.contents = Buffer.from(_source, 'utf-8')
39 | this.push(file)
40 | cb()
41 | }
42 | )
43 | }
44 |
45 | module.exports = px2rpx
46 |
--------------------------------------------------------------------------------
/gulpfile.js/gulp-wx-babel/babel-plugins.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const babel = require('@babel/core')
3 | const _ = require('lodash')
4 | const fs = require('fs-extra')
5 | const resolve = require('enhanced-resolve');
6 | const { appENV } = require('../../config/env')
7 |
8 | const myResolve = resolve.create.sync(require(path.resolve('webpack.config')).resolve);
9 |
10 | const node_modules = {}
11 | const src_path = path.join(process.cwd(), 'src')
12 | const dist_path = path.join(process.cwd(), 'dist')
13 | const node_modules_path = path.join(process.cwd(), 'node_modules')
14 | const miniprogram_npm_path = path.join(dist_path, 'miniprogram_npm')
15 | const require_mask_reg = new RegExp(', "gulp-wx-babel-done"', 'g')
16 |
17 | function handle(nodePath, state) {
18 | if (nodePath.node.callee.name === 'require') {
19 | const file_path = state.file.opts.filename
20 | const require_path = _.get(nodePath, 'node.arguments[0].value')
21 | const status = _.get(nodePath, 'node.arguments[1].value')
22 | if (require_path && status !== 'gulp-wx-babel-done') {
23 | const absolutePath = myResolve(path.dirname(file_path), require_path)
24 | if (_.includes(file_path, src_path) && _.includes(absolutePath, node_modules_path)) {
25 | const require_module = absolutePath.replace(node_modules_path, '').replace(/\\/g, '/').replace(/^\//, '')
26 | nodePath.replaceWithSourceString(
27 | `require("${require_module}", "gulp-wx-babel-done")`
28 | )
29 | } else {
30 | const require_module = path.relative(path.dirname(file_path), absolutePath)
31 | nodePath.replaceWithSourceString(
32 | `require("${require_module.replace(/\\/g, '/')}", "gulp-wx-babel-done")`
33 | )
34 | }
35 |
36 | if (_.includes(absolutePath, node_modules_path)) {
37 | transformFileSync(absolutePath, opts)
38 | }
39 | }
40 | }
41 | }
42 |
43 | function transformFileSync(file_path, opts) {
44 | if (node_modules[file_path]) return
45 | const res = babel.transformFileSync(file_path, opts)
46 | const output_path = file_path.replace(node_modules_path, miniprogram_npm_path)
47 | fs.outputFileSync(output_path, res.code.replace(require_mask_reg, ''))
48 | node_modules[file_path] = output_path
49 | }
50 |
51 | const plugin = function () {
52 | return {
53 | visitor: {
54 | CallExpression: handle
55 | }
56 | }
57 | }
58 |
59 | const opts = {
60 | configFile: false,
61 | plugins: [[
62 | 'transform-define',
63 | _.mapKeys(appENV, (v, key) => `process.env.${key}`)
64 | ],
65 | '@babel/plugin-transform-modules-commonjs',
66 | plugin
67 | ],
68 | }
69 |
70 | exports.plugin = plugin
71 | exports.node_modules = node_modules
72 | exports.require_mask_reg = require_mask_reg
73 |
--------------------------------------------------------------------------------
/gulpfile.js/gulp-wx-babel/index.js:
--------------------------------------------------------------------------------
1 | const through = require('through2')
2 | const path = require('path')
3 | const PluginError = require('plugin-error')
4 | const babel = require('@babel/core')
5 | const { plugin, require_mask_reg } = require('./babel-plugins')
6 |
7 | const app_js_path = path.join(process.cwd(), 'src', 'app.js')
8 |
9 | function wxBabel() {
10 | return through.obj(
11 | function (file, encoding, cb) {
12 | if (file.isNull()) return cb(null, file)
13 |
14 | const opts = {
15 | filename: file.path,
16 | filenameRelative: file.relative,
17 | sourceMap: Boolean(file.sourceMap),
18 | sourceFileName: file.relative,
19 | configFile: false,
20 | plugins: [plugin],
21 | }
22 |
23 | babel.transformAsync(file.contents.toString(), opts)
24 | .then(res => {
25 | let code = res.code.replace(/"use strict";/g, '').replace(require_mask_reg, '')
26 | if (file.path === app_js_path) {
27 | code = `(function () {globalThis = this})();
28 | ${code}`
29 | }
30 | file.contents = Buffer.from(code, 'utf-8')
31 | this.push(file)
32 | })
33 | .catch(err => {
34 | this.emit('error', new PluginError('gulp-wx-babel', err, {
35 | fileName: file.path,
36 | showProperties: true
37 | }))
38 | })
39 | .finally(cb)
40 | }
41 | )
42 | }
43 |
44 | module.exports = wxBabel
45 |
--------------------------------------------------------------------------------
/gulpfile.js/index.js:
--------------------------------------------------------------------------------
1 | require('../config/env')
2 | const del = require('del')
3 | const gulp = require('gulp')
4 | const gutil = require('gulp-util')
5 | const uglify = require('gulp-uglify-es').default
6 | const eslint = require('gulp-eslint')
7 | const gulpif = require('gulp-if')
8 | const changed = require('gulp-changed')
9 | const notifier = require('node-notifier')
10 | const babel = require('gulp-babel')
11 | const sass = require('gulp-sass')
12 | const header = require('gulp-header')
13 | const autoprefixer = require('gulp-autoprefixer')
14 | const rename = require('gulp-rename')
15 | const wxBabel = require('./gulp-wx-babel')
16 | const px2rpx = require('./gulp-px2rpx')
17 |
18 | const handleError = function (err) {
19 | const colors = gutil.colors
20 | gutil.log(colors.red('Error!'))
21 | gutil.log('fileName: ' + colors.red(err.fileName))
22 | gutil.log('lineNumber: ' + colors.red(err.lineNumber))
23 | gutil.log('message: ' + err.message)
24 | gutil.log('plugin: ' + colors.yellow(err.plugin))
25 | notifier.notify({ title: `${err.plugin}错误`, message: err.fileName || err.message })
26 | this.emit('end')
27 | }
28 |
29 | const fileTypes = {
30 | fileCopy: ['src/**/*.json', 'src/**/*.{png,svg,jpg,jpeg}', 'src/**/*.wxs'],
31 | wxmlCopy: 'src/**/*.wxml',
32 | wxssCompile: 'src/**/*.wxss',
33 | scssCompile: ['src/**/*.scss', '!_*.scss', '!src/styles/*.scss'],
34 | buildJS: ['src/**/*.js'],
35 | }
36 |
37 | function fileCopy() {
38 | return gulp.src(fileTypes.fileCopy, { base: 'src' })
39 | .pipe(changed('dist'))
40 | .pipe(gulp.dest('dist'))
41 | }
42 |
43 | function wxmlCopy() {
44 | return gulp.src(fileTypes.wxmlCopy, { base: 'src' })
45 | .pipe(changed('dist'))
46 | .pipe(px2rpx())
47 | .pipe(gulp.dest('dist'))
48 | }
49 |
50 | function wxssCompile() {
51 | return gulp.src(fileTypes.wxssCompile, { base: 'src' })
52 | .pipe(changed('dist'))
53 | .pipe(px2rpx())
54 | .pipe(autoprefixer(['iOS >= 8', 'Android >= 4.1']))
55 | .pipe(gulp.dest('dist'))
56 | }
57 |
58 | const sassHeader = `
59 | @import 'src/styles/colors.scss';
60 | @import 'src/styles/mixins.scss';
61 | `
62 |
63 | function scssTask(useChanged) {
64 | return gulp.src(fileTypes.scssCompile, { base: 'src' })
65 | .pipe(gulpif(useChanged, changed('dist', { extension: '.wxss' })))
66 | .pipe(header(sassHeader))
67 | .pipe(sass())
68 | .on('error', handleError)
69 | .pipe(px2rpx())
70 | .pipe(autoprefixer(['iOS >= 8', 'Android >= 4.1']))
71 | .pipe(rename({ extname: '.wxss' }))
72 | .pipe(gulp.dest('dist'))
73 | }
74 |
75 | function scssCompile() {
76 | return scssTask(true)
77 | }
78 |
79 | function buildJS() {
80 | return gulp.src(fileTypes.buildJS, { base: 'src' })
81 | .pipe(changed('dist'))
82 | .pipe(eslint())
83 | .pipe(babel())
84 | .pipe(wxBabel())
85 | .on('error', handleError)
86 | .pipe(gulp.dest('dist'))
87 | }
88 |
89 | function uglifyJs() {
90 | return gulp.src('dist/**/*.js', { base: 'dist' })
91 | .pipe(uglify({ toplevel: true }))
92 | .pipe(gulp.dest('dist'))
93 | }
94 |
95 | function watch(done) {
96 | gulp.watch(fileTypes.fileCopy, gulp.series(fileCopy))
97 | gulp.watch(fileTypes.wxmlCopy, gulp.series(wxmlCopy))
98 | gulp.watch(fileTypes.wxssCompile, gulp.series(wxssCompile))
99 | gulp.watch(fileTypes.scssCompile, gulp.series(scssCompile))
100 | gulp.watch(fileTypes.buildJS, gulp.series(buildJS))
101 | gulp.watch(
102 | ['_*.scss', 'src/styles/*.scss'],
103 | function scssCompile() {
104 | return scssTask(false)
105 | }
106 | )
107 | done()
108 | }
109 |
110 | function clean() {
111 | return del(['./dist/**'])
112 | }
113 |
114 | function eslintCode() {
115 | return gulp
116 | .src('src/**/*.js')
117 | .pipe(eslint())
118 | .pipe(eslint.failAfterError())
119 | }
120 |
121 | const common = [clean, gulp.parallel(fileCopy, wxmlCopy, wxssCompile, buildJS, scssCompile)]
122 |
123 | exports.dev = gulp.series(...common, watch)
124 | exports.build = gulp.series(...common, uglifyJs)
125 | exports.eslint = eslintCode
126 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fengma-mini-program",
3 | "version": "2.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "start": "cross-env APP_ENV=development gulp dev",
7 | "lint": "gulp eslint",
8 | "build:staging": "cross-env APP_ENV=staging gulp build",
9 | "build:production": "cross-env APP_ENV=production gulp build"
10 | },
11 | "dependencies": {
12 | "@minapp/wx": "^2.2.1",
13 | "@vuex-orm/core": "^0.36.3",
14 | "axios": "^0.19.2",
15 | "core-decorators": "^0.20.0",
16 | "core-js": "^3.6.5",
17 | "dayjs": "^1.8.30",
18 | "lodash": "^4.17.19",
19 | "mobx": "^5.15.4",
20 | "qs": "^6.9.4"
21 | },
22 | "devDependencies": {
23 | "@babel/core": "^7.10.5",
24 | "@babel/plugin-proposal-class-properties": "^7.10.4",
25 | "@babel/plugin-proposal-decorators": "^7.10.5",
26 | "@babel/plugin-proposal-do-expressions": "^7.10.4",
27 | "@babel/plugin-proposal-export-default-from": "^7.10.4",
28 | "@babel/plugin-proposal-export-namespace-from": "^7.10.4",
29 | "@babel/plugin-proposal-function-bind": "^7.10.5",
30 | "@babel/plugin-proposal-optional-chaining": "^7.10.4",
31 | "@babel/plugin-proposal-pipeline-operator": "^7.10.5",
32 | "@babel/plugin-proposal-throw-expressions": "^7.10.4",
33 | "@babel/plugin-syntax-dynamic-import": "^7.8.3",
34 | "@babel/plugin-syntax-import-meta": "^7.10.4",
35 | "@babel/plugin-transform-runtime": "^7.10.5",
36 | "@babel/preset-env": "^7.10.4",
37 | "@babel/runtime": "^7.10.5",
38 | "babel-plugin-lodash": "^3.3.4",
39 | "babel-plugin-transform-define": "^2.0.0",
40 | "cross-env": "^7.0.2",
41 | "del": "^5.1.0",
42 | "enhanced-resolve": "^4.3.0",
43 | "eslint-config-beansmile": "https://github.com/beansmile/eslint-config-beansmile.git",
44 | "fs-extra": "^9.0.1",
45 | "gulp": "^4.0.2",
46 | "gulp-autoprefixer": "^7.0.1",
47 | "gulp-babel": "^8.0.0",
48 | "gulp-changed": "^4.0.2",
49 | "gulp-eslint": "^6.0.0",
50 | "gulp-header": "^2.0.9",
51 | "gulp-if": "^3.0.0",
52 | "gulp-rename": "^2.0.0",
53 | "gulp-sass": "^4.1.0",
54 | "gulp-uglify-es": "^2.0.0",
55 | "gulp-util": "^3.0.8",
56 | "ipv4": "^1.0.4",
57 | "node-env-file": "^0.1.8",
58 | "node-notifier": "^7.0.2"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/project.config.json.example:
--------------------------------------------------------------------------------
1 | {
2 | "description": "项目配置文件。",
3 | "setting": {
4 | "urlCheck": false,
5 | "es6": false,
6 | "postcss": false,
7 | "minified": false,
8 | "newFeature": true
9 | },
10 | "miniprogramRoot": "./dist/",
11 | "compileType": "miniprogram",
12 | "libVersion": "2.0.0",
13 | "appid": "touristappid",
14 | "projectname": "weapp-starter-kit",
15 | "condition": {
16 | "search": {
17 | "current": -1,
18 | "list": []
19 | },
20 | "conversation": {
21 | "current": -1,
22 | "list": []
23 | },
24 | "plugin": {
25 | "current": -1,
26 | "list": []
27 | },
28 | "game": {
29 | "list": []
30 | },
31 | "miniprogram": {
32 | "current": -1,
33 | "list": []
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/script/upload.js:
--------------------------------------------------------------------------------
1 | const dayjs = require('dayjs');
2 | const path = require('path');
3 | const ci = require(
4 | path.join(
5 | process.execPath.replace(/bin\/node$/, ''),
6 | 'lib/node_modules/miniprogram-ci',
7 | )
8 | );
9 |
10 | const robots = ['', 'staging', 'production'];
11 |
12 | const config = {
13 | version: dayjs().format('YYMMDDTHH'),
14 | env: process.env.APP_ENV,
15 | };
16 |
17 | function getAppId() {
18 | return require('../project.config.json').appid;
19 | }
20 |
21 | async function upload(env = config.env, desc, version = config.version) {
22 | const desc_str = desc || env;
23 | const projectPath = path.resolve();
24 | const privateKeyPath = path.resolve('script/upload.key');
25 | const appid = getAppId();
26 | const robot = robots.indexOf(env);
27 |
28 | const project = new ci.Project({
29 | appid, type: 'miniProgram',
30 | projectPath, privateKeyPath,
31 | ignores: ['node_modules/**/*'],
32 | });
33 |
34 | await ci.upload({
35 | project, version,
36 | desc: desc_str,
37 | robot, onProgressUpdate: console.log,
38 | });
39 | }
40 |
41 | upload().catch(err => {
42 | console.error(err);
43 | process.exit(1);
44 | });
45 |
--------------------------------------------------------------------------------
/script/upload.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | demo
3 | -----END RSA PRIVATE KEY-----
4 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import './utils/wxp'
2 |
3 | App({})
4 |
--------------------------------------------------------------------------------
/src/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": [
3 | "pages/root/home/home"
4 | ],
5 | "window": {
6 | "backgroundTextStyle": "light",
7 | "navigationBarBackgroundColor": "#fff",
8 | "navigationBarTitleText": "app",
9 | "navigationBarTextStyle": "black"
10 | },
11 | "usingComponents": {}
12 | }
13 |
--------------------------------------------------------------------------------
/src/app.scss:
--------------------------------------------------------------------------------
1 | /*每个页面公共css */
2 | @import './styles/flex.scss';
3 |
4 | page {
5 | font-size: 14px;
6 | color: #333333;
7 | padding-bottom: env(safe-area-inset-bottom);
8 | }
9 |
10 | ::-webkit-scrollbar {
11 | display: none;
12 | }
13 |
14 | .text-color-primary {
15 | color: $primary;
16 | }
17 |
18 | .text-center {
19 | text-align: center;
20 | }
21 |
22 | .text-overflow {
23 | overflow: hidden;
24 | text-overflow: ellipsis;
25 | white-space: nowrap;
26 | }
27 |
28 | .text-overflow-2 {
29 | @include text-overflow(2);
30 | }
31 |
32 | // 重置 button 样式,当做 div 使用
33 | .reset-btn {
34 | padding: 0;
35 | line-height: inherit;
36 | border-radius: 0;
37 | margin: 0;
38 | background-color: inherit;
39 | color: inherit;
40 | font-size: inherit;
41 |
42 | &::after {
43 | display: none;
44 | }
45 | }
46 |
47 | image {
48 | height: 0;
49 | vertical-align: top;
50 | }
51 |
52 | .disabled {
53 | pointer-events: none;
54 | }
55 |
--------------------------------------------------------------------------------
/src/constants/index.js:
--------------------------------------------------------------------------------
1 | export const ACCESS_TOKEN_KEY = `${process.env.APP_ENV}.access_token`;
2 |
3 | export const SHARE_COVER = '';
4 |
--------------------------------------------------------------------------------
/src/models/record.d.ts:
--------------------------------------------------------------------------------
1 | import { Model } from '@vuex-orm/core/lib'
2 | import { AxiosInstance } from 'axios'
3 |
4 | export default class Record extends Model {
5 | static api(): AxiosInstance
6 | api(): AxiosInstance
7 | updateAttrs(attrs: Object)
8 | }
9 |
--------------------------------------------------------------------------------
/src/models/record.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import { decorate, observable, extendObservable } from 'mobx';
3 | import Model from '@vuex-orm/core/lib/model/Model';
4 | import { request } from '@/utils/request';
5 |
6 | export default class Record extends Model {
7 | static fields() {
8 | return {
9 | id: this.attr(null),
10 | createdAt: this.attr(''),
11 | updatedAt: this.attr(''),
12 | remarks: this.attr(''),
13 | };
14 | }
15 |
16 | static api(cb) {
17 | const methods = ['get', 'delete', 'post', 'put', 'patch', 'request'];
18 | const obj = Object.create(request);
19 | methods.forEach(method => {
20 | obj[method] = async (...arg) => {
21 | const res = await request[method](...arg);
22 | if (cb) {
23 | cb(res);
24 | } else if (_.isArray(res.data)) {
25 | res.data = res.data.map(v => new this(v));
26 | } else {
27 | res.data = new this(res.data);
28 | }
29 | return res;
30 | };
31 | });
32 | return obj;
33 | }
34 |
35 | api() {
36 | return this.constructor.api(res => res.data = this.updateAttrs(res.data));
37 | }
38 |
39 | constructor(attrs) {
40 | super(attrs);
41 | const newAttrs = {}
42 | _.keys(attrs).forEach(key => {
43 | if (!_.hasIn(this, key)) {
44 | newAttrs[key] = attrs[key];
45 | }
46 | });
47 | extendObservable(this, newAttrs)
48 | }
49 |
50 | updateAttrs(attrs) {
51 | if (!_.isPlainObject(attrs)) {
52 | // eslint-disable-next-line no-console
53 | console.warn('attrs 尽量传递[普通对象](https://www.html.cn/doc/lodash/#_isplainobjectvalue)');
54 | }
55 | const model = new this.constructor(attrs);
56 | const newAttr = _.pick(model, _.keys(attrs));
57 | return Object.assign(this, newAttr);
58 | }
59 | }
60 |
61 | export function observables(target) {
62 | const fields = target.fields()
63 | decorate(target, _.mapValues(fields, () => observable))
64 | return target
65 | }
66 |
67 | Record.prototype.toJSON = require('@/utils/to-json-deep').default;
68 |
--------------------------------------------------------------------------------
/src/models/user.js:
--------------------------------------------------------------------------------
1 | import Record, { observables } from './record';
2 |
3 | @observables
4 | export default class User extends Record {
5 | static fields() {
6 | return {
7 | ...super.fields(),
8 | nickname: this.attr(''),
9 | avatar: this.attr(''),
10 | };
11 | }
12 |
13 | get wechat_authorized() {
14 | return this.avatar && this.nickname;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/pages/root/home/home.js:
--------------------------------------------------------------------------------
1 | import { Observer, authStore, Collection } from '@/stores'
2 |
3 | Observer.Page({
4 | state() {
5 | return {
6 | authStore, list: new Collection
7 | }
8 | },
9 | computed: {
10 | get nickname() {
11 | return authStore.user.nickname
12 | }
13 | }
14 | })
15 |
--------------------------------------------------------------------------------
/src/pages/root/home/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {
3 | }
4 | }
5 |
--------------------------------------------------------------------------------
/src/pages/root/home/home.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamieYou/weapp-starter-kit/a1be8c77ae57ecb6c1c687bbb3f3fd4530c14371/src/pages/root/home/home.scss
--------------------------------------------------------------------------------
/src/pages/root/home/home.wxml:
--------------------------------------------------------------------------------
1 | {{ authStore.user.nickname || 'hello' }}
2 |
--------------------------------------------------------------------------------
/src/static/empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamieYou/weapp-starter-kit/a1be8c77ae57ecb6c1c687bbb3f3fd4530c14371/src/static/empty.png
--------------------------------------------------------------------------------
/src/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamieYou/weapp-starter-kit/a1be8c77ae57ecb6c1c687bbb3f3fd4530c14371/src/static/logo.png
--------------------------------------------------------------------------------
/src/static/refresh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamieYou/weapp-starter-kit/a1be8c77ae57ecb6c1c687bbb3f3fd4530c14371/src/static/refresh.png
--------------------------------------------------------------------------------
/src/stores/auth-store.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx'
2 | import { SimpleStore } from './helper/simple-store';
3 | import { ACCESS_TOKEN_KEY } from '@/constants';
4 | import User from '@/models/user';
5 | import { request } from '@/utils';
6 |
7 | class AuthStore extends SimpleStore {
8 | @observable $access_token = wxp.getStorageSync(ACCESS_TOKEN_KEY)
9 | user = new User
10 |
11 | async checkLogin() {
12 | if (this.token) {
13 | try {
14 | await wxp.checkSession();
15 | return this.token;
16 | } catch (e) {
17 | return this.login();
18 | }
19 | }
20 | return this.login();
21 | }
22 |
23 | async login() {
24 | const { code } = await wxp.login();
25 | const { data: { access_token, user } } = await request.post('/users/token', { code });
26 | this.user = new User(user);
27 | return this.access_token = access_token;
28 | }
29 |
30 | set access_token(v) {
31 | this.$access_token = v;
32 | wxp.setStorageSync(ACCESS_TOKEN_KEY, v);
33 | }
34 |
35 | get access_token() {
36 | return this.$access_token;
37 | }
38 | }
39 |
40 | export const authStore = AuthStore.create();
41 |
--------------------------------------------------------------------------------
/src/stores/helper/collection.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx'
2 | import { SimpleStore } from './simple-store';
3 |
4 | export class Collection extends SimpleStore {
5 | static defaultParams = {
6 | page: 1,
7 | per_page: 10,
8 | }
9 |
10 | static excludeJsonNames = ['$params']
11 |
12 | @observable meta = { total: 0, page: 1, offset: 0 }
13 | @observable data = []
14 | $params = Object.assign({}, this.constructor.defaultParams, this.params)
15 |
16 | fetchData() {
17 | this.params.offset = 0;
18 | return this.fetching(async () => {
19 | const { data, meta } = await this.fetch(this.params);
20 | this.meta = meta;
21 | this.data = data;
22 | });
23 | }
24 |
25 | fetchMoreData() {
26 | if (this.isFetching || this.isComplete) {
27 | return ;
28 | }
29 | this.params.offset = this.data.length;
30 | return this.fetching(async () => {
31 | const { data, meta } = await this.fetch(this.params);
32 | this.meta = meta;
33 | this.data.push(...data);
34 | });
35 | }
36 |
37 | resetData() {
38 | this.isFulfilled = false;
39 | this.data = [];
40 | }
41 |
42 | unshift(item) {
43 | this.data.unshift(item);
44 | this.meta.total += 1;
45 | }
46 |
47 | findItemById(id) {
48 | return this.data.find(item => +item.id === +id);
49 | }
50 |
51 | removeItemById(id) {
52 | const index = this.data.findIndex(item => Number(item.id) === Number(id));
53 | if (index !== -1) {
54 | this.data.splice(index, 1);
55 | }
56 | }
57 |
58 | replaceItem(newItem) {
59 | const index = this.data.findIndex(item => +item.id === +newItem.id);
60 | if (index > -1) {
61 | this.data.splice(index, 1, newItem);
62 | }
63 | }
64 |
65 | get params() {
66 | return this.$params;
67 | }
68 |
69 | set params(properties) {
70 | this.$params = Object.assign({}, this.constructor.defaultParams, properties);
71 | }
72 |
73 | get isComplete() {
74 | return this.isFulfilled && this.data.length >= this.meta.total;
75 | }
76 |
77 | get isEmpty() {
78 | return this.isFulfilled && this.data.length === 0;
79 | }
80 |
81 | get loadMoreStatus() {
82 | if (this.isEmpty) { return 'empty'; }
83 | if (this.isComplete) { return 'noMore'; }
84 | if (this.isFetching) { return 'loading'; }
85 | return 'more';
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/stores/helper/observable.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | export default class Observable {
4 | static create(state) {
5 | const instance = new this();
6 | if (state) {
7 | _.forEach(Object.getOwnPropertyDescriptors(state), (descriptor, key) => {
8 | if ('value' in descriptor) {
9 | instance[key] = descriptor.value;
10 | } else {
11 | Object.defineProperty(instance, key, descriptor);
12 | }
13 | });
14 | }
15 | return instance;
16 | }
17 | }
18 |
19 | Observable.prototype.toJSON = require('@/utils/to-json-deep').default;
20 |
--------------------------------------------------------------------------------
/src/stores/helper/simple-store.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx'
2 | import Observable from './observable';
3 |
4 | export class SimpleStore extends Observable {
5 | @observable isFetching = false
6 | @observable isRejected = false
7 | @observable isFulfilled = false
8 |
9 | fetchData() {
10 | return Promise.resolve();
11 | }
12 |
13 | tryFetchData() {
14 | return !this.isFulfilled && this.fetchData(...arguments);
15 | }
16 |
17 | mergeFetched(handle) {
18 | return this.fetching(handle, true);
19 | }
20 |
21 | async fetching(handle, autoMerge = false) {
22 | this.isFetching = true;
23 |
24 | try {
25 | const res = await (typeof handle === 'function' ? handle() : handle);
26 | const newState = autoMerge ? (res.isResponse ? res.data : res) : void 0;
27 | Object.assign(this, {
28 | isFetching: false,
29 | isRejected: false,
30 | isFulfilled: true,
31 | }, newState);
32 | return res;
33 | } catch (err) {
34 | Object.assign(this, {
35 | isFetching: false,
36 | isRejected: true,
37 | });
38 | throw err;
39 | }
40 | }
41 | }
42 |
43 | export function fetching(target, name, descriptor) {
44 | const func = descriptor.value;
45 | descriptor.value = function () {
46 | return this.fetching(func.apply(this, arguments));
47 | };
48 | }
49 |
50 | export function mergeFetched(target, name, descriptor) {
51 | const func = descriptor.value;
52 | descriptor.value = function () {
53 | return this.mergeFetched(func.apply(this, arguments));
54 | };
55 | }
56 |
--------------------------------------------------------------------------------
/src/stores/index.js:
--------------------------------------------------------------------------------
1 | export { SimpleStore } from './helper/simple-store';
2 | export { Collection } from './helper/collection';
3 | export { authStore } from './auth-store';
4 | export { default as uiStore } from './ui-store';
5 | export Observer from './observer';
6 |
--------------------------------------------------------------------------------
/src/stores/observer.js:
--------------------------------------------------------------------------------
1 | import { autorun, observable, createAtom, extendObservable } from 'mobx'
2 | import _ from 'lodash'
3 |
4 | export default function Observer(options = {}) {
5 | let {
6 | $type, state, computed,
7 | onLoad, onUnload,
8 | lifetimes = {}, methods = {},
9 | ...config
10 | } = options
11 |
12 | function install(e) {
13 | // 只设置一次新属性, 使用 behaviors 时,install 会被 behaviors 和 Component 触发
14 | if (!this.isObserver) {
15 | this.$clears = []
16 | this.$dataAtom = createAtom('setDataAtom')
17 | this.$setData = this.setData
18 | this.setData = (data, cb) => {
19 | this.$setData(data, cb)
20 | this.$dataAtom.reportChanged()
21 | }
22 | this.$stateKeys = []
23 | this.$computedKeys = []
24 | this.$propertyKeys = []
25 | this.isObserver = true
26 | }
27 |
28 | if (state) {
29 | const states = state.call(this, e)
30 | this.$stateKeys.push(...Object.keys(states))
31 | extendObservable(this, states)
32 | }
33 |
34 | // 只有页面和组件才同步 state 到 data
35 | if (['Page', 'Component'].includes($type) && this.$stateKeys.length) {
36 | this.$clears.push(
37 | autorun(() => {
38 | this.$dataAtom.reportObserved()
39 | const state = _.pick(this, this.$stateKeys)
40 | this.$setData(_.mapValues(state, toJSON))
41 | }, { delay: 1 })
42 | )
43 | }
44 |
45 | if (computed) {
46 | this.$computedKeys.push(...Object.getOwnPropertyNames(computed))
47 | extendObservable(this, computed)
48 | }
49 |
50 | // 只有页面和组件才同步 computed 到 data
51 | if (['Page', 'Component'].includes($type) && this.$computedKeys.length) {
52 | this.$clears.push(
53 | autorun(() => {
54 | this.$dataAtom.reportObserved()
55 | const computed = _.pick(this, this.$computedKeys)
56 | this.$setData(_.mapValues(computed, toJSON))
57 | }, { delay: 1 })
58 | )
59 | }
60 |
61 | if (config.properties) {
62 | this.$propertyKeys.push(...Object.keys(config.properties))
63 | }
64 |
65 | // 只有页面和组件才让 properties 变成 observable
66 | if (['Page', 'Component'].includes($type) && this.$propertyKeys.length) {
67 | const properties = observable({}, null, { deep: false })
68 | _.forEach(this.properties, (v, key) => {
69 | if (this.$propertyKeys.includes(key)) {
70 | properties[key] = v
71 |
72 | Object.defineProperty(this.properties, key, {
73 | get() {
74 | return properties[key]
75 | },
76 | set(value) {
77 | properties[key] = value
78 | }
79 | })
80 | }
81 | })
82 | }
83 | }
84 |
85 | function uninstall() {
86 | this.$clears.forEach(cb => cb())
87 | }
88 |
89 | function $nextTick(cb) {
90 | if (cb) {
91 | this.$setData(null, cb)
92 | } else {
93 | return new Promise(resolve => this.$setData(null, resolve))
94 | }
95 | }
96 |
97 | if ($type === 'Page') {
98 | return Page(Object.assign(config, {
99 | $nextTick,
100 | onLoad(e) {
101 | install.call(this, e)
102 | return onLoad && onLoad.call(this, e)
103 | },
104 |
105 | onUnload() {
106 | uninstall.call(this)
107 | return onUnload && onUnload.call(this)
108 | }
109 | }))
110 | } else {
111 | const attachedEnd = lifetimes.attached || config.attached
112 | const detachedEnd = lifetimes.detached || config.detached
113 |
114 | const attached = function () {
115 | install.call(this)
116 | return attachedEnd && attachedEnd.call(this)
117 | }
118 |
119 | const detached = function () {
120 | uninstall.call(this)
121 | return detachedEnd && detachedEnd.call(this)
122 | }
123 |
124 | if ($type === 'Component') {
125 | Object.assign(methods, { $nextTick })
126 | Object.assign(lifetimes, { attached, detached })
127 |
128 | return Component(Object.assign(config, { methods, lifetimes }))
129 | } else {
130 | return Behavior(Object.assign(config, { methods, attached }))
131 | }
132 | }
133 | }
134 |
135 | function toJSON(v) {
136 | if (v && v.toJSON) {
137 | v = v.toJSON()
138 | }
139 | if (_.isObjectLike(v)) {
140 | if (_.isArray(v)) {
141 | return _.map(v, toJSON)
142 | } else {
143 | return _.mapValues(v, toJSON)
144 | }
145 | } else {
146 | return v
147 | }
148 | }
149 |
150 | Observer.Page = function (options = {}) {
151 | return Observer({ ...options, $type: 'Page' })
152 | }
153 |
154 | Observer.Component = function (options = {}) {
155 | return Observer({ ...options, $type: 'Component' })
156 | }
157 |
158 | Observer.Behavior = function (options = {}) {
159 | return Observer({ ...options, $type: 'Behavior' })
160 | }
161 |
--------------------------------------------------------------------------------
/src/stores/ui-store.js:
--------------------------------------------------------------------------------
1 | export default new class {
2 | systemInfo = {}
3 |
4 | constructor() {
5 | this.setSystemInfo();
6 | }
7 |
8 | setSystemInfo() {
9 | try {
10 | this.systemInfo = wxp.getSystemInfoSync();
11 | } catch (error) {
12 | this.systemInfo = wxp.getSystemInfoSync();
13 | } finally {
14 | this.systemInfo.platform || (this.systemInfo = wxp.getSystemInfoSync());
15 | }
16 | }
17 |
18 | isFullScreenModel() {
19 | return this.isIphone11 || this.isIphoneX;
20 | }
21 |
22 | /**
23 | * iPhone X: iPhone10,3 / iPhone10,6
24 | * iPhone XR: iPhone11,8
25 | * iPhone XS: iPhone11,2
26 | * iPhone 11: iPhone12,1
27 | * iPhone 11 Pro: iPhone12,3
28 | * iPhone XS Max: iPhone11,6 / iPhone11,4
29 | * iPhone 11 Pro Max: iPhone12,5
30 | */
31 | get isIphoneX() {
32 | const { model } = this.systemInfo;
33 | return /iPhone10,3|iPhone10,6|iPhone11,8|iPhone11,1|iPhone11,2|iPhone11,3|iPhone11,6|iPhone11,4|iPhone12,5/ig.test(model);
34 | }
35 |
36 | get safeBottom() {
37 | return this.isIphoneX ? '32rpx' : '';
38 | }
39 |
40 | get isIOS() {
41 | const { platform } = this.systemInfo;
42 | if (platform) {
43 | return platform.toUpperCase() === 'IOS';
44 | } else {
45 | return false;
46 | }
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/src/styles/colors.scss:
--------------------------------------------------------------------------------
1 | $primary: #007aff;
2 | $success: green;
3 | $warning: #f0ad4e;
4 | $danger: #dd524d;
5 |
6 | $text-color: #333;
7 | $bg-color: #f8f8f8;
8 | $border-color: #c8c7cc;
9 | $text-color-sm: #979797;
10 |
--------------------------------------------------------------------------------
/src/styles/flex.scss:
--------------------------------------------------------------------------------
1 | .flex {
2 | display: flex;
3 |
4 | &.no-shirk,
5 | .no-shirk {
6 | flex-shrink: 0;
7 | }
8 |
9 | .flex-1 {
10 | flex: 1;
11 | }
12 |
13 | &.row {
14 | flex-direction: row;
15 | }
16 |
17 | &.column {
18 | flex-direction: column;
19 | }
20 |
21 | &.wrap {
22 | flex-wrap: wrap;
23 | }
24 |
25 | &.item-start {
26 | align-items: flex-start;
27 | }
28 |
29 | &.item-end {
30 | align-items: flex-end;
31 | }
32 |
33 | &.item-center {
34 | align-items: center;
35 | }
36 |
37 | &.item-baseline {
38 | align-items: baseline;
39 | }
40 |
41 | &.item-stretch {
42 | align-items: stretch;
43 | }
44 |
45 | &.content-start {
46 | justify-content: flex-start;
47 | }
48 |
49 | &.content-end {
50 | justify-content: flex-end;
51 | }
52 |
53 | &.content-center {
54 | justify-content: center;
55 | }
56 |
57 | &.content-between {
58 | justify-content: space-between;
59 | }
60 |
61 | &.content-around {
62 | justify-content: space-around;
63 | }
64 |
65 | .self-center {
66 | align-self: center;
67 | }
68 |
69 | .self-start {
70 | align-self: flex-start;
71 | }
72 |
73 | .self-end {
74 | align-self: flex-end;
75 | }
76 |
77 | .overflow-hidden {
78 | overflow: hidden;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/styles/mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin text-overflow($line: 1) {
2 | display: -webkit-box;
3 | -webkit-box-orient: vertical;
4 | -webkit-line-clamp: $line;
5 | overflow: hidden;
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/check-api-auth.js:
--------------------------------------------------------------------------------
1 | const SCOPE_MAP = {
2 | userInfo: '用户信息',
3 | userLocation: '地理位置',
4 | userLocationBackground: '后台定位',
5 | address: '通讯地址',
6 | invoiceTitle: '发票抬头',
7 | invoice: '发票',
8 | werun: '微信运动步数',
9 | record: '录音功能',
10 | writePhotosAlbum: '相册功能',
11 | };
12 |
13 | const SCOPES = {
14 | getUserInfo: 'userInfo',
15 | chooseLocation: 'userLocation',
16 | getLocation: 'userLocation',
17 | startLocationUpdateBackground: 'userLocationBackground',
18 | chooseAddress: 'address',
19 | chooseInvoiceTitle: 'invoiceTitle',
20 | chooseInvoice: 'invoice',
21 | getWeRunData: 'werun',
22 | startRecord: 'record',
23 | saveVideoToPhotosAlbum: 'writePhotosAlbum',
24 | saveImageToPhotosAlbum: 'writePhotosAlbum',
25 | };
26 |
27 | /**
28 | * 自动检查并获取手机权限
29 | *
30 | * @param {String} scope 需要调用的小程序API,取值 SCOPES
31 | * @param {Object} options 调用API传的参数,默认空对象{}
32 | * @param {Boolean} isAuto 是否自动调用相对应的API,默认 true
33 | *
34 | * @return {Object|Boolean} 返回API调用后的返回值,如果 isAuto 为 false,打开权限后return true,用户拒绝 return false
35 | */
36 | async function checkApiAuth(scope, options = {}, isAuto = true) {
37 | const fullScope = SCOPES[scope];
38 | const { authSetting } = await wxp.getSetting();
39 | const isFirst = !Object.prototype.hasOwnProperty.call(authSetting, `scope.${fullScope}`);
40 |
41 | try {
42 | if (!authSetting[`scope.${fullScope}`]) {
43 | await wxp.authorize({ scope: `scope.${fullScope}` });
44 | }
45 | const res = isAuto ? (await wxp[scope](options)) : true;
46 | return res;
47 | } catch (e) {
48 | if (e.errMsg.indexOf('authorize:fail') !== -1) {
49 | if (isFirst) {
50 | return false;
51 | }
52 | const { confirm } = await wxp.showModal({ content: `授权失败,请在设置中打开“${SCOPE_MAP[fullScope]}”开关后继续操作`, confirmText: '去设置' });
53 | if (confirm) {
54 | const { authSetting } = await wxp.openSetting();
55 | if (authSetting[`scope.${fullScope}`]) {
56 | return true;
57 | }
58 | }
59 | return false;
60 | }
61 | throw e;
62 | }
63 | }
64 |
65 | export default checkApiAuth;
66 |
--------------------------------------------------------------------------------
/src/utils/decoder.js:
--------------------------------------------------------------------------------
1 | export default function decoder(text) {
2 | if (/^(\d+|\d*\.\d+)$/.test(text)) {
3 | const num = parseFloat(text);
4 | return num.toString() === text ? num : text;
5 | }
6 | const keywords = { true: true, false: false, null: null, undefined: undefined };
7 | if (text in keywords) {
8 | return keywords[text];
9 | }
10 | try {
11 | return decodeURIComponent(text.replace(/\+/g, ' '));
12 | } catch (err) {
13 | return text;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export { request } from './request';
2 | export {
3 | autoLoading,
4 | autoLoadingDecorator,
5 | showLoading,
6 | showToast,
7 | errToast,
8 | pageRefresh,
9 | confirm,
10 | alert
11 | } from './prompt';
12 | export { nav } from './nav';
13 | export { default as sleep } from './sleep';
14 | export { randomString, randomFileName } from './random';
15 | export { uploadFiles } from './upload-files';
16 | export { saveFiles } from './save-files';
17 | export checkApiAuth from './check-api-auth';
18 | export mergeShareMessage from './merge-share-message';
19 |
--------------------------------------------------------------------------------
/src/utils/merge-share-message.js:
--------------------------------------------------------------------------------
1 | export default function mergeShareMessage(config = {}) {
2 | let { title, path, imageUrl } = config
3 |
4 | if (/^http/.test(path)) {
5 | path = `/pages/extra/web-site/index?src=${encodeURIComponent(path)}`
6 | } else if (!path) {
7 | path = '/pages/root/home/index'
8 | }
9 |
10 | if (!imageUrl) {
11 | imageUrl = ''
12 | }
13 |
14 | return { title, path, imageUrl }
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/nav.js:
--------------------------------------------------------------------------------
1 | import qs from 'qs';
2 |
3 | class Nav {
4 | tabPages = [
5 | '/pages/root/home'
6 | ]
7 |
8 | tabQueryList = {
9 | '/pages/root/home': {},
10 | }
11 |
12 | get currentPage() {
13 | const pages = getCurrentPages();
14 | return pages[pages.length - 1];
15 | }
16 |
17 | get prevPage() {
18 | const pages = getCurrentPages();
19 | return pages[pages.length - 2];
20 | }
21 |
22 | goHome() {
23 | return wxp.reLaunch({ url: '/pages/root/home' });
24 | }
25 |
26 | isTabPage(url = this.currentPage.route) {
27 | const reg = new RegExp(url);
28 | return !!this.tabPages.find(item => reg.test(item));
29 | }
30 |
31 | navigateTo(options) {
32 | const { url } = options;
33 |
34 | // 处理超过十级页面无法跳转问题
35 | const pages = getCurrentPages();
36 | const navType = pages.length < 10 ? 'navigateTo' : 'redirectTo';
37 | return wxp[navType]({ ...options, url });
38 | }
39 |
40 | redirectTo(options) {
41 | return wxp.redirectTo(options);
42 | }
43 |
44 | reLaunch(options) {
45 | return wxp.redirectTo(options);
46 | }
47 |
48 | switchTab(options) {
49 | const { url } = options;
50 | const [link, search] = url.split('?');
51 | // 支持跳转 tab 时,传递参数,一般是 url 来源 api
52 | this.tabQueryList[url] = qs.parse(search);
53 | wxp.switchTab({ ...options, url: link });
54 | }
55 |
56 | navigateBack(options = {}) {
57 | const { delta = 1 } = options;
58 | const pages = getCurrentPages();
59 | const canBack = pages.length > delta;
60 | canBack ? wxp.navigateBack(options) : this.goHome();
61 | }
62 |
63 | nav(link) {
64 | if (link) {
65 | if (/^http/.test(link)) {
66 | this.navigateTo({ url: `/pages/extra/web-site?src=${encodeURIComponent(link)}` });
67 | } else if (this.isTabPage(link)) {
68 | this.switchTab({ url: link });
69 | } else {
70 | this.navigateTo({ url: link });
71 | }
72 | }
73 | }
74 | }
75 |
76 | export const nav = new Nav();
77 |
--------------------------------------------------------------------------------
/src/utils/prompt.js:
--------------------------------------------------------------------------------
1 | export function showToast(params) {
2 | const options = {
3 | title: '',
4 | icon: 'none',
5 | mask: true,
6 | duration: 1500,
7 | };
8 | if (typeof params === 'string') {
9 | Object.assign(options, { title: params });
10 | } else {
11 | Object.assign(options, params);
12 | }
13 | wxp.showToast(options);
14 | return new Promise(resolve => setTimeout(resolve, options.duration));
15 | }
16 |
17 | export function showLoading(params) {
18 | const options = {
19 | title: '',
20 | mask: true,
21 | };
22 | if (typeof params === 'string') {
23 | Object.assign(options, { title: params });
24 | } else {
25 | Object.assign(options, params);
26 | }
27 | return wxp.showLoading(options);
28 | }
29 |
30 | export const alert = (content, opts = {}) => {
31 | return wxp.showModal({
32 | content,
33 | confirmText: '我知道了',
34 | showCancel: false,
35 | ...opts
36 | });
37 | };
38 |
39 | export const confirm = async (content, opts = {}) => {
40 | const { confirm } = await wxp.showModal({
41 | content,
42 | confirmText: '确认',
43 | showCancel: true,
44 | cancelText: '取消',
45 | ...opts
46 | });
47 | if (!confirm) {
48 | return Promise.reject(new Error('用户取消'));
49 | }
50 | };
51 |
52 | export function autoLoadingDecorator(target, name, descriptor) {
53 | const func = descriptor.value;
54 | descriptor.value = function () {
55 | return autoLoading(func.apply(this, arguments));
56 | };
57 | }
58 |
59 | export function autoLoading(target, options) {
60 | showLoading(options || '加载中');
61 | const action = Promise.resolve(target instanceof Function ? target() : target);
62 | return action
63 | .finally(() => {
64 | wxp.hideLoading();
65 | })
66 | .catch(err => {
67 | errHandle(err);
68 | });
69 | }
70 |
71 | export function pageRefresh(target, name, descriptor) {
72 | const func = descriptor.value;
73 | descriptor.value = async function () {
74 | try {
75 | await func.apply(this, arguments);
76 | } catch (err) {
77 | errHandle(err);
78 | } finally {
79 | wxp.stopPullDownRefresh();
80 | }
81 | };
82 | }
83 |
84 | export function errToast(target, name, descriptor) {
85 | const func = descriptor.value;
86 | descriptor.value = function () {
87 | return func.apply(this, arguments).catch(err => {
88 | wxp.hideLoading();
89 | errHandle(err);
90 | });
91 | };
92 | }
93 |
94 | export function errHandle(err) {
95 | const ignoreErrors = /(cancel|ignore|请先登录)/i;
96 | const msg = err.message || err.errMsg;
97 | if (!ignoreErrors.test(msg)) {
98 | msg && alert(msg, {
99 | title: '请求失败',
100 | });
101 | }
102 | throw err;
103 | }
104 |
--------------------------------------------------------------------------------
/src/utils/random.js:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 |
3 | export function randomString() {
4 | return Math.random().toString(36).substr(2, 9);
5 | }
6 |
7 | export function randomFileName(fileName) {
8 | return [
9 | dayjs().format('YYMMDD'),
10 | randomString(),
11 | fileName
12 | ].filter(Boolean).join('/');
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/request.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import qs from 'qs';
3 | import axios from 'axios';
4 | import settle from 'axios/lib/core/settle';
5 | import createError from 'axios/lib/core/createError';
6 | import buildFullPath from 'axios/lib/core/buildFullPath';
7 | import buildURL from 'axios/lib/helpers/buildURL';
8 | import decoder from './decoder';
9 |
10 | const request = axios.create({
11 | baseURL: process.env.APP_API_HOST + '/app/api/v1',
12 | timeout: 30000,
13 | headers: { 'Content-Type': 'application/json' },
14 | paramsSerializer(params) {
15 | return qs.stringify(params, { arrayFormat: 'brackets' });
16 | },
17 | transformRequest: [(data, headers) => {
18 | if (!headers['Authorization']) {
19 | const { authStore } = require('@/stores/auth-store');
20 | headers['Authorization'] = authStore.access_token;
21 | }
22 | return data;
23 | }],
24 | adapter(config) {
25 | const fullPath = buildFullPath(config.baseURL, config.url);
26 | return wxp.request({
27 | method: config.method.toUpperCase(),
28 | url: buildURL(fullPath, config.params, config.paramsSerializer),
29 | header: config.headers,
30 | data: config.data,
31 | dataType: config.dataType || undefined,
32 | responseType: config.responseType || 'text',
33 | enableCache: true,
34 | })
35 | .then(
36 | res => {
37 | return new Promise((resolve, reject) => {
38 | settle(resolve, reject, {
39 | data: res.data,
40 | status: res.statusCode,
41 | statusText: res.errMsg,
42 | headers: res.header,
43 | config: config,
44 | });
45 | });
46 | },
47 | res => {
48 | return Promise.reject(createError(
49 | res.errMsg,
50 | config,
51 | 0,
52 | ));
53 | }
54 | );
55 | }
56 | });
57 |
58 | request.interceptors.response.use(
59 | res => {
60 | res.isResponse = true;
61 | res.meta = {};
62 | _.forEach(res.headers, (v, k) => {
63 | if (/^x-/i.test(k)) {
64 | const key = _.snakeCase(k.replace(/^x-/i, ''));
65 | res.meta[key] = decoder(v);
66 | }
67 | });
68 | return res;
69 | },
70 | err => {
71 | const response = _.get(err, 'response', {});
72 | const { error_message, messages, error, code } = response.data;
73 | err.message = error_message || messages || error || err.message;
74 | err.code = code;
75 | err.status = response.status;
76 | if (response.status === 401) {
77 | return login_once().then(() => request.request(response.config));
78 | }
79 | return Promise.reject(err);
80 | }
81 | );
82 |
83 | let login_once = _.once(login);
84 |
85 | async function login() {
86 | const { authStore } = require('@/stores/auth-store');
87 | try {
88 | await authStore.login();
89 | } finally {
90 | login_once = _.once(login);
91 | }
92 | }
93 |
94 | export { request };
95 |
--------------------------------------------------------------------------------
/src/utils/save-files.js:
--------------------------------------------------------------------------------
1 | import checkApiAuth from './check-api-auth';
2 |
3 | /**
4 | * 保存图片或视频到本地相册
5 | *
6 | * @param {String|Array} urls 媒体文件本地链接或网络链接
7 | * @param {String} mediumType 标识保存的是图片还是视频,默认保存图片
8 | */
9 | export async function saveFiles(urls, mediumType = 'image') {
10 | const method = mediumType === 'video' ? 'saveVideoToPhotosAlbum' : 'saveImageToPhotosAlbum';
11 | await checkApiAuth(method, {}, false);
12 | await saveToPhotosAlbum(urls, method);
13 | }
14 |
15 | async function saveToPhotosAlbum(urls, method) {
16 | for (const url of [].concat(urls)) {
17 | let filePath;
18 | if (/^wxfile/.test(url) || /^http:\/\/tmp\//.test(url)) {
19 | filePath = url;
20 | } else {
21 | const { tempFilePath } = await wxp.downloadFile({ url });
22 | filePath = tempFilePath;
23 | }
24 | await wxp[method]({ filePath });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/sleep.js:
--------------------------------------------------------------------------------
1 | export default function(time) {
2 | return new Promise((resolve) => {
3 | setTimeout(() => resolve(), time);
4 | });
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/to-json-deep.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | // 通过 class new 出来的对象,在小程序 setData 时会丢失 getter 属性,通过下面的函数可解决
4 | // 此方法已用于 src/stores/helper/observable.js、src/models/record.js
5 | export default function toJSONDeep() {
6 | if (!this.$json_names) {
7 | const ownNames = Object.keys(this);
8 | const getComputedNames = function (store, __proto__ = store.__proto__) {
9 | if (__proto__ === Object.prototype) {
10 | return;
11 | }
12 | const descriptors = Object.getOwnPropertyDescriptors(__proto__);
13 | _.forEach(descriptors, (descriptor, name) => {
14 | if (!ownNames.includes(name) && descriptor.get) {
15 | ownNames.push(name);
16 | }
17 | });
18 | getComputedNames(store, __proto__.__proto__);
19 | };
20 |
21 | getComputedNames(this);
22 | this.$json_names = ownNames;
23 | }
24 |
25 | const excludeJsonNames = this.constructor.excludeJsonNames || [];
26 | return _.pick(this, _.without(this.$json_names, ...excludeJsonNames));
27 | }
28 |
--------------------------------------------------------------------------------
/src/utils/upload-files.js:
--------------------------------------------------------------------------------
1 | import { request } from './request';
2 | import { randomString } from './random';
3 |
4 | /**
5 | * 上传文件
6 | *
7 | * @param {String|String[]} tempFilePaths 本地文件地址
8 | * @param {String} type 文件类型,默认是 image,上传视频时传 video
9 | *
10 | * @return {Promise