├── .babelrc
├── .eslintrc.js
├── .gitignore
├── .npmignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── commitlint.config.js
├── doc
└── preview.gif
├── gulpfile.js
├── package.json
├── src
├── extra
│ ├── customActive.js
│ └── redDot.js
├── index.js
├── index.json
├── index.less
├── index.wxml
└── index.wxss
├── test
├── index.test.js
├── utils.js
└── wx.test.js
├── tools
├── build.js
├── checkcomponents.js
├── checkwxss.js
├── config.js
├── demo
│ ├── app.js
│ ├── app.json
│ ├── app.wxss
│ ├── image
│ │ ├── index-pre.png
│ │ ├── index.png
│ │ ├── mine-pre.png
│ │ ├── mine.png
│ │ └── x.png
│ ├── package.json
│ ├── pages
│ │ └── index
│ │ │ ├── index.js
│ │ │ ├── index.json
│ │ │ ├── index.wxml
│ │ │ ├── index.wxss
│ │ │ ├── mine.js
│ │ │ ├── mine.json
│ │ │ ├── mine.wxml
│ │ │ └── mine.wxss
│ └── project.config.json
└── utils.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | ["module-resolver", {
4 | "root": ["./src"],
5 | "alias": {}
6 | }]
7 | ],
8 | "presets": [
9 | ["env", {"loose": true, "modules": "commonjs"}]
10 | ]
11 | }
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'extends': [
3 | 'airbnb-base',
4 | 'plugin:promise/recommended'
5 | ],
6 | 'parserOptions': {
7 | 'ecmaVersion': 9,
8 | 'ecmaFeatures': {
9 | 'jsx': false
10 | },
11 | 'sourceType': 'module'
12 | },
13 | 'env': {
14 | 'es6': true,
15 | 'node': true,
16 | 'jest': true
17 | },
18 | 'plugins': [
19 | 'import',
20 | 'node',
21 | 'promise'
22 | ],
23 | 'rules': {
24 | 'arrow-parens': 'off',
25 | 'comma-dangle': [
26 | 'error',
27 | 'only-multiline'
28 | ],
29 | 'complexity': ['error', 10],
30 | 'func-names': 'off',
31 | 'global-require': 'off',
32 | 'handle-callback-err': [
33 | 'error',
34 | '^(err|error)$'
35 | ],
36 | 'import/no-unresolved': [
37 | 'error',
38 | {
39 | 'caseSensitive': true,
40 | 'commonjs': true,
41 | 'ignore': ['^[^.]']
42 | }
43 | ],
44 | 'import/prefer-default-export': 'off',
45 | 'linebreak-style': 'off',
46 | 'no-catch-shadow': 'error',
47 | 'no-continue': 'off',
48 | 'no-div-regex': 'warn',
49 | 'no-else-return': 'off',
50 | 'no-param-reassign': 'off',
51 | 'no-plusplus': 'off',
52 | 'no-shadow': 'off',
53 | 'no-multi-assign': 'off',
54 | 'no-underscore-dangle': 'off',
55 | 'node/no-deprecated-api': 'error',
56 | 'node/process-exit-as-throw': 'error',
57 | 'object-curly-spacing': [
58 | 'error',
59 | 'never'
60 | ],
61 | 'operator-linebreak': [
62 | 'error',
63 | 'after',
64 | {
65 | 'overrides': {
66 | ':': 'before',
67 | '?': 'before'
68 | }
69 | }
70 | ],
71 | 'prefer-arrow-callback': 'off',
72 | 'prefer-destructuring': 'off',
73 | 'prefer-template': 'off',
74 | 'quote-props': [
75 | 1,
76 | 'as-needed',
77 | {
78 | 'unnecessary': true
79 | }
80 | ],
81 | 'semi': [
82 | 'error',
83 | 'never'
84 | ]
85 | },
86 | 'globals': {
87 | 'window': true,
88 | 'document': true,
89 | 'App': true,
90 | 'Page': true,
91 | 'Component': true,
92 | 'Behavior': true,
93 | 'wx': true,
94 | 'getCurrentPages': true,
95 | '__wxConfig': true
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .DS_Store
3 | package-lock.json
4 |
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 |
11 | miniprogram_dist
12 | miniprogram_dev
13 | node_modules
14 | coverage
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .DS_Store
3 | package-lock.json
4 |
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 |
11 | test
12 | tools
13 | docs
14 | miniprogram_dev
15 | node_modules
16 | coverage
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "prettier.eslintIntegration": true,
3 | "eslint.autoFixOnSave": true
4 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 wechat-miniprogram
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # miniprogram-custom-tab-bar
2 |
3 | 小程序自定义底部导航栏组件
4 |
5 | * 适配iPhone X
6 | * 无需额外配置,甚至不用传参,自动读取app.json中tabBar的配置
7 | * 使用wx.showTabBarRedDot方法可以设置红点
8 | * 可以自定义样式,摆脱微信限制(如borderStyle仅支持black/white的限制)
9 | * 可以定制个性化逻辑
10 |
11 | ## 效果图
12 |
13 | 
14 |
15 | ## 使用
16 |
17 | * npm i miniprogram-custom-tab-bar
18 | * 在tab页面注册并引入组件即可
19 |
20 | ## 如何实现预览图中 中间突出来的图标
21 |
22 | 因为组件原理是拿app.json实现的,我们可以直接在app.json的`"tabBar"`中加入如下代码,其中iconPath可以用绝对路径,也可以用网上路径
23 |
24 | // app.json
25 | "tabBar":{
26 | ...,
27 | "customNode": {
28 | "iconPath": "/image/x.png",
29 | "activeStyle": "transform: rotate(45deg);",
30 | "style": "transform: rotate(0deg);"
31 | },
32 | ...
33 | }
34 |
35 | **组件还在开发中,如果大家需要某些功能可以提一个issue,我会考虑把它加入到组件中去**
36 |
37 | ## 二次开发
38 |
39 | clone 本项目,之后修改src目录中的内容,目录结构基本与小程序插件模板一致,此处是[具体文档](https://github.com/wechat-miniprogram/miniprogram-custom-component)
40 |
41 | 1. 安装依赖:
42 |
43 | ```
44 | npm i 或 yarn
45 | ```
46 |
47 | 2. 执行命令:
48 |
49 | ```
50 | npm run dev
51 | ```
52 |
53 | 默认会在包根目录下生成 miniprogram\_dev 目录,src 中的源代码会被构建并生成到 miniprogram\_dev/components 目录下。如果需要监听文件变化动态构建,则可以执行命令:
54 |
55 | ```
56 | npm run watch
57 | ```
58 |
59 | > ps: 如果 minirpogram\_dev 目录下已存在小程序 demo,执行`npm run dev`则不会再将 tools 下的 demo 拷贝到此目录下。而执行`npm run watch`则会监听 tools 目录下的 demo 变动并进行拷贝。
60 |
61 | 3. 生成的 miniprogram\_dev 目录是一个小程序项目目录,以此目录作为小程序项目目录在开发者工具中打开即可查看自定义组件被使用的效果。
62 |
63 | 4. 进阶:
64 |
65 | * 如果有额外的构建需求,可自行修改 tools 目录中的构建脚本。
66 | * 内置支持 less、sourcemap 等功能,默认关闭。如若需要可以自行修改 tools/config.js 配置文件中相关配置。
67 | * 内置支持多入口构建,如若需要可自行调整 tools/config.js 配置文件的 entry 字段。
68 | * 默认开启 eslint,可自行调整规则或在 tools/config.js 中注释掉 eslint-loader 行来关闭此功能。
69 |
70 | ## 目录结构
71 |
72 | 以下为推荐使用的目录结构,如果有必要开发者也可以自行做一些调整:
73 |
74 | ```
75 | |--miniprogram_dev // 开发环境构建目录
76 | |--miniprogram_dist // 生产环境构建目录
77 | |--src // 源码
78 | | |--components // 通用自定义组件
79 | | |--images // 图片资源
80 | | |
81 | | |--xxx.js/xxx.wxml/xxx.json/xxx.wxss // 暴露的 js 模块/自定义组件入口文件
82 | |
83 | |--test // 测试用例
84 | |--tools // 构建相关代码
85 | | |--demo // demo 小程序目录,开发环境下会被拷贝生成到 miniprogram_dev 目录中
86 | | |--config.js // 构建相关配置文件
87 | |
88 | |--gulpfile.js
89 | ```
90 |
91 | > PS:对外暴露的 js 模块/自定义组件请放在 src 目录下,不宜放置在过深的目录。另外新增的暴露模块需要在 tools/config.js 的 entry 字段中补充,不然不会进行构建。
92 |
93 | ## 测试
94 |
95 | * 执行测试用例:
96 |
97 | ```
98 | npm run test
99 | ```
100 |
101 | * 检测覆盖率:
102 |
103 | ```
104 | npm run coverage
105 | ```
106 |
107 | 测试用例放在 test 目录下,使用 **miniprogram-simulate** 工具集进行测试,[点击此处查看](https://github.com/wechat-miniprogram/miniprogram-simulate/blob/master/README.md)使用方法。在测试中可能需要变更或调整工具集中的一些方法,可在 test/utils 下自行实现。
108 |
109 | ## 其他命令
110 |
111 | * 清空 miniprogram_dist 目录:
112 |
113 | ```
114 | npm run clean
115 | ```
116 |
117 | * 清空 miniprogam_dev 目录:
118 |
119 | ```
120 | npm run clean-dev
121 | ```
122 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {extends: ['@commitlint/config-conventional']}
2 |
--------------------------------------------------------------------------------
/doc/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ljybill/miniprogram-custom-tab-bar/5948e3e25b60e3f473084dc7971a821377d06ab1/doc/preview.gif
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp')
2 | const clean = require('gulp-clean')
3 |
4 | const config = require('./tools/config')
5 | const BuildTask = require('./tools/build')
6 | const id = require('./package.json').name || 'miniprogram-custom-component'
7 |
8 | // 构建任务实例
9 | // eslint-disable-next-line no-new
10 | new BuildTask(id, config.entry)
11 |
12 | // 清空生成目录和文件
13 | gulp.task('clean', gulp.series(() => gulp.src(config.distPath, {read: false, allowEmpty: true}).pipe(clean()), done => {
14 | if (config.isDev) {
15 | return gulp.src(config.demoDist, {read: false, allowEmpty: true})
16 | .pipe(clean())
17 | }
18 |
19 | return done()
20 | }))
21 | // 监听文件变化并进行开发模式构建
22 | gulp.task('watch', gulp.series(`${id}-watch`))
23 | // 开发模式构建
24 | gulp.task('dev', gulp.series(`${id}-dev`))
25 | // 生产模式构建
26 | gulp.task('default', gulp.series(`${id}-default`))
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "miniprogram-custom-tab-bar",
3 | "version": "1.0.3",
4 | "description": "小程序自定义底部导航栏组件",
5 | "main": "miniprogram_dist/index.js",
6 | "scripts": {
7 | "dev": "gulp dev --develop",
8 | "watch": "gulp watch --develop --watch",
9 | "build": "gulp",
10 | "dist": "npm run build",
11 | "clean-dev": "gulp clean --develop",
12 | "clean": "gulp clean",
13 | "test": "jest ./test/* --silent --bail",
14 | "coverage": "jest ./test/* --coverage --bail",
15 | "lint": "eslint \"src/**/*.js\"",
16 | "lint-tools": "eslint \"tools/**/*.js\" --rule \"import/no-extraneous-dependencies: false\""
17 | },
18 | "miniprogram": "miniprogram_dist",
19 | "jest": {
20 | "testEnvironment": "jsdom",
21 | "testURL": "https://jest.test",
22 | "collectCoverageFrom": [
23 | "src/**/*.js"
24 | ],
25 | "moduleDirectories": [
26 | "node_modules",
27 | "src"
28 | ]
29 | },
30 | "repository": {
31 | "type": "git",
32 | "url": "https://github.com/ljybill/miniprogram-custom-tab-bar"
33 | },
34 | "author": "ljybill@aliyun.com",
35 | "license": "MIT",
36 | "devDependencies": {
37 | "babel-core": "^6.26.3",
38 | "babel-loader": "^7.1.5",
39 | "babel-plugin-module-resolver": "^3.2.0",
40 | "babel-preset-env": "^1.7.0",
41 | "colors": "^1.3.1",
42 | "eslint": "^5.14.1",
43 | "eslint-config-airbnb-base": "13.1.0",
44 | "eslint-loader": "^2.1.2",
45 | "eslint-plugin-import": "^2.16.0",
46 | "eslint-plugin-node": "^7.0.1",
47 | "eslint-plugin-promise": "^3.8.0",
48 | "gulp": "^4.0.0",
49 | "gulp-clean": "^0.4.0",
50 | "gulp-if": "^2.0.2",
51 | "gulp-install": "^1.1.0",
52 | "gulp-less": "^4.0.1",
53 | "gulp-rename": "^1.4.0",
54 | "gulp-sourcemaps": "^2.6.5",
55 | "jest": "^23.5.0",
56 | "miniprogram-simulate": "^1.0.0",
57 | "through2": "^2.0.3",
58 | "vinyl": "^2.2.0",
59 | "webpack": "^4.29.5",
60 | "webpack-node-externals": "^1.7.2"
61 | },
62 | "dependencies": {}
63 | }
64 |
--------------------------------------------------------------------------------
/src/extra/customActive.js:
--------------------------------------------------------------------------------
1 | let active = false
2 | const bus = []
3 |
4 | export const getActive = function () {
5 | return active
6 | }
7 |
8 | export const setActive = function (status) {
9 | active = status
10 | bus.forEach(fn => {
11 | if (typeof fn === 'function') {
12 | fn(active)
13 | }
14 | })
15 | }
16 | export const listenActiveChange = function (fn) {
17 | bus.push(fn)
18 | }
19 |
20 | export default {
21 | getActive,
22 | setActive,
23 | listenActiveChange
24 | }
25 |
--------------------------------------------------------------------------------
/src/extra/redDot.js:
--------------------------------------------------------------------------------
1 | function index() {
2 | const showTabBarRedDotOld = wx.showTabBarRedDot
3 | const hideTabBarRedDotOld = wx.hideTabBarRedDot
4 | const redDotList = Array.from({length: __wxConfig.tabBar.list.length}, () => false)
5 | const redDotBus = []
6 | const showTabBarRedDotNew = function (option) {
7 | const {index} = option
8 | if (typeof index !== 'number' || index < 0 || index >= redDotList.length) {
9 | return
10 | }
11 | redDotList[index] = true
12 | redDotBus.forEach(fun => {
13 | if (typeof fun === 'function') {
14 | fun()
15 | }
16 | })
17 | showTabBarRedDotOld(option)
18 | }
19 | const hideTabBarRedDotNew = function (option) {
20 | const {index} = option
21 | if (typeof index !== 'number' || index < 0 || index >= redDotList.length) {
22 | return
23 | }
24 | redDotList[index] = false
25 | redDotBus.forEach(fun => {
26 | if (typeof fun === 'function') {
27 | fun()
28 | }
29 | })
30 | hideTabBarRedDotOld(option)
31 | }
32 | Object.defineProperty(wx, 'showTabBarRedDot', {
33 | enumerable: true,
34 | configurable: true,
35 | writable: true,
36 | value: showTabBarRedDotNew
37 | })
38 | Object.defineProperty(wx, 'hideTabBarRedDot', {
39 | enumerable: true,
40 | configurable: true,
41 | writable: true,
42 | value: hideTabBarRedDotNew
43 | })
44 | return [redDotList, redDotBus]
45 | }
46 |
47 | const [redDotList, redDotBus] = index()
48 |
49 | export const getDotList = function () {
50 | return redDotList || []
51 | }
52 |
53 | export const listenDotListChange = function (fn) {
54 | if (Array.isArray(redDotBus)) {
55 | redDotBus.push(fn)
56 | }
57 | }
58 |
59 | export default {
60 | getDotList,
61 | listenDotListChange
62 | }
63 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import {getDotList, listenDotListChange} from './extra/redDot'
2 | import {getActive, setActive, listenActiveChange} from './extra/customActive'
3 | // 通过app.json配置的 tabBar 解析成组件可用的list
4 | const fixListConfig = function (item, index) {
5 | const result = {} // 使用新对象,类似浅拷贝
6 | result.pagePath = '/' + item.pagePath.replace(/.html$/g, '')
7 | result.iconPath = item.iconData
8 | ? 'data:image/png;base64,' + item.iconData
9 | : '/' + item.iconPath.replace(/\\/g, '/')
10 | result.selectedIconPath = item.selectedIconData
11 | ? 'data:image/png;base64,' + item.selectedIconData
12 | : '/' + item.selectedIconPath.replace(/\\/g, '/')
13 | result.idx = index
14 | result.redDot = false
15 | result.text = item.text
16 | return result
17 | }
18 |
19 | const _tabBar = __wxConfig.tabBar
20 | if (!_tabBar) {
21 | throw new Error('app.json 未定义tabBar')
22 | }
23 | wx.hideTabBar()
24 |
25 | Component({
26 | properties: {
27 | borderColor: {
28 | type: String,
29 | value: ''
30 | }
31 | },
32 | data: {
33 | activeIdx: -1,
34 | config: _tabBar,
35 | list: _tabBar.list.map(fixListConfig),
36 | // 自定义节点
37 | customOrder: Math.floor(_tabBar.list.length / 2) - 1,
38 | customActive: false,
39 | customTransitionTime: '0.3s'
40 | },
41 | methods: {
42 | switchTab(evt) {
43 | const {pagePath} = evt.currentTarget.dataset
44 | wx.switchTab({
45 | url: pagePath
46 | })
47 | },
48 | updateRedDot() {
49 | if (Array.isArray(getDotList())) {
50 | this.setData({
51 | list: this.data.list.map(item => {
52 | item.redDot = getDotList()[item.idx]
53 | return item
54 | })
55 | })
56 | }
57 | },
58 | updateCustomNodeActive(status) {
59 | this.setData({
60 | customActive: status
61 | })
62 | },
63 | handleCustomNodeTap() {
64 | setActive(!this.data.customActive)
65 | }
66 | },
67 | ready() {
68 | this.updateRedDot()
69 | listenDotListChange(this.updateRedDot.bind(this))
70 | if (this.data.config.customNode) {
71 | listenActiveChange(this.updateCustomNodeActive.bind(this))
72 | }
73 | },
74 | pageLifetimes: {
75 | show() {
76 | const pages = getCurrentPages()
77 | const page = pages[pages.length - 1]
78 | const route = page.__route__
79 | const idx = this.data.list.find(item => item.pagePath === `/${route}`).idx
80 | if (this.data.activeIdx !== idx) {
81 | this.setData({
82 | activeIdx: idx
83 | })
84 | }
85 | if (this.data.config.customNode && this.data.customActive !== getActive()) {
86 | this.setData({
87 | customActive: getActive()
88 | })
89 | }
90 | }
91 | }
92 | })
93 |
--------------------------------------------------------------------------------
/src/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "component": true,
3 | "usingComponents": {}
4 | }
--------------------------------------------------------------------------------
/src/index.less:
--------------------------------------------------------------------------------
1 | // out: ./index.wxss, compress: false, sourceMap: false
2 | .tab-bar {
3 | position: fixed;
4 | bottom: 0;
5 | left: 0;
6 | right: 0;
7 | background-color: #fff;
8 | padding-bottom: env(safe-area-inset-bottom);
9 |
10 | &-border {
11 | position: absolute;
12 | top: 0;
13 | left: 0;
14 | right: 0;
15 | border-top: 0.5px solid rgba(0, 0, 0, 0.2);
16 |
17 | &.white {
18 | border-top-color: #fff;
19 | }
20 | }
21 |
22 | &-list {
23 | display: flex;
24 | }
25 |
26 | &-item {
27 | flex: 1;
28 | text-align: center;
29 | padding: 10rpx 0 0;
30 |
31 | &-icon {
32 | position: relative;
33 | margin: 0 auto;
34 | width: 27px;
35 | height: 27px;
36 |
37 | image {
38 | width: 27px;
39 | height: 27px;
40 | }
41 | }
42 |
43 | &-text {
44 | font-size: 20rpx;
45 | }
46 |
47 | &.custom {
48 | .tab-bar-item-icon {
49 | image {
50 | position: absolute;
51 | width: 100rpx;
52 | height: 100rpx;
53 | border-radius: 50%;
54 | top: 50%;
55 | left: 50%;
56 | margin-left: -50rpx;
57 | margin-top: -66rpx;
58 | transform-origin: center center;
59 | transition: transform 0.3s;
60 | }
61 | }
62 | }
63 | }
64 | }
65 |
66 | .red-dot {
67 | position: absolute;
68 | background-color: red;
69 | top: 0;
70 | right: 0;
71 | width: 20rpx;
72 | height: 20rpx;
73 | border-radius: 50%;
74 | overflow: hidden;
75 | transform: translate3d(10rpx, 0, 0);
76 | }
77 |
--------------------------------------------------------------------------------
/src/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{item.text}}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/index.wxss:
--------------------------------------------------------------------------------
1 | .tab-bar {
2 | position: fixed;
3 | bottom: 0;
4 | left: 0;
5 | right: 0;
6 | background-color: #fff;
7 | padding-bottom: env(safe-area-inset-bottom);
8 | }
9 | .tab-bar-border {
10 | position: absolute;
11 | top: 0;
12 | left: 0;
13 | right: 0;
14 | border-top: 0.5px solid rgba(0, 0, 0, 0.2);
15 | }
16 | .tab-bar-border.white {
17 | border-top-color: #fff;
18 | }
19 | .tab-bar-list {
20 | display: flex;
21 | }
22 | .tab-bar-item {
23 | flex: 1;
24 | text-align: center;
25 | padding: 10rpx 0 0;
26 | }
27 | .tab-bar-item-icon {
28 | position: relative;
29 | margin: 0 auto;
30 | width: 27px;
31 | height: 27px;
32 | }
33 | .tab-bar-item-icon image {
34 | width: 27px;
35 | height: 27px;
36 | }
37 | .tab-bar-item-text {
38 | font-size: 20rpx;
39 | }
40 | .tab-bar-item.custom .tab-bar-item-icon image {
41 | position: absolute;
42 | width: 100rpx;
43 | height: 100rpx;
44 | border-radius: 50%;
45 | top: 50%;
46 | left: 50%;
47 | margin-left: -50rpx;
48 | margin-top: -66rpx;
49 | transform-origin: center center;
50 | transition: transform 0.3s;
51 | }
52 | .red-dot {
53 | position: absolute;
54 | background-color: red;
55 | top: 0;
56 | right: 0;
57 | width: 20rpx;
58 | height: 20rpx;
59 | border-radius: 50%;
60 | overflow: hidden;
61 | transform: translate3d(10rpx, 0, 0);
62 | }
63 |
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | const _ = require('./utils')
2 |
3 | test('render', async () => {
4 | const componentId = _.load('index', 'comp')
5 | const component = _.render(componentId, {prop: 'index.test.properties'})
6 |
7 | const parent = document.createElement('parent-wrapper')
8 | component.attach(parent)
9 |
10 | expect(
11 | _.match(
12 | component.dom,
13 | 'index.test.properties-false'
14 | )
15 | ).toBe(true)
16 |
17 | await _.sleep(10)
18 |
19 | expect(
20 | _.match(
21 | component.dom,
22 | 'index.test.properties-true'
23 | )
24 | ).toBe(true)
25 | })
26 |
--------------------------------------------------------------------------------
/test/utils.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const simulate = require('miniprogram-simulate')
3 |
4 | const config = require('../tools/config')
5 |
6 | const srcPath = config.srcPath
7 | const oldLoad = simulate.load
8 | simulate.load = function (componentPath, ...args) {
9 | componentPath = path.join(srcPath, componentPath)
10 | return oldLoad(componentPath, ...args)
11 | }
12 |
13 | module.exports = simulate
14 |
15 | // adjust the simulated wx api
16 | const oldGetSystemInfoSync = global.wx.getSystemInfoSync
17 | global.wx.getSystemInfoSync = function () {
18 | const res = oldGetSystemInfoSync()
19 | res.SDKVersion = '2.4.1'
20 |
21 | return res
22 | }
23 |
--------------------------------------------------------------------------------
/test/wx.test.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-unused-vars
2 | const _ = require('./utils')
3 |
4 | test('wx.getSystemInfo', async () => {
5 | wx.getSystemInfo({
6 | success(res) {
7 | expect(res.errMsg).toBe('getSystemInfo:ok')
8 | },
9 | complete(res) {
10 | expect(res.errMsg).toBe('getSystemInfo:ok')
11 | }
12 | })
13 | })
14 |
15 | test('wx.getSystemInfoSync', async () => {
16 | const info = wx.getSystemInfoSync()
17 | expect(info.SDKVersion).toBe('2.4.1')
18 | expect(info.version).toBe('6.6.3')
19 | })
20 |
--------------------------------------------------------------------------------
/tools/build.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const gulp = require('gulp')
4 | const clean = require('gulp-clean')
5 | const less = require('gulp-less')
6 | const rename = require('gulp-rename')
7 | const gulpif = require('gulp-if')
8 | const sourcemaps = require('gulp-sourcemaps')
9 | const webpack = require('webpack')
10 | const gulpInstall = require('gulp-install')
11 |
12 | const config = require('./config')
13 | const checkComponents = require('./checkcomponents')
14 | const checkWxss = require('./checkwxss')
15 | const _ = require('./utils')
16 |
17 | const wxssConfig = config.wxss || {}
18 | const srcPath = config.srcPath
19 | const distPath = config.distPath
20 |
21 | /**
22 | * 获取 wxss 流
23 | */
24 | function wxss(wxssFileList) {
25 | if (!wxssFileList.length) return false
26 |
27 | return gulp.src(wxssFileList, {cwd: srcPath, base: srcPath})
28 | .pipe(checkWxss.start()) // 开始处理 import
29 | .pipe(gulpif(wxssConfig.less && wxssConfig.sourcemap, sourcemaps.init()))
30 | .pipe(gulpif(wxssConfig.less, less({paths: [srcPath]})))
31 | .pipe(checkWxss.end()) // 结束处理 import
32 | .pipe(rename({extname: '.wxss'}))
33 | .pipe(gulpif(wxssConfig.less && wxssConfig.sourcemap, sourcemaps.write('./')))
34 | .pipe(_.logger(wxssConfig.less ? 'generate' : undefined))
35 | .pipe(gulp.dest(distPath))
36 | }
37 |
38 | /**
39 | * 获取 js 流
40 | */
41 | function js(jsFileMap, scope) {
42 | const webpackConfig = config.webpack
43 | const webpackCallback = (err, stats) => {
44 | if (!err) {
45 | // eslint-disable-next-line no-console
46 | console.log(stats.toString({
47 | assets: true,
48 | cached: false,
49 | colors: true,
50 | children: false,
51 | errors: true,
52 | warnings: true,
53 | version: true,
54 | modules: false,
55 | publicPath: true,
56 | }))
57 | } else {
58 | // eslint-disable-next-line no-console
59 | console.log(err)
60 | }
61 | }
62 |
63 | webpackConfig.entry = jsFileMap
64 | webpackConfig.output.path = distPath
65 |
66 | if (scope.webpackWatcher) {
67 | scope.webpackWatcher.close()
68 | scope.webpackWatcher = null
69 | }
70 |
71 | if (config.isWatch) {
72 | scope.webpackWatcher = webpack(webpackConfig).watch({
73 | ignored: /node_modules/,
74 | }, webpackCallback)
75 | } else {
76 | webpack(webpackConfig).run(webpackCallback)
77 | }
78 | }
79 |
80 | /**
81 | * 拷贝文件
82 | */
83 | function copy(copyFileList) {
84 | if (!copyFileList.length) return false
85 |
86 | return gulp.src(copyFileList, {cwd: srcPath, base: srcPath})
87 | .pipe(_.logger())
88 | .pipe(gulp.dest(distPath))
89 | }
90 |
91 | /**
92 | * 安装依赖包
93 | */
94 | function install() {
95 | return gulp.series(async () => {
96 | const demoDist = config.demoDist
97 | const demoPackageJsonPath = path.join(demoDist, 'package.json')
98 | const packageJson = _.readJson(path.resolve(__dirname, '../package.json'))
99 | const dependencies = packageJson.dependencies || {}
100 |
101 | await _.writeFile(demoPackageJsonPath, JSON.stringify({dependencies}, null, '\t')) // write dev demo's package.json
102 | }, () => {
103 | const demoDist = config.demoDist
104 | const demoPackageJsonPath = path.join(demoDist, 'package.json')
105 |
106 | return gulp.src(demoPackageJsonPath)
107 | .pipe(gulpInstall({production: true}))
108 | })
109 | }
110 |
111 | class BuildTask {
112 | constructor(id, entry) {
113 | if (!entry) return
114 |
115 | this.id = id
116 | this.entries = Array.isArray(config.entry) ? config.entry : [config.entry]
117 | this.copyList = Array.isArray(config.copy) ? config.copy : []
118 | this.componentListMap = {}
119 | this.cachedComponentListMap = {}
120 |
121 | this.init()
122 | }
123 |
124 | init() {
125 | const id = this.id
126 |
127 | /**
128 | * 清空目标目录
129 | */
130 | gulp.task(`${id}-clean-dist`, () => gulp.src(distPath, {read: false, allowEmpty: true}).pipe(clean()))
131 |
132 | /**
133 | * 拷贝 demo 到目标目录
134 | */
135 | let isDemoExists = false
136 | gulp.task(`${id}-demo`, gulp.series(async () => {
137 | const demoDist = config.demoDist
138 |
139 | isDemoExists = await _.checkFileExists(path.join(demoDist, 'project.config.json'))
140 | }, done => {
141 | if (!isDemoExists) {
142 | const demoSrc = config.demoSrc
143 | const demoDist = config.demoDist
144 |
145 | return gulp.src('**/*', {cwd: demoSrc, base: demoSrc})
146 | .pipe(gulp.dest(demoDist))
147 | }
148 |
149 | return done()
150 | }))
151 |
152 | /**
153 | * 安装依赖包
154 | */
155 | gulp.task(`${id}-install`, install())
156 |
157 | /**
158 | * 检查自定义组件
159 | */
160 | gulp.task(`${id}-component-check`, async () => {
161 | const entries = this.entries
162 | const mergeComponentListMap = {}
163 | for (let i = 0, len = entries.length; i < len; i++) {
164 | let entry = entries[i]
165 | entry = path.join(srcPath, `${entry}.json`)
166 | // eslint-disable-next-line no-await-in-loop
167 | const newComponentListMap = await checkComponents(entry)
168 |
169 | _.merge(mergeComponentListMap, newComponentListMap)
170 | }
171 |
172 | this.cachedComponentListMap = this.componentListMap
173 | this.componentListMap = mergeComponentListMap
174 | })
175 |
176 | /**
177 | * 写 json 文件到目标目录
178 | */
179 | gulp.task(`${id}-component-json`, done => {
180 | const jsonFileList = this.componentListMap.jsonFileList
181 |
182 | if (jsonFileList && jsonFileList.length) {
183 | return copy(this.componentListMap.jsonFileList)
184 | }
185 |
186 | return done()
187 | })
188 |
189 | /**
190 | * 拷贝 wxml 文件到目标目录
191 | */
192 | gulp.task(`${id}-component-wxml`, done => {
193 | const wxmlFileList = this.componentListMap.wxmlFileList
194 |
195 | if (wxmlFileList &&
196 | wxmlFileList.length &&
197 | !_.compareArray(this.cachedComponentListMap.wxmlFileList, wxmlFileList)) {
198 | return copy(wxmlFileList)
199 | }
200 |
201 | return done()
202 | })
203 |
204 | /**
205 | * 生成 wxss 文件到目标目录
206 | */
207 | gulp.task(`${id}-component-wxss`, done => {
208 | const wxssFileList = this.componentListMap.wxssFileList
209 |
210 | if (wxssFileList &&
211 | wxssFileList.length &&
212 | !_.compareArray(this.cachedComponentListMap.wxssFileList, wxssFileList)) {
213 | return wxss(wxssFileList, srcPath, distPath)
214 | }
215 |
216 | return done()
217 | })
218 |
219 | /**
220 | * 生成 js 文件到目标目录
221 | */
222 | gulp.task(`${id}-component-js`, done => {
223 | const jsFileList = this.componentListMap.jsFileList
224 |
225 | if (jsFileList &&
226 | jsFileList.length &&
227 | !_.compareArray(this.cachedComponentListMap.jsFileList, jsFileList)) {
228 | js(this.componentListMap.jsFileMap, this)
229 | }
230 |
231 | return done()
232 | })
233 |
234 | /**
235 | * 拷贝相关资源到目标目录
236 | */
237 | gulp.task(`${id}-copy`, gulp.parallel(done => {
238 | const copyList = this.copyList
239 | const copyFileList = copyList.map(dir => path.join(dir, '**/*.!(wxss)'))
240 |
241 | if (copyFileList.length) return copy(copyFileList)
242 |
243 | return done()
244 | }, done => {
245 | const copyList = this.copyList
246 | const copyFileList = copyList.map(dir => path.join(dir, '**/*.wxss'))
247 |
248 | if (copyFileList.length) return wxss(copyFileList, srcPath, distPath)
249 |
250 | return done()
251 | }))
252 |
253 | /**
254 | * 监听 json 变化
255 | */
256 | gulp.task(`${id}-watch-json`, () => gulp.watch(this.componentListMap.jsonFileList, {cwd: srcPath, base: srcPath}, gulp.series(`${id}-component-check`, gulp.parallel(`${id}-component-wxml`, `${id}-component-wxss`, `${id}-component-js`, `${id}-component-json`))))
257 |
258 | /**
259 | * 监听 wxml 变化
260 | */
261 | gulp.task(`${id}-watch-wxml`, () => {
262 | this.cachedComponentListMap.wxmlFileList = null
263 | return gulp.watch(this.componentListMap.wxmlFileList, {cwd: srcPath, base: srcPath}, gulp.series(`${id}-component-wxml`))
264 | })
265 |
266 | /**
267 | * 监听 wxss 变化
268 | */
269 | gulp.task(`${id}-watch-wxss`, () => {
270 | this.cachedComponentListMap.wxssFileList = null
271 | return gulp.watch('**/*.wxss', {cwd: srcPath, base: srcPath}, gulp.series(`${id}-component-wxss`))
272 | })
273 |
274 | /**
275 | * 监听相关资源变化
276 | */
277 | gulp.task(`${id}-watch-copy`, () => {
278 | const copyList = this.copyList
279 | const copyFileList = copyList.map(dir => path.join(dir, '**/*'))
280 | const watchCallback = filePath => copy([filePath])
281 |
282 | return gulp.watch(copyFileList, {cwd: srcPath, base: srcPath})
283 | .on('change', watchCallback)
284 | .on('add', watchCallback)
285 | .on('unlink', watchCallback)
286 | })
287 |
288 | /**
289 | * 监听 demo 变化
290 | */
291 | gulp.task(`${id}-watch-demo`, () => {
292 | const demoSrc = config.demoSrc
293 | const demoDist = config.demoDist
294 | const watchCallback = filePath => gulp.src(filePath, {cwd: demoSrc, base: demoSrc})
295 | .pipe(gulp.dest(demoDist))
296 |
297 | return gulp.watch('**/*', {cwd: demoSrc, base: demoSrc})
298 | .on('change', watchCallback)
299 | .on('add', watchCallback)
300 | .on('unlink', watchCallback)
301 | })
302 |
303 | /**
304 | * 监听安装包列表变化
305 | */
306 | gulp.task(`${id}-watch-install`, () => gulp.watch(path.resolve(__dirname, '../package.json'), install()))
307 |
308 | /**
309 | * 构建相关任务
310 | */
311 | gulp.task(`${id}-build`, gulp.series(`${id}-clean-dist`, `${id}-component-check`, gulp.parallel(`${id}-component-wxml`, `${id}-component-wxss`, `${id}-component-js`, `${id}-component-json`, `${id}-copy`)))
312 |
313 | gulp.task(`${id}-watch`, gulp.series(`${id}-build`, `${id}-demo`, `${id}-install`, gulp.parallel(`${id}-watch-wxml`, `${id}-watch-wxss`, `${id}-watch-json`, `${id}-watch-copy`, `${id}-watch-install`, `${id}-watch-demo`)))
314 |
315 | gulp.task(`${id}-dev`, gulp.series(`${id}-build`, `${id}-demo`, `${id}-install`))
316 |
317 | gulp.task(`${id}-default`, gulp.series(`${id}-build`))
318 | }
319 | }
320 |
321 | module.exports = BuildTask
322 |
--------------------------------------------------------------------------------
/tools/checkcomponents.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const _ = require('./utils')
4 | const config = require('./config')
5 |
6 | const srcPath = config.srcPath
7 |
8 | /**
9 | * 获取 json 路径相关信息
10 | */
11 | function getJsonPathInfo(jsonPath) {
12 | const dirPath = path.dirname(jsonPath)
13 | const fileName = path.basename(jsonPath, '.json')
14 | const relative = path.relative(srcPath, dirPath)
15 | const fileBase = path.join(relative, fileName)
16 |
17 | return {
18 | dirPath, fileName, relative, fileBase
19 | }
20 | }
21 |
22 | /**
23 | * 检测是否包含其他自定义组件
24 | */
25 | const checkProps = ['usingComponents', 'componentGenerics']
26 | async function checkIncludedComponents(jsonPath, componentListMap) {
27 | const json = _.readJson(jsonPath)
28 | if (!json) throw new Error(`json is not valid: "${jsonPath}"`)
29 |
30 | const {dirPath, fileName, fileBase} = getJsonPathInfo(jsonPath)
31 |
32 | for (let i = 0, len = checkProps.length; i < len; i++) {
33 | const checkProp = checkProps[i]
34 | const checkPropValue = json[checkProp] || {}
35 | const keys = Object.keys(checkPropValue)
36 |
37 | for (let j = 0, jlen = keys.length; j < jlen; j++) {
38 | const key = keys[j]
39 | let value = typeof checkPropValue[key] === 'object' ? checkPropValue[key].default : checkPropValue[key]
40 | if (!value) continue
41 |
42 | value = _.transformPath(value, path.sep)
43 |
44 | // 检查相对路径
45 | const componentPath = `${path.join(dirPath, value)}.json`
46 | // eslint-disable-next-line no-await-in-loop
47 | const isExists = await _.checkFileExists(componentPath)
48 | if (isExists) {
49 | // eslint-disable-next-line no-await-in-loop
50 | await checkIncludedComponents(componentPath, componentListMap)
51 | }
52 | }
53 | }
54 |
55 | // 进入存储
56 | componentListMap.wxmlFileList.push(`${fileBase}.wxml`)
57 | componentListMap.wxssFileList.push(`${fileBase}.wxss`)
58 | componentListMap.jsonFileList.push(`${fileBase}.json`)
59 | componentListMap.jsFileList.push(`${fileBase}.js`)
60 |
61 | componentListMap.jsFileMap[fileBase] = `${path.join(dirPath, fileName)}.js`
62 | }
63 |
64 | module.exports = async function (entry) {
65 | const componentListMap = {
66 | wxmlFileList: [],
67 | wxssFileList: [],
68 | jsonFileList: [],
69 | jsFileList: [],
70 |
71 | jsFileMap: {}, // 为 webpack entry 所用
72 | }
73 |
74 | const isExists = await _.checkFileExists(entry)
75 | if (!isExists) {
76 | const {dirPath, fileName, fileBase} = getJsonPathInfo(entry)
77 |
78 | componentListMap.jsFileList.push(`${fileBase}.js`)
79 | componentListMap.jsFileMap[fileBase] = `${path.join(dirPath, fileName)}.js`
80 |
81 | return componentListMap
82 | }
83 |
84 | await checkIncludedComponents(entry, componentListMap)
85 |
86 | return componentListMap
87 | }
88 |
--------------------------------------------------------------------------------
/tools/checkwxss.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const through = require('through2')
3 | const Vinyl = require('vinyl')
4 |
5 | const _ = require('./utils')
6 |
7 | /**
8 | * 获取 import 列表
9 | */
10 | function getImportList(wxss, filePath) {
11 | const reg = /@import\s+(?:(?:"([^"]+)")|(?:'([^"]+)'));/ig
12 | const importList = []
13 | let execRes = reg.exec(wxss)
14 |
15 | while (execRes && (execRes[1] || execRes[2])) {
16 | importList.push({
17 | code: execRes[0],
18 | path: path.join(path.dirname(filePath), execRes[1] || execRes[2]),
19 | })
20 | execRes = reg.exec(wxss)
21 | }
22 |
23 | return importList
24 | }
25 |
26 | /**
27 | * 获取 wxss 内容
28 | */
29 | async function getContent(wxss, filePath, cwd) {
30 | let importList = []
31 |
32 | if (wxss) {
33 | const currentImportList = getImportList(wxss, filePath)
34 |
35 | for (const item of currentImportList) {
36 | // 替换掉 import 语句,不让 less 编译
37 | wxss = wxss.replace(item.code, `/* *updated for miniprogram-custom-component* ${item.code} */`)
38 |
39 | // 处理依赖的 wxss
40 | const importWxss = await _.readFile(item.path)
41 | const importInfo = await getContent(importWxss, item.path, cwd)
42 |
43 | // 获取依赖列表
44 | importList.push(new Vinyl({
45 | cwd,
46 | path: item.path,
47 | contents: Buffer.from(importInfo.wxss, 'utf8'),
48 | }))
49 | importList = importList.concat(importInfo.importList)
50 | }
51 | }
52 |
53 | return {
54 | wxss,
55 | importList,
56 | }
57 | }
58 |
59 | module.exports = {
60 | start() {
61 | return through.obj(function (file, enc, cb) {
62 | if (file.isBuffer()) {
63 | getContent(file.contents.toString('utf8'), file.path, file.cwd).then(res => {
64 | const { wxss, importList } = res
65 |
66 | importList.forEach(importFile => this.push(importFile))
67 |
68 | file.contents = Buffer.from(wxss, 'utf8')
69 | this.push(file)
70 | cb()
71 | }).catch(err => {
72 | // eslint-disable-next-line no-console
73 |
74 | console.warn(`deal with ${file.path} failed: ${err.stack}`)
75 | this.push(file)
76 | cb()
77 | })
78 | } else {
79 | this.push(file)
80 | cb()
81 | }
82 | })
83 | },
84 |
85 | end() {
86 | return through.obj(function (file, enc, cb) {
87 | if (file.isBuffer) {
88 | const reg = /\/\*\s\*updated for miniprogram-custom-component\*\s(@import\s+(?:(?:"([^"]+)")|(?:'([^"]+)'));)\s\*\//ig
89 | const wxss = file.contents.toString('utf8').replace(reg, (all, $1) => $1)
90 |
91 | file.contents = Buffer.from(wxss, 'utf8')
92 | this.push(file)
93 | cb()
94 | } else {
95 | this.push(file)
96 | cb()
97 | }
98 | })
99 | },
100 | }
101 |
--------------------------------------------------------------------------------
/tools/config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const webpack = require('webpack')
4 | const nodeExternals = require('webpack-node-externals')
5 |
6 | const isDev = process.argv.indexOf('--develop') >= 0
7 | const isWatch = process.argv.indexOf('--watch') >= 0
8 | const demoSrc = path.resolve(__dirname, './demo')
9 | const demoDist = path.resolve(__dirname, '../miniprogram_dev')
10 | const src = path.resolve(__dirname, '../src')
11 | const dev = path.join(demoDist, 'components')
12 | const dist = path.resolve(__dirname, '../miniprogram_dist')
13 |
14 | module.exports = {
15 | entry: ['index'],
16 |
17 | isDev,
18 | isWatch,
19 | srcPath: src, // 源目录
20 | distPath: isDev ? dev : dist, // 目标目录
21 |
22 | demoSrc, // demo 源目录
23 | demoDist, // demo 目标目录
24 |
25 | wxss: {
26 | less: true, // 使用 less 来编写 wxss
27 | sourcemap: false, // 生成 less sourcemap
28 | },
29 |
30 | webpack: {
31 | mode: 'production',
32 | output: {
33 | filename: '[name].js',
34 | libraryTarget: 'commonjs2',
35 | },
36 | target: 'node',
37 | externals: [nodeExternals()], // 忽略 node_modules
38 | module: {
39 | rules: [{
40 | test: /\.js$/i,
41 | use: [
42 | 'babel-loader',
43 | 'eslint-loader'
44 | ],
45 | exclude: /node_modules/
46 | }],
47 | },
48 | resolve: {
49 | modules: [src, 'node_modules'],
50 | extensions: ['.js', '.json'],
51 | },
52 | plugins: [
53 | new webpack.DefinePlugin({}),
54 | new webpack.optimize.LimitChunkCountPlugin({maxChunks: 1}),
55 | ],
56 | optimization: {
57 | minimize: false,
58 | },
59 | // devtool: 'nosources-source-map', // 生成 js sourcemap
60 | performance: {
61 | hints: 'warning',
62 | assetFilter: assetFilename => assetFilename.endsWith('.js')
63 | }
64 | },
65 |
66 | copy: ['./images'], // 将会复制到目标目录
67 | }
68 |
--------------------------------------------------------------------------------
/tools/demo/app.js:
--------------------------------------------------------------------------------
1 | App({})
2 |
--------------------------------------------------------------------------------
/tools/demo/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": ["pages/index/index", "pages/index/mine"],
3 | "window": {
4 | "backgroundTextStyle": "light",
5 | "navigationBarBackgroundColor": "#fff",
6 | "navigationBarTitleText": "WeChat",
7 | "navigationBarTextStyle": "black"
8 | },
9 | "tabBar": {
10 | "color": "#000000",
11 | "selectedColor": "#db0028",
12 | "backgroundColor": "#FFFFFF",
13 | "borderStyle": "black",
14 | "customNode": {
15 | "iconPath": "/image/x.png",
16 | "activeStyle": "transform: rotate(45deg);",
17 | "style": "transform: rotate(0deg);"
18 | },
19 | "list": [
20 | {
21 | "iconPath": "/image/index.png",
22 | "selectedIconPath": "/image/index-pre.png",
23 | "pagePath": "pages/index/index",
24 | "text": "首页"
25 | },
26 | {
27 | "iconPath": "/image/mine.png",
28 | "selectedIconPath": "/image/mine-pre.png",
29 | "pagePath": "pages/index/mine",
30 | "text": "我的"
31 | }
32 | ]
33 | },
34 | "usingComponents": {
35 | "custom-tab-bar": "/components/index"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tools/demo/app.wxss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ljybill/miniprogram-custom-tab-bar/5948e3e25b60e3f473084dc7971a821377d06ab1/tools/demo/app.wxss
--------------------------------------------------------------------------------
/tools/demo/image/index-pre.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ljybill/miniprogram-custom-tab-bar/5948e3e25b60e3f473084dc7971a821377d06ab1/tools/demo/image/index-pre.png
--------------------------------------------------------------------------------
/tools/demo/image/index.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ljybill/miniprogram-custom-tab-bar/5948e3e25b60e3f473084dc7971a821377d06ab1/tools/demo/image/index.png
--------------------------------------------------------------------------------
/tools/demo/image/mine-pre.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ljybill/miniprogram-custom-tab-bar/5948e3e25b60e3f473084dc7971a821377d06ab1/tools/demo/image/mine-pre.png
--------------------------------------------------------------------------------
/tools/demo/image/mine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ljybill/miniprogram-custom-tab-bar/5948e3e25b60e3f473084dc7971a821377d06ab1/tools/demo/image/mine.png
--------------------------------------------------------------------------------
/tools/demo/image/x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ljybill/miniprogram-custom-tab-bar/5948e3e25b60e3f473084dc7971a821377d06ab1/tools/demo/image/x.png
--------------------------------------------------------------------------------
/tools/demo/package.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/tools/demo/pages/index/index.js:
--------------------------------------------------------------------------------
1 | const app = getApp();
2 |
3 | Page({
4 | data: {},
5 | onLoad: function() {},
6 | setRedDot() {
7 | wx.showTabBarRedDot({
8 | index: 1
9 | });
10 | },
11 | hideRedDot() {
12 | wx.hideTabBarRedDot({
13 | index: 1
14 | });
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/tools/demo/pages/index/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {}
3 | }
--------------------------------------------------------------------------------
/tools/demo/pages/index/index.wxml:
--------------------------------------------------------------------------------
1 | 直接复制custom-tab-bar文件夹到你的项目,并配置全局组件即可使用
2 |
3 | 设置红点方法测试
4 |
5 |
6 |
--------------------------------------------------------------------------------
/tools/demo/pages/index/index.wxss:
--------------------------------------------------------------------------------
1 | .hr {
2 | margin: 10rpx 0;
3 | border-top: 10rpx solid #eee;
4 | }
--------------------------------------------------------------------------------
/tools/demo/pages/index/mine.js:
--------------------------------------------------------------------------------
1 | // index/mine.js
2 | Page({
3 |
4 | /**
5 | * 页面的初始数据
6 | */
7 | data: {
8 |
9 | },
10 |
11 | /**
12 | * 生命周期函数--监听页面加载
13 | */
14 | onLoad: function (options) {
15 |
16 | },
17 |
18 | /**
19 | * 生命周期函数--监听页面初次渲染完成
20 | */
21 | onReady: function () {
22 |
23 | },
24 |
25 | /**
26 | * 生命周期函数--监听页面显示
27 | */
28 | onShow: function () {
29 |
30 | },
31 |
32 | /**
33 | * 生命周期函数--监听页面隐藏
34 | */
35 | onHide: function () {
36 |
37 | },
38 |
39 | /**
40 | * 生命周期函数--监听页面卸载
41 | */
42 | onUnload: function () {
43 |
44 | },
45 |
46 | /**
47 | * 页面相关事件处理函数--监听用户下拉动作
48 | */
49 | onPullDownRefresh: function () {
50 |
51 | },
52 |
53 | /**
54 | * 页面上拉触底事件的处理函数
55 | */
56 | onReachBottom: function () {
57 |
58 | },
59 |
60 | /**
61 | * 用户点击右上角分享
62 | */
63 | onShareAppMessage: function () {
64 |
65 | }
66 | })
--------------------------------------------------------------------------------
/tools/demo/pages/index/mine.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {}
3 | }
--------------------------------------------------------------------------------
/tools/demo/pages/index/mine.wxml:
--------------------------------------------------------------------------------
1 | index/mine.wxml
2 |
--------------------------------------------------------------------------------
/tools/demo/pages/index/mine.wxss:
--------------------------------------------------------------------------------
1 | /* index/mine.wxss */
--------------------------------------------------------------------------------
/tools/demo/project.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "项目配置文件。",
3 | "packOptions": {
4 | "ignore": []
5 | },
6 | "setting": {
7 | "urlCheck": true,
8 | "es6": true,
9 | "postcss": true,
10 | "minified": true,
11 | "newFeature": true,
12 | "nodeModules": true
13 | },
14 | "compileType": "miniprogram",
15 | "libVersion": "2.2.3",
16 | "appid": "",
17 | "projectname": "miniprogram-demo",
18 | "isGameTourist": false,
19 | "condition": {
20 | "search": {
21 | "current": -1,
22 | "list": []
23 | },
24 | "conversation": {
25 | "current": -1,
26 | "list": []
27 | },
28 | "game": {
29 | "currentL": -1,
30 | "list": []
31 | },
32 | "miniprogram": {
33 | "current": -1,
34 | "list": []
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/tools/utils.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 |
4 | // eslint-disable-next-line no-unused-vars
5 | const colors = require('colors')
6 | const through = require('through2')
7 |
8 | /**
9 | * 异步函数封装
10 | */
11 | function wrap(func, scope) {
12 | return function (...args) {
13 | if (args.length) {
14 | const temp = args.pop()
15 | if (typeof temp !== 'function') {
16 | args.push(temp)
17 | }
18 | }
19 |
20 | return new Promise(function (resolve, reject) {
21 | args.push(function (err, data) {
22 | if (err) reject(err)
23 | else resolve(data)
24 | })
25 |
26 | func.apply((scope || null), args)
27 | })
28 | }
29 | }
30 |
31 | const accessSync = wrap(fs.access)
32 | const statSync = wrap(fs.stat)
33 | const renameSync = wrap(fs.rename)
34 | const mkdirSync = wrap(fs.mkdir)
35 | const readFileSync = wrap(fs.readFile)
36 | const writeFileSync = wrap(fs.writeFile)
37 |
38 | /**
39 | * 调整路径分隔符
40 | */
41 | function transformPath(filePath, sep = '/') {
42 | return filePath.replace(/[\\/]/g, sep)
43 | }
44 |
45 | /**
46 | * 检查文件是否存在
47 | */
48 | async function checkFileExists(filePath) {
49 | try {
50 | await accessSync(filePath)
51 | return true
52 | } catch (err) {
53 | return false
54 | }
55 | }
56 |
57 | /**
58 | * 递归创建目录
59 | */
60 | async function recursiveMkdir(dirPath) {
61 | const prevDirPath = path.dirname(dirPath)
62 | try {
63 | await accessSync(prevDirPath)
64 | } catch (err) {
65 | // 上一级目录不存在
66 | await recursiveMkdir(prevDirPath)
67 | }
68 |
69 | try {
70 | await accessSync(dirPath)
71 |
72 | const stat = await statSync(dirPath)
73 | if (stat && !stat.isDirectory()) {
74 | // 目标路径存在,但不是目录
75 | await renameSync(dirPath, `${dirPath}.bak`) // 将此文件重命名为 .bak 后缀
76 | await mkdirSync(dirPath)
77 | }
78 | } catch (err) {
79 | // 目标路径不存在
80 | await mkdirSync(dirPath)
81 | }
82 | }
83 |
84 | /**
85 | * 读取 json
86 | */
87 | function readJson(filePath) {
88 | try {
89 | // eslint-disable-next-line import/no-dynamic-require
90 | const content = require(filePath)
91 | delete require.cache[require.resolve(filePath)]
92 | return content
93 | } catch (err) {
94 | return null
95 | }
96 | }
97 |
98 | /**
99 | * 读取文件
100 | */
101 | async function readFile(filePath) {
102 | try {
103 | return await readFileSync(filePath, 'utf8')
104 | } catch (err) {
105 | // eslint-disable-next-line no-console
106 | return console.error(err)
107 | }
108 | }
109 |
110 | /**
111 | * 写文件
112 | */
113 | async function writeFile(filePath, data) {
114 | try {
115 | await recursiveMkdir(path.dirname(filePath))
116 | return await writeFileSync(filePath, data, 'utf8')
117 | } catch (err) {
118 | // eslint-disable-next-line no-console
119 | return console.error(err)
120 | }
121 | }
122 |
123 | /**
124 | * 时间格式化
125 | */
126 | function format(time, reg) {
127 | const date = typeof time === 'string' ? new Date(time) : time
128 | const map = {}
129 | map.yyyy = date.getFullYear()
130 | map.yy = ('' + map.yyyy).substr(2)
131 | map.M = date.getMonth() + 1
132 | map.MM = (map.M < 10 ? '0' : '') + map.M
133 | map.d = date.getDate()
134 | map.dd = (map.d < 10 ? '0' : '') + map.d
135 | map.H = date.getHours()
136 | map.HH = (map.H < 10 ? '0' : '') + map.H
137 | map.m = date.getMinutes()
138 | map.mm = (map.m < 10 ? '0' : '') + map.m
139 | map.s = date.getSeconds()
140 | map.ss = (map.s < 10 ? '0' : '') + map.s
141 |
142 | return reg.replace(/\byyyy|yy|MM|M|dd|d|HH|H|mm|m|ss|s\b/g, $1 => map[$1])
143 | }
144 |
145 | /**
146 | * 日志插件
147 | */
148 | function logger(action = 'copy') {
149 | return through.obj(function (file, enc, cb) {
150 | const type = path.extname(file.path).slice(1).toLowerCase()
151 |
152 | // eslint-disable-next-line no-console
153 | console.log(`[${format(new Date(), 'yyyy-MM-dd HH:mm:ss').grey}] [${action.green} ${type.green}] ${'=>'.cyan} ${file.path}`)
154 |
155 | this.push(file)
156 | cb()
157 | })
158 | }
159 |
160 | /**
161 | * 比较数组是否相等
162 | */
163 | function compareArray(arr1, arr2) {
164 | if (!Array.isArray(arr1) || !Array.isArray(arr2)) return false
165 | if (arr1.length !== arr2.length) return false
166 |
167 | for (let i = 0, len = arr1.length; i < len; i++) {
168 | if (arr1[i] !== arr2[i]) return false
169 | }
170 |
171 | return true
172 | }
173 |
174 | /**
175 | * 合并两个对象
176 | */
177 | function merge(obj1, obj2) {
178 | Object.keys(obj2).forEach(key => {
179 | if (Array.isArray(obj1[key]) && Array.isArray(obj2[key])) {
180 | obj1[key] = obj1[key].concat(obj2[key])
181 | } else if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object') {
182 | obj1[key] = Object.assign(obj1[key], obj2[key])
183 | } else {
184 | obj1[key] = obj2[key]
185 | }
186 | })
187 |
188 | return obj1
189 | }
190 |
191 | /**
192 | * 获取 id
193 | */
194 | let seed = +new Date()
195 | function getId() {
196 | return ++seed
197 | }
198 |
199 | module.exports = {
200 | wrap,
201 | transformPath,
202 |
203 | checkFileExists,
204 | readJson,
205 | readFile,
206 | writeFile,
207 |
208 | logger,
209 | format,
210 | compareArray,
211 | merge,
212 | getId,
213 | }
214 |
--------------------------------------------------------------------------------