zengh
2021-07-01 198408c04745fba7d9170a1c0a79468334c88740
首页修改
6 files modified
170 files added
29696 ■■■■■ changed files
App.vue 6 ●●●● patch | view | raw | blame | history
api/demo.js 14 ●●●●● patch | view | raw | blame | history
api/mock/home.js 147 ●●●●● patch | view | raw | blame | history
api/mock/service.js 152 ●●●●● patch | view | raw | blame | history
api/user.js 33 ●●●●● patch | view | raw | blame | history
main.js 4 ●●●● patch | view | raw | blame | history
package-lock.json 50 ●●●●● patch | view | raw | blame | history
pages.json 13 ●●●●● patch | view | raw | blame | history
pages/home/home.vue 482 ●●●●● patch | view | raw | blame | history
pages/loging/loging.vue 2 ●●● patch | view | raw | blame | history
static/images/home/banner.png patch | view | raw | blame | history
static/images/home/date.png patch | view | raw | blame | history
static/images/home/focus.png patch | view | raw | blame | history
static/images/home/message.png patch | view | raw | blame | history
static/images/home/n1.png patch | view | raw | blame | history
static/images/home/n2.png patch | view | raw | blame | history
static/images/home/n3.png patch | view | raw | blame | history
static/images/home/n4.png patch | view | raw | blame | history
static/images/home/n5.png patch | view | raw | blame | history
static/images/home/notice.png patch | view | raw | blame | history
static/images/home/qr.png patch | view | raw | blame | history
static/images/home/s1.png patch | view | raw | blame | history
static/images/home/s2.png patch | view | raw | blame | history
static/images/home/s3.png patch | view | raw | blame | history
static/images/home/s4.png patch | view | raw | blame | history
static/images/home/s5.png patch | view | raw | blame | history
static/images/home/s6.png patch | view | raw | blame | history
static/images/home/s7.png patch | view | raw | blame | history
static/images/home/s8.png patch | view | raw | blame | history
static/images/home/search.png patch | view | raw | blame | history
static/images/home/service.png patch | view | raw | blame | history
static/style/app.scss 81 ●●●●● patch | view | raw | blame | history
uni.scss 2 ●●●●● patch | view | raw | blame | history
uview-ui/LICENSE 21 ●●●●● patch | view | raw | blame | history
uview-ui/README.md 106 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-action-sheet/u-action-sheet.vue 190 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-alert-tips/u-alert-tips.vue 256 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-avatar-cropper/u-avatar-cropper.vue 290 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-avatar-cropper/weCropper.js 1265 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-avatar/u-avatar.vue 244 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-back-top/u-back-top.vue 153 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-badge/u-badge.vue 216 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-button/u-button.vue 596 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-calendar/u-calendar.vue 639 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-car-keyboard/u-car-keyboard.vue 257 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-card/u-card.vue 299 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-cell-group/u-cell-group.vue 70 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-cell-item/u-cell-item.vue 316 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-checkbox-group/u-checkbox-group.vue 123 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-checkbox/u-checkbox.vue 284 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-circle-progress/u-circle-progress.vue 220 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-circle-progress/u-line-progress/u-line-progress.vue 147 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-col/u-col.vue 156 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-collapse-item/u-collapse-item.vue 204 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-collapse/u-collapse.vue 99 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-column-notice/u-column-notice.vue 237 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-count-down/u-count-down.vue 318 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-count-to/u-count-to.vue 241 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-divider/u-divider.vue 153 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-dropdown-item/u-dropdown-item.vue 132 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-dropdown/u-dropdown.vue 298 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-empty/u-empty.vue 193 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-field/u-field.vue 384 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-form-item/u-form-item.vue 431 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-form/u-form.vue 134 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-full-screen/u-full-screen.vue 52 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-gap/u-gap.vue 54 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-grid-item/u-grid-item.vue 126 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-grid/u-grid.vue 108 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-icon/u-icon.vue 336 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-image/u-image.vue 267 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-index-anchor/u-index-anchor.vue 89 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-index-list/u-index-list.vue 315 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-input/u-input.vue 387 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-keyboard/u-keyboard.vue 217 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-lazy-load/u-lazy-load.vue 244 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-line-progress/u-line-progress.vue 147 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-line/u-line.vue 84 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-link/u-link.vue 89 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-loading-page/u-loading-page.vue 25 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-loading/u-loading.vue 103 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-loadmore/u-loadmore.vue 203 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-mask/u-mask.vue 123 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-message-input/u-message-input.vue 311 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-modal/u-modal.vue 283 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-navbar/u-navbar.vue 315 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-no-network/u-no-network.vue 233 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-notice-bar/u-notice-bar.vue 272 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-number-box/u-number-box.vue 363 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-number-keyboard/u-number-keyboard.vue 158 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-parse/libs/CssHandler.js 100 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-parse/libs/MpHtmlParser.js 580 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-parse/libs/config.js 80 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-parse/libs/handler.wxs 22 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-parse/libs/trees.vue 505 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-parse/u-parse.vue 645 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-picker/u-picker.vue 676 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-popup/u-popup.vue 456 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-radio-group/u-radio-group.vue 128 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-radio/u-radio.vue 271 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-rate/u-rate.vue 275 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-read-more/u-read-more.vue 179 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-row-notice/u-row-notice.vue 269 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-row/u-row.vue 84 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-search/u-search.vue 342 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-section/u-section.vue 154 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-select/u-select.vue 417 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-skeleton/u-skeleton.vue 199 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-slider/u-slider.vue 257 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-steps/u-steps.vue 200 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-sticky/u-sticky.vue 157 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-subsection/u-subsection.vue 355 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-swipe-action/u-swipe-action.vue 255 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-swiper/u-swiper.vue 340 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-switch/u-switch.vue 163 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-tabbar/u-tabbar.vue 330 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-table/u-table.vue 84 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-tabs-swiper/u-tabs-swiper.vue 488 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-tabs/u-tabs.vue 369 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-tag/u-tag.vue 294 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-td/u-td.vue 66 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-th/u-th.vue 62 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-time-line-item/u-time-line-item.vue 83 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-time-line/u-time-line.vue 43 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-toast/u-toast.vue 220 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-top-tips/u-top-tips.vue 121 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-tr/u-tr.vue 25 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-upload/u-upload.vue 654 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-verification-code/u-verification-code.vue 164 ●●●●● patch | view | raw | blame | history
uview-ui/components/u-waterfall/u-waterfall.vue 176 ●●●●● patch | view | raw | blame | history
uview-ui/iconfont.css 910 ●●●●● patch | view | raw | blame | history
uview-ui/index.js 141 ●●●●● patch | view | raw | blame | history
uview-ui/index.scss 23 ●●●●● patch | view | raw | blame | history
uview-ui/libs/config/config.js 15 ●●●●● patch | view | raw | blame | history
uview-ui/libs/config/zIndex.js 20 ●●●●● patch | view | raw | blame | history
uview-ui/libs/css/color.scss 155 ●●●●● patch | view | raw | blame | history
uview-ui/libs/css/common.scss 176 ●●●●● patch | view | raw | blame | history
uview-ui/libs/css/style.components.scss 7 ●●●●● patch | view | raw | blame | history
uview-ui/libs/css/style.h5.scss 8 ●●●●● patch | view | raw | blame | history
uview-ui/libs/css/style.mp.scss 72 ●●●●● patch | view | raw | blame | history
uview-ui/libs/css/style.nvue.scss 3 ●●●●● patch | view | raw | blame | history
uview-ui/libs/css/style.vue.scss 175 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/$parent.js 18 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/addUnit.js 8 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/bem.js 5 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/color.js 37 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/colorGradient.js 134 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/debounce.js 29 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/deepClone.js 23 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/deepMerge.js 30 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/getParent.js 47 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/guid.js 41 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/md5.js 385 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/queryParams.js 58 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/random.js 10 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/randomArray.js 7 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/route.js 122 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/sys.js 9 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/test.js 232 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/throttle.js 32 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/timeFormat.js 51 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/timeFrom.js 47 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/toast.js 9 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/trim.js 15 ●●●●● patch | view | raw | blame | history
uview-ui/libs/function/type2icon.js 35 ●●●●● patch | view | raw | blame | history
uview-ui/libs/mixin/mixin.js 64 ●●●●● patch | view | raw | blame | history
uview-ui/libs/mixin/mpShare.js 18 ●●●●● patch | view | raw | blame | history
uview-ui/libs/request/index.js 169 ●●●●● patch | view | raw | blame | history
uview-ui/libs/store/index.js 19 ●●●●● patch | view | raw | blame | history
uview-ui/libs/util/area.js 1 ●●●● patch | view | raw | blame | history
uview-ui/libs/util/async-validator.js 1356 ●●●●● patch | view | raw | blame | history
uview-ui/libs/util/city.js 1 ●●●● patch | view | raw | blame | history
uview-ui/libs/util/emitter.js 51 ●●●●● patch | view | raw | blame | history
uview-ui/libs/util/province.js 1 ●●●● patch | view | raw | blame | history
uview-ui/package.json 39 ●●●●● patch | view | raw | blame | history
uview-ui/theme.scss 38 ●●●●● patch | view | raw | blame | history
App.vue
@@ -47,8 +47,12 @@
    }
</script>
<style>
<style lang="scss">
    @import url("/components/feng-parse/parse.css");
    /*uview全局样式*/
    @import "uview-ui/index.scss";
    /*app全局样式*/
    @import 'static/style/app.scss';
    uni-page-wrapper {
        width: 100%;
        height: 100%;
api/demo.js
New file
@@ -0,0 +1,14 @@
import http from '@/http/api.js'
// 获取验证码
const captcha = (data) => {
    return http.request({
        url: '/blade-auth/oauth/captcha',
        method: 'GET',
        data
    })
}
export default {
    captcha,
}
api/mock/home.js
New file
@@ -0,0 +1,147 @@
// 获取部门数据
export function fakePosition() {
    return new Promise((resolute, reject) => {
        try {
            const detail = {
                position: "研发部",
                positionArr: ['研发部', '产品部']
            }
            resolute(detail);
        } catch (e) {
            //模拟接口请求失败
            reject(e);
        }
    })
}
// 获取公告数据
export function fakeBannerList() {
    return new Promise((resolute, reject) => {
        try {
            const list = [{
                id: '1',
                img: '/static/images/home/banner.png',
                url: ''
            }, {
                id: '2',
                img: '/static/images/home/banner.png',
                url: ''
            }, {
                id: '3',
                img: '/static/images/home/banner.png',
                url: ''
            }];
            resolute(list);
        } catch (e) {
            //模拟接口请求失败
            reject(e);
        }
    })
}
// 获取公告数据
export function fakeNoticeList() {
    return new Promise((resolute, reject) => {
        try {
            const list = [{
                id: '1',
                img: '/static/images/home/focus.png',
                title: '神舟十二号载人发射任务取得圆满成功'
            }, {
                id: '2',
                img: '/static/images/home/service.png',
                title: '全国累计报告接种新冠疫苗超9亿剂次'
            }];
            resolute(list);
        } catch (e) {
            //模拟接口请求失败
            reject(e);
        }
    })
}
// 获取顶部按钮数据
export function fakeNavButton() {
    return new Promise((resolute, reject) => {
        try {
            const list = [{
                    name: '我要看',
                    img: '/static/images/home/n1.png',
                    url: ''
                },
                {
                    name: '我要查',
                    img: '/static/images/home/n2.png',
                    url: ''
                },
                {
                    name: '我要报',
                    img: '/static/images/home/n3.png',
                    url: ''
                },
                {
                    name: '我要说',
                    img: '/static/images/home/n4.png',
                    url: ''
                },
                {
                    name: '我要巡',
                    img: '/static/images/home/n5.png',
                    url: ''
                }
            ];
            resolute(list);
        } catch (e) {
            //模拟接口请求失败
            reject(e);
        }
    })
}
// 获取服务按钮数据
export function fakeServiceButton() {
    return new Promise((resolute, reject) => {
        try {
            const list = [{
                    name: '人口登记',
                    img: '/static/images/home/s1.png',
                    url: ''
                },
                {
                    name: '工商年报',
                    img: '/static/images/home/s2.png',
                    url: ''
                },
                {
                    name: '税务登记',
                    img: '/static/images/home/s3.png',
                    url: ''
                },
                {
                    name: '健康码',
                    img: '/static/images/home/s4.png',
                    url: ''
                },
                {
                    name: '便民服务',
                    img: '/static/images/home/s5.png',
                    url: ''
                },
                {
                    name: '通办大厅',
                    img: '/static/images/home/s6.png',
                    url: ''
                },
                {
                    name: '智能问答',
                    img: '/static/images/home/s7.png',
                    url: ''
                }
            ];
            resolute(list);
        } catch (e) {
            //模拟接口请求失败
            reject(e);
        }
    })
}
api/mock/service.js
New file
@@ -0,0 +1,152 @@
// 获取服务数据
export function fakeSubscribeList() {
    return new Promise((resolute, reject) => {
        try {
            const list = [{
                    img: '/static/images/service/s1.png',
                    name: '违章查询',
                    url: ''
                },
                {
                    img: '/static/images/service/s2.png',
                    name: '限行信息',
                    url: ''
                },
                {
                    img: '/static/images/service/s3.png',
                    name: '油价查询',
                    url: ''
                },
                {
                    img: '/static/images/service/s4.png',
                    name: '消费记录',
                    url: ''
                },
                {
                    img: '/static/images/service/s5.png',
                    name: '保养记录',
                    url: ''
                },
                {
                    img: '/static/images/service/s6.png',
                    name: '油耗记录',
                    url: ''
                },
                {
                    img: '/static/images/service/s7.png',
                    name: '周边查询',
                    url: ''
                },
                {
                    img: '/static/images/service/s8.png',
                    name: '教育咨询',
                    url: ''
                }
            ];
            resolute(list);
        } catch (e) {
            //模拟接口请求失败
            reject(e);
        }
    })
}
export function fakeServiceList() {
    return new Promise((resolute, reject) => {
        try {
            const list = [{
                    name: '交通出行',
                    list: [{
                            img: '/static/images/service/t1.png',
                            name: '地铁',
                            url: ''
                        },
                        {
                            img: '/static/images/service/t2.png',
                            name: '公交',
                            url: ''
                        },
                        {
                            img: '/static/images/service/t3.png',
                            name: '自驾',
                            url: ''
                        },
                        {
                            img: '/static/images/service/t4.png',
                            name: '飞机',
                            url: ''
                        },
                        {
                            img: '/static/images/service/t5.png',
                            name: '动车',
                            url: ''
                        },
                        {
                            img: '/static/images/service/t6.png',
                            name: '火车',
                            url: ''
                        },
                        {
                            img: '/static/images/service/t7.png',
                            name: '摩托车',
                            url: ''
                        },
                        {
                            img: '/static/images/service/t8.png',
                            name: '自行车',
                            url: ''
                        }
                    ]
                },
                {
                    name: '社会保险',
                    list: [{
                            img: '/static/images/service/b1.png',
                            name: '人身保险',
                            url: ''
                        },
                        {
                            img: '/static/images/service/b2.png',
                            name: '财产保险',
                            url: ''
                        },
                        {
                            img: '/static/images/service/b3.png',
                            name: '火灾保险',
                            url: ''
                        },
                        {
                            img: '/static/images/service/b4.png',
                            name: '运输保险',
                            url: ''
                        },
                        {
                            img: '/static/images/service/b5.png',
                            name: '工程保险',
                            url: ''
                        },
                        {
                            img: '/static/images/service/b6.png',
                            name: '盗窃保险',
                            url: ''
                        },
                        {
                            img: '/static/images/service/b7.png',
                            name: '农业保险',
                            url: ''
                        },
                        {
                            img: '/static/images/service/b8.png',
                            name: '信用保险',
                            url: ''
                        }
                    ]
                },
            ]
            resolute(list);
        } catch (e) {
            //模拟接口请求失败
            reject(e);
        }
    })
}
api/user.js
New file
@@ -0,0 +1,33 @@
import http from '@/http/api.js'
// 获取token
const token = (tenantId, username, password, type) => {
    return http.request({
        url: '/blade-auth/oauth/token',
        method: 'POST',
        header: {
            'Tenant-Id': tenantId
        },
        params: {
            tenantId,
            username,
            password,
            grant_type: "password",
            scope: "all",
            type
        }
    })
}
// 获取用户信息
const userInfo = () => {
    return http.request({
        url: '/blade-user/info',
        method: 'GET',
    })
}
export default {
    token,
    userInfo
}
main.js
@@ -4,6 +4,10 @@
import store from './store' //vuex
Vue.prototype.$store = store
// 引入全局uView
import uView from 'uview-ui'
Vue.use(uView);
import WxStorage from "static/lib/wxStorage.js" //微信Storage
Vue.prototype.$WxStorage = WxStorage
package-lock.json
@@ -1,54 +1,6 @@
{
  "name": "qfqk-android",
  "lockfileVersion": 2,
  "requires": true,
  "packages": {
    "": {
      "dependencies": {
        "axios": "^0.21.1",
        "js-md5": "^0.7.3",
        "uni-read-pages": "^1.0.5"
      }
    },
    "node_modules/axios": {
      "version": "0.21.1",
      "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
      "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
      "dependencies": {
        "follow-redirects": "^1.10.0"
      }
    },
    "node_modules/follow-redirects": {
      "version": "1.14.1",
      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz",
      "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==",
      "funding": [
        {
          "type": "individual",
          "url": "https://github.com/sponsors/RubenVerborgh"
        }
      ],
      "engines": {
        "node": ">=4.0"
      },
      "peerDependenciesMeta": {
        "debug": {
          "optional": true
        }
      }
    },
    "node_modules/js-md5": {
      "version": "0.7.3",
      "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz",
      "integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ=="
    },
    "node_modules/uni-read-pages": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/uni-read-pages/-/uni-read-pages-1.0.5.tgz",
      "integrity": "sha512-GkrrZ0LX0vn9R5k6RKEi0Ez3Q3e2vUpjXQ8Z6/K/d28KudI9ajqgt8WEjQFlG5EPm1K6uTArN8LlqmZTEixDUA==",
      "hasInstallScript": true
    }
  },
  "lockfileVersion": 1,
  "dependencies": {
    "axios": {
      "version": "0.21.1",
pages.json
@@ -1,4 +1,7 @@
{
    "easycom": {
        "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
    },
    "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
        {
            "path": "pages/loging/loging",
@@ -9,6 +12,14 @@
                //     "animationType": "fade-in",
                //     "animationDuration": 300
                // }
            }
        },
        {
            "path": "pages/home/home",
            "style": {
                "navigationBarTitleText": "首页",
                "enablePullDownRefresh": false,
                "navigationStyle": "custom"
            }
        },
        {
@@ -286,7 +297,7 @@
        "selectedColor": "#0faeff",
        "backgroundColor": "#ffffff",
        "list": [{
                "pagePath": "pages/index/index",
                "pagePath": "pages/home/home",
                "text": "首页",
                "iconPath": "static/tabbar/index01.png",
                "selectedIconPath": "static/tabbar/index02.png"
pages/home/home.vue
New file
@@ -0,0 +1,482 @@
<template>
    <view class="container">
        <!-- 头部 start -->
        <view class="head">
            <u-navbar :is-fixed="false" :border-bottom="false" :is-back="false" title=""
                :background="{ background: '#0BB9C8' }">
                <view class="nav-wrap">
                    <picker mode="selector" :range="positionArr" range-key="name" @change="changePicker"
                        class="picker-box">
                        {{ position }}
                        <u-icon name="arrow-down" class="arrow" size="18" color="#C9C9C9"></u-icon>
                    </picker>
                    <image src="/static/images/rider.png" class="rider" mode="widthFix" v-if="!focus"></image>
                    <view class="search-input" v-else>
                        <u-search height="50" placeholder="关键字" @blur="handleSearchBlur" :show-action="false"
                            v-model="keyword"></u-search>
                    </view>
                    <view class="tool">
                        <image src="/static/images/home/search.png" class="icon search-icon" mode="widthFix"
                            @click="handleFocus" v-if="!focus"></image>
                        <image src="/static/images/home/message.png" class="icon message-icon" mode="widthFix"></image>
                        <image src="/static/images/home/qr.png" class="icon qr-icon" mode="widthFix"></image>
                    </view>
                </view>
            </u-navbar>
            <view class="head-bg"></view>
            <swiper class="swiper-box" :indicator-dots="false" :autoplay="true" :interval="3000" :duration="1000">
                <swiper-item v-for="(item, index) in bannerList" :key="index">
                    <navigator :url="item.url" hover-class="none" class="swiper-item">
                        <image :src="item.img" class="banner"></image>
                    </navigator>
                </swiper-item>
            </swiper>
        </view>
        <!-- 头部 end -->
        <!-- 头部按钮 start -->
        <view class="nav">
            <u-grid :col="5" :border="false">
                <u-grid-item bg-color="transparent" v-for="(item, index) in navButton" :key="index">
                    <navigator :url="item.url" hover-class="none" class="nav-item">
                        <image :src="item.img" mode="widthFix" class="nav-item-img"></image>
                        <view class="nav-item-name">{{ item.name }}</view>
                    </navigator>
                </u-grid-item>
            </u-grid>
        </view>
        <!-- 头部按钮 end -->
        <!-- 公告 start -->
        <view class="notice-box">
            <image src="/static/images/home/notice.png" class="img" mode="widthFix"></image>
            <view class="notice-info">
                <navigator hover-class="none" class="notice-cell" v-for="(item, index) in noticeList" :key="index">
                    <image :src="item.img" class="icon" mode="widthFix"></image>
                    <view class="text">{{item.title}}</view>
                    <u-icon name="arrow-right" size="12" color="#C9C9C9"></u-icon>
                </navigator>
            </view>
        </view>
        <!-- 公告 end -->
        <!-- 服务按钮 start -->
        <!-- <view class="service-box">
            <u-grid :col="4" :border="false">
                <u-grid-item bg-color="transparent" v-for="(item, index) in serviceButton" :key="index">
                    <navigator url="" hover-class="none" class="service-item">
                        <image :src="item.img" mode="widthFix" class="img"></image>
                        <view class="name">{{ item.name }}</view>
                    </navigator>
                </u-grid-item>
                <u-grid-item bg-color="transparent" key="8">
                    <navigator url="/pages/service/service" hover-class="none" class="service-item">
                        <image src="/static/images/home/s8.png" mode="widthFix" class="img"></image>
                        <view class="name">更多</view>
                    </navigator>
                </u-grid-item>
            </u-grid>
        </view> -->
        <!-- 服务按钮 end -->
        <!-- 新闻模块 start -->
        <view class="news">
            <navigator hover-class="none" url="/pages/news/list" class="cell">
                <view class="ctitle">新闻</view>
                <view class="more">
                    更多
                    <u-icon name="arrow-right" color="#A6ABB5" size="16"></u-icon>
                </view>
            </navigator>
            <view class="news-list">
                <navigator url="/pages/news/detail" hover-class="none" class="news-item"
                    v-for="(item, index) in newsList" :key="index">
                    <view class="left">
                        <view class="info">
                            [中国空间站] 神舟十二号载人发射任务取得圆满成功
                        </view>
                        <view class="date">
                            <image src="/static/images/home/date.png" class="icon" mode=""></image>
                            2021.06.17
                        </view>
                    </view>
                    <image src="" mode="" class="img"></image>
                </navigator>
                <u-loadmore :status="status" />
            </view>
            <!-- 新闻模块 end -->
        </view>
    </view>
</template>
<script>
    import {
        fakePosition,
        fakeBannerList,
        fakeNoticeList,
        fakeNavButton,
        fakeServiceButton
    } from "@/api/mock/home.js";
    export default {
        data() {
            return {
                position: '',
                positionArr: [],
                status: 'loadmore',
                list: 15,
                page: 0,
                keyword: '',
                focus: false,
                bannerList: [],
                noticeList: [],
                newsList: [{}, {}],
                navButton: [],
                serviceButton: []
            };
        },
        onLoad() {
            // 后续将改为与后端联动
            // 加载banner数据
            fakePosition().then(data => {
                this.position = data.position;
                this.positionArr = data.positionArr;
            });
            // 加载banner数据
            fakeBannerList().then(data => {
                this.bannerList = data;
            });
            // 加载通知公告数据
            fakeNoticeList().then(data => {
                this.noticeList = data;
            });
            // 加载顶部按钮数据
            fakeNavButton().then(data => {
                this.navButton = data;
            });
            // 加载服务按钮数据
            fakeServiceButton().then(data => {
                this.serviceButton = data;
            });
        },
        onReachBottom() {
            // 后续将改为与后端联动
            if (this.page >= 3) return;
            this.status = 'loading';
            this.page = ++this.page;
            setTimeout(() => {
                this.list += 10;
                if (this.page >= 3) this.status = 'nomore';
                else this.status = 'loading';
                this.newsList.push(...[{}, {}]);
            }, 2000);
        },
        methods: {
            changePicker(e) {
                console.log(this.pickerArr[e.detail.value].name);
                this.position = this.pickerArr[e.detail.value].name;
            },
            handleFocus() {
                this.focus = !this.focus;
            },
            handleSearchBlur() {
                this.focus = false;
            }
        }
    };
</script>
<style lang="scss">
    .container {
        background-color: #f7f7f7;
        min-height: 100vh;
        overflow: hidden;
    }
    .head {
        position: relative;
        top: 0;
        left: 0;
        z-index: 1;
    }
    .head-bg {
        position: absolute;
        left: 0px;
        top: 0px;
        z-index: -1;
        width: 750rpx;
        height: 270rpx;
        background: #0bb9c8;
    }
    .nav-wrap {
        width: 100%;
        padding: 0 22rpx;
        display: flex;
        justify-content: space-between;
        align-items: center;
        .picker-box {
            flex: 1;
            display: flex;
            align-items: center;
            justify-content: flex-start;
            font-size: 32rpx;
            font-family: Microsoft YaHei;
            font-weight: bold;
            color: #ffffff;
            .arrow {
                margin-left: 10rpx;
            }
        }
        .rider {
            flex-shrink: 0;
            width: 131rpx;
            height: auto;
        }
        .search-input {
            width: 300rpx;
            height: 50rpx;
        }
        .tool {
            flex: 1;
            display: flex;
            align-items: center;
            justify-content: flex-end;
            .icon {
                height: auto;
            }
            .search-icon {
                width: 40rpx;
                margin-right: 34rpx;
            }
            .message-icon {
                width: 32rpx;
                margin-right: 27rpx;
            }
            .qr-icon {
                width: 37rpx;
            }
        }
    }
    .swiper-box {
        margin: 50rpx auto 0;
        width: 710rpx;
        height: 253rpx;
        .swiper-item {
            width: 100%;
            height: 100%;
            .banner {
                width: 100%;
                height: 100%;
            }
        }
    }
    .nav {
        margin: 0rpx 0;
        box-sizing: border-box;
        padding: 0 10rpx;
        &-item {
            width: 100%;
            box-sizing: border-box;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: space-between;
            height: 130rpx;
            &-img {
                width: 80rpx;
                height: 80rpx;
            }
            &-name {
                font-size: 26rpx;
                font-family: PingFang SC;
                font-weight: 400;
                color: #585b61;
            }
        }
    }
    .notice-box {
        display: flex;
        align-items: center;
        background: #ffffff;
        border-radius: 20px 20px 20px 20px;
        margin: 0 20rpx;
        padding: 30rpx 20rpx;
        .img {
            width: 75rpx;
            height: auto;
            margin-right: 36rpx;
            margin-left: 10rpx;
        }
        .notice-info {
            flex: 1;
            .notice-cell:first-of-type {
                margin-bottom: 15rpx;
            }
        }
        .notice-cell {
            display: flex;
            align-items: center;
            font-size: 24rpx;
            font-family: PingFang SC;
            font-weight: 500;
            color: #585b61;
            .icon {
                width: 63rpx;
                margin-right: 18rpx;
            }
            .text {
                flex: 1;
                min-width: 0;
                overflow: hidden;
                text-overflow: ellipsis;
                display: -webkit-box;
                -webkit-line-clamp: 1;
                -webkit-box-orient: vertical;
            }
        }
    }
    .service-box {
        background: #ffffff;
        border-radius: 20px 20px 20px 20px;
        margin: 30rpx 20rpx 0;
        padding: 0rpx 20rpx;
        .service-item {
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            align-items: center;
            width: 100%;
            height: 120rpx;
            font-size: 26rpx;
            font-family: PingFang SC;
            font-weight: 500;
            color: #585b61;
            .img {
                width: 70rpx;
                height: auto;
            }
        }
    }
    .news {
        background: #ffffff;
        border-radius: 20px 20px 20px 20px;
        margin: 30rpx 20rpx 60rpx;
        padding: 30rpx 20rpx;
        .cell {
            display: flex;
            justify-content: space-between;
            align-items: center;
            .ctitle {
                font-size: 32rpx;
                font-family: PingFang SC;
                font-weight: bold;
                color: #585b61;
            }
            .more {
                display: flex;
                align-items: center;
                font-size: 26rpx;
                font-family: PingFang SC;
                font-weight: 500;
                color: #a6abb5;
            }
        }
        .news-list {
            margin-top: 30rpx;
            .news-item {
                &:not(:last-of-type) {
                    padding: 0 0 30rpx;
                    margin-bottom: 30rpx;
                    border-bottom: 1px solid #eeeeee;
                }
                display: flex;
                align-items: center;
                justify-content: space-between;
                .left {
                    flex: 1;
                    height: 160rpx;
                    display: flex;
                    flex-direction: column;
                    justify-content: space-around;
                    .info {
                        min-width: 0;
                        overflow: hidden;
                        text-overflow: ellipsis;
                        display: -webkit-box;
                        -webkit-line-clamp: 2;
                        -webkit-box-orient: vertical;
                        font-size: 28rpx;
                        font-family: PingFang SC;
                        font-weight: 500;
                        color: #585b61;
                        line-height: 36rpx;
                    }
                    .date {
                        display: flex;
                        align-items: center;
                        .icon {
                            width: 21rpx;
                            height: 21rpx;
                            margin-right: 9rpx;
                        }
                        font-size: 26rpx;
                        font-family: PingFang SC;
                        font-weight: 500;
                        color: #a6abb5;
                    }
                }
                .img {
                    flex-shrink: 0;
                    width: 252rpx;
                    height: 160rpx;
                    border-radius: 20rpx;
                    background-color: #82848a;
                }
            }
        }
    }
</style>
pages/loging/loging.vue
@@ -105,7 +105,7 @@
                    this.passwords = '******';
                    uni.hideNavigationBarLoading();
                    uni.reLaunch({
                        url: '../index/index'
                        url: '../home/home'
                    })
                    return '成功登录'
                } else {
static/images/home/banner.png
static/images/home/date.png
static/images/home/focus.png
static/images/home/message.png
static/images/home/n1.png
static/images/home/n2.png
static/images/home/n3.png
static/images/home/n4.png
static/images/home/n5.png
static/images/home/notice.png
static/images/home/qr.png
static/images/home/s1.png
static/images/home/s2.png
static/images/home/s3.png
static/images/home/s4.png
static/images/home/s5.png
static/images/home/s6.png
static/images/home/s7.png
static/images/home/s8.png
static/images/home/search.png
static/images/home/service.png
static/style/app.scss
New file
@@ -0,0 +1,81 @@
@import '~@/uni.scss';
/* start--演示页面使用的统一样式--start */
.u-demo {
    padding: 25px 20px;
}
.u-demo-wrap {
    border-width: 1px;
    border-color: #ddd;
    border-style: dashed;
    background-color: rgb(250, 250, 250);
    padding: 20px 10px;
    border-radius: 3px;
}
.u-demo-area {
    text-align: center;
}
.u-no-demo-here {
    color: $u-tips-color;
    font-size: 13px;
}
.u-demo-result-line {
    border-width: 1px;
    border-color: #ddd;
    border-style: dashed;
    padding: 5px 20px;
    margin-top: 30px;
    border-radius: 5px;
    background-color: rgb(240, 240, 240);
    color: $u-content-color;
    font-size: 16px;
    /* #ifndef APP-NVUE */
    word-break: break-word;
    display: inline-block;
    /* #endif */
    text-align: left;
}
.u-demo-title,
.u-config-title {
    text-align: center;
    font-size: 16px;
    font-weight: bold;
    margin-bottom: 20px;
}
.u-config-item {
    margin-top: 25px;
}
.u-config-title {
    margin-top: 20px;
    padding-bottom: 5px;
}
.u-item-title {
    position: relative;
    font-size: 15px;
    padding-left: 8px;
    line-height: 1;
    margin-bottom: 11px;
}
.u-item-title:after {
    position: absolute;
    width: 4px;
    top: -1px;
    height: 16px;
    /* #ifndef APP-NVUE */
    content: '';
    /* #endif */
    left: 0;
    border-radius: 10px;
    background-color: $u-content-color;
}
/* end--演示页面使用的统一样式--end */
uni.scss
@@ -6,6 +6,8 @@
 *
 */
@import 'uview-ui/theme.scss';
/**
 * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
 *
uview-ui/LICENSE
New file
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 www.uviewui.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
uview-ui/README.md
New file
@@ -0,0 +1,106 @@
<p align="center">
    <img alt="logo" src="https://uviewui.com/common/logo.png" width="120" height="120" style="margin-bottom: 10px;">
</p>
<h3 align="center" style="margin: 30px 0 30px;font-weight: bold;font-size:40px;">uView</h3>
<h3 align="center">多平台快速开发的UI框架</h3>
## 说明
uView UI,是[uni-app](https://uniapp.dcloud.io/)生态优秀的UI框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水
## 特性
- 兼容安卓,iOS,微信小程序,H5,QQ小程序,百度小程序,支付宝小程序,头条小程序
- 60+精选组件,功能丰富,多端兼容,让您快速集成,开箱即用
- 众多贴心的JS利器,让您飞镖在手,召之即来,百步穿杨
- 众多的常用页面和布局,让您专注逻辑,事半功倍
- 详尽的文档支持,现代化的演示效果
- 按需引入,精简打包体积
## 安装
```bash
# npm方式安装
npm i uview-ui
```
## 快速上手
1. `main.js`引入uView库
```js
// main.js
import uView from 'uview-ui';
Vue.use(uView);
```
2. `App.vue`引入基础样式(注意style标签需声明scss属性支持)
```css
/* App.vue */
<style lang="scss">
@import "uview-ui/index.scss";
</style>
```
3. `uni.scss`引入全局scss变量文件
```css
/* uni.scss */
@import "uview-ui/theme.scss";
```
4. `pages.json`配置easycom规则(按需引入)
```js
// pages.json
{
    "easycom": {
        // npm安装的方式不需要前面的"@/",下载安装的方式需要"@/"
        // npm安装方式
        "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
        // 下载安装方式
        // "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
    },
    // 此为本身已有的内容
    "pages": [
        // ......
    ]
}
```
请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容
## 使用方法
配置easycom规则后,自动按需引入,无需`import`组件,直接引用即可。
```html
<template>
    <u-button>按钮</u-button>
</template>
```
请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容
## 链接
- [官方文档](https://uviewui.com/)
- [更新日志](https://uviewui.com/components/changelog.html)
- [升级指南](https://uviewui.com/components/changelog.html)
- [关于我们](https://uviewui.com/cooperation/about.html)
## 预览
您可以通过**微信**扫码,查看最佳的演示效果。
<br>
<br>
<img src="https://uviewui.com/common/weixin_mini_qrcode.png" width="220" height="220" >
<!-- ## 捐赠uView的研发
uView文档和源码全部开源免费,如果您认为uView帮到了您的开发工作,您可以捐赠uView的研发工作,捐赠无门槛,哪怕是一杯可乐也好(相信这比打赏主播更有意义)。
<img src="https://uviewui.com/common/wechat.png" width="220" >
<img style="margin-left: 100px;" src="https://uviewui.com/common/alipay.png" width="220" >
 -->
## 版权信息
uView遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uView应用到您的产品中。
uview-ui/components/u-action-sheet/u-action-sheet.vue
New file
@@ -0,0 +1,190 @@
<template>
    <u-popup mode="bottom" :border-radius="borderRadius" :popup="false" v-model="value" :maskCloseAble="maskCloseAble"
        length="auto" :safeAreaInsetBottom="safeAreaInsetBottom" @close="popupClose" :z-index="uZIndex">
        <view class="u-tips u-border-bottom" v-if="tips.text" :style="[tipsStyle]">
            {{tips.text}}
        </view>
        <block v-for="(item, index) in list" :key="index">
            <view
                @touchmove.stop.prevent
                @tap="itemClick(index)"
                :style="[itemStyle(index)]"
                class="u-action-sheet-item u-line-1"
                :class="[index < list.length - 1 ? 'u-border-bottom' : '']"
                :hover-stay-time="150"
            >
                <text>{{item.text}}</text>
                <text class="u-action-sheet-item__subtext u-line-1" v-if="item.subText">{{item.subText}}</text>
            </view>
        </block>
        <view class="u-gab" v-if="cancelBtn">
        </view>
        <view @touchmove.stop.prevent class="u-actionsheet-cancel u-action-sheet-item" hover-class="u-hover-class"
            :hover-stay-time="150" v-if="cancelBtn" @tap="close">{{cancelText}}</view>
    </u-popup>
</template>
<script>
    /**
     * actionSheet 操作菜单
     * @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
     * @tutorial https://www.uviewui.com/components/actionSheet.html
     * @property {Array<Object>} list 按钮的文字数组,见官方文档示例
     * @property {Object} tips 顶部的提示文字,见官方文档示例
     * @property {String} cancel-text 取消按钮的提示文字
     * @property {Boolean} cancel-btn 是否显示底部的取消按钮(默认true)
     * @property {Number String} border-radius 弹出部分顶部左右的圆角值,单位rpx(默认0)
     * @property {Boolean} mask-close-able 点击遮罩是否可以关闭(默认true)
     * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
     * @property {Number String} z-index z-index值(默认1075)
     * @property {String} cancel-text 取消按钮的提示文字
     * @event {Function} click 点击ActionSheet列表项时触发
     * @event {Function} close 点击取消按钮时触发
     * @example <u-action-sheet :list="list" @click="click" v-model="show"></u-action-sheet>
     */
    export default {
        name: "u-action-sheet",
        props: {
            // 点击遮罩是否可以关闭actionsheet
            maskCloseAble: {
                type: Boolean,
                default: true
            },
            // 按钮的文字数组,可以自定义颜色和字体大小,字体单位为rpx
            list: {
                type: Array,
                default () {
                    // 如下
                    // return [{
                    //     text: '确定',
                    //     color: '',
                    //     fontSize: ''
                    // }]
                    return [];
                }
            },
            // 顶部的提示文字
            tips: {
                type: Object,
                default () {
                    return {
                        text: '',
                        color: '',
                        fontSize: '26'
                    }
                }
            },
            // 底部的取消按钮
            cancelBtn: {
                type: Boolean,
                default: true
            },
            // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
            safeAreaInsetBottom: {
                type: Boolean,
                default: false
            },
            // 通过双向绑定控制组件的弹出与收起
            value: {
                type: Boolean,
                default: false
            },
            // 弹出的顶部圆角值
            borderRadius: {
                type: [String, Number],
                default: 0
            },
            // 弹出的z-index值
            zIndex: {
                type: [String, Number],
                default: 0
            },
            // 取消按钮的文字提示
            cancelText: {
                type: String,
                default: '取消'
            }
        },
        computed: {
            // 顶部提示的样式
            tipsStyle() {
                let style = {};
                if (this.tips.color) style.color = this.tips.color;
                if (this.tips.fontSize) style.fontSize = this.tips.fontSize + 'rpx';
                return style;
            },
            // 操作项目的样式
            itemStyle() {
                return (index) => {
                    let style = {};
                    if (this.list[index].color) style.color = this.list[index].color;
                    if (this.list[index].fontSize) style.fontSize = this.list[index].fontSize + 'rpx';
                    // 选项被禁用的样式
                    if (this.list[index].disabled) style.color = '#c0c4cc';
                    return style;
                }
            },
            uZIndex() {
                // 如果用户有传递z-index值,优先使用
                return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
            }
        },
        methods: {
            // 点击取消按钮
            close() {
                // 发送input事件,并不会作用于父组件,而是要设置组件内部通过props传递的value参数
                // 这是一个vue发送事件的特殊用法
                this.popupClose();
                this.$emit('close');
            },
            // 弹窗关闭
            popupClose() {
                this.$emit('input', false);
            },
            // 点击某一个item
            itemClick(index) {
                // disabled的项禁止点击
                if(this.list[index].disabled) return;
                this.$emit('click', index);
                this.$emit('input', false);
            }
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-tips {
        font-size: 26rpx;
        text-align: center;
        padding: 34rpx 0;
        line-height: 1;
        color: $u-tips-color;
    }
    .u-action-sheet-item {
        @include vue-flex;;
        line-height: 1;
        justify-content: center;
        align-items: center;
        font-size: 32rpx;
        padding: 34rpx 0;
        flex-direction: column;
    }
    .u-action-sheet-item__subtext {
        font-size: 24rpx;
        color: $u-tips-color;
        margin-top: 20rpx;
    }
    .u-gab {
        height: 12rpx;
        background-color: rgb(234, 234, 236);
    }
    .u-actionsheet-cancel {
        color: $u-main-color;
    }
</style>
uview-ui/components/u-alert-tips/u-alert-tips.vue
New file
@@ -0,0 +1,256 @@
<template>
    <view class="u-alert-tips" v-if="show" :class="[
        !show ? 'u-close-alert-tips': '',
        type ? 'u-alert-tips--bg--' + type + '-light' : '',
        type ? 'u-alert-tips--border--' + type + '-disabled' : '',
    ]" :style="{
        backgroundColor: bgColor,
        borderColor: borderColor
    }">
        <view class="u-icon-wrap">
            <u-icon v-if="showIcon" :name="uIcon" :size="description ? 40 : 32" class="u-icon" :color="uIconType" :custom-style="iconStyle"></u-icon>
        </view>
        <view class="u-alert-content" @tap.stop="click">
            <view class="u-alert-title" :style="[uTitleStyle]">
                {{title}}
            </view>
            <view v-if="description" class="u-alert-desc" :style="[descStyle]">
                {{description}}
            </view>
        </view>
        <view class="u-icon-wrap">
            <u-icon @click="close" v-if="closeAble && !closeText" hoverClass="u-type-error-hover-color" name="close" color="#c0c4cc"
             :size="22" class="u-close-icon" :style="{
                top: description ? '18rpx' : '24rpx'
            }"></u-icon>
        </view>
        <text v-if="closeAble && closeText" class="u-close-text" :style="{
            top: description ? '18rpx' : '24rpx'
        }">{{closeText}}</text>
    </view>
</template>
<script>
    /**
     * alertTips 警告提示
     * @description 警告提示,展现需要关注的信息
     * @tutorial https://uviewui.com/components/alertTips.html
     * @property {String} title 显示的标题文字
     * @property {String} description 辅助性文字,颜色比title浅一点,字号也小一点,可选
     * @property {String} type 关闭按钮(默认为叉号icon图标)
     * @property {String} icon 图标名称
     * @property {Object} icon-style 图标的样式,对象形式
     * @property {Object} title-style 标题的样式,对象形式
     * @property {Object} desc-style 描述的样式,对象形式
     * @property {String} close-able 用文字替代关闭图标,close-able为true时有效
     * @property {Boolean} show-icon 是否显示左边的辅助图标
     * @property {Boolean} show 显示或隐藏组件
     * @event {Function} click 点击组件时触发
     * @event {Function} close 点击关闭按钮时触发
     */
    export default {
        name: 'u-alert-tips',
        props: {
            // 显示文字
            title: {
                type: String,
                default: ''
            },
            // 主题,success/warning/info/error
            type: {
                type: String,
                default: 'warning'
            },
            // 辅助性文字
            description: {
                type: String,
                default: ''
            },
            // 是否可关闭
            closeAble: {
                type: Boolean,
                default: false
            },
            // 关闭按钮自定义文本
            closeText: {
                type: String,
                default: ''
            },
            // 是否显示图标
            showIcon: {
                type: Boolean,
                default: false
            },
            // 文字颜色,如果定义了color值,icon会失效
            color: {
                type: String,
                default: ''
            },
            // 背景颜色
            bgColor: {
                type: String,
                default: ''
            },
            // 边框颜色
            borderColor: {
                type: String,
                default: ''
            },
            // 是否显示
            show: {
                type: Boolean,
                default: true
            },
            // 左边显示的icon
            icon: {
                type: String,
                default: ''
            },
            // icon的样式
            iconStyle: {
                type: Object,
                default() {
                    return {}
                }
            },
            // 标题的样式
            titleStyle: {
                type: Object,
                default() {
                    return {}
                }
            },
            // 描述文字的样式
            descStyle: {
                type: Object,
                default() {
                    return {}
                }
            },
        },
        data() {
            return {
            }
        },
        computed: {
            uTitleStyle() {
                let style = {};
                // 如果有描述文字的话,标题进行加粗
                style.fontWeight = this.description ? 500 : 'normal';
                // 将用户传入样式对象和style合并,传入的优先级比style高,同属性会被覆盖
                return this.$u.deepMerge(style, this.titleStyle);
            },
            uIcon() {
                // 如果有设置icon名称就使用,否则根据type主题,推定一个默认的图标
                return this.icon ? this.icon : this.$u.type2icon(this.type);
            },
            uIconType() {
                // 如果有设置图标的样式,优先使用,没有的话,则用type的样式
                return Object.keys(this.iconStyle).length ? '' : this.type;
            }
        },
        methods: {
            // 点击内容
            click() {
                this.$emit('click');
            },
            // 点击关闭按钮
            close() {
                this.$emit('close');
            }
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-alert-tips {
        @include vue-flex;
        align-items: center;
        padding: 16rpx 30rpx;
        border-radius: 8rpx;
        position: relative;
        transition: all 0.3s linear;
        border: 1px solid #fff;
        &--bg--primary-light {
            background-color: $u-type-primary-light;
        }
        &--bg--info-light {
            background-color: $u-type-info-light;
        }
        &--bg--success-light {
            background-color: $u-type-success-light;
        }
        &--bg--warning-light {
            background-color: $u-type-warning-light;
        }
        &--bg--error-light {
            background-color: $u-type-error-light;
        }
        &--border--primary-disabled {
            border-color: $u-type-primary-disabled;
        }
        &--border--success-disabled {
            border-color: $u-type-success-disabled;
        }
        &--border--error-disabled {
            border-color: $u-type-error-disabled;
        }
        &--border--warning-disabled {
            border-color: $u-type-warning-disabled;
        }
        &--border--info-disabled {
            border-color: $u-type-info-disabled;
        }
    }
    .u-close-alert-tips {
        opacity: 0;
        visibility: hidden;
    }
    .u-icon {
        margin-right: 16rpx;
    }
    .u-alert-title {
        font-size: 28rpx;
        color: $u-main-color;
    }
    .u-alert-desc {
        font-size: 26rpx;
        text-align: left;
        color: $u-content-color;
    }
    .u-close-icon {
        position: absolute;
        top: 20rpx;
        right: 20rpx;
    }
    .u-close-hover {
        color: red;
    }
    .u-close-text {
        font-size: 24rpx;
        color: $u-tips-color;
        position: absolute;
        top: 20rpx;
        right: 20rpx;
        line-height: 1;
    }
</style>
uview-ui/components/u-avatar-cropper/u-avatar-cropper.vue
New file
@@ -0,0 +1,290 @@
<template>
    <view class="content">
        <view class="cropper-wrapper" :style="{ height: cropperOpt.height + 'px' }">
            <canvas
                class="cropper"
                :disable-scroll="true"
                @touchstart="touchStart"
                @touchmove="touchMove"
                @touchend="touchEnd"
                :style="{ width: cropperOpt.width, height: cropperOpt.height, backgroundColor: 'rgba(0, 0, 0, 0.8)' }"
                canvas-id="cropper"
                id="cropper"
            ></canvas>
            <canvas
                class="cropper"
                :disable-scroll="true"
                :style="{
                    position: 'fixed',
                    top: `-${cropperOpt.width * cropperOpt.pixelRatio}px`,
                    left: `-${cropperOpt.height * cropperOpt.pixelRatio}px`,
                    width: `${cropperOpt.width * cropperOpt.pixelRatio}px`,
                    height: `${cropperOpt.height * cropperOpt.pixelRatio}`
                }"
                canvas-id="targetId"
                id="targetId"
            ></canvas>
        </view>
        <view class="cropper-buttons safe-area-padding" :style="{ height: bottomNavHeight + 'px' }">
            <!-- #ifdef H5 -->
            <view class="upload" @tap="uploadTap">选择图片</view>
            <!-- #endif -->
            <!-- #ifndef H5 -->
            <view class="upload" @tap="uploadTap">重新选择</view>
            <!-- #endif -->
            <view class="getCropperImage" @tap="getCropperImage(false)">确定</view>
        </view>
    </view>
</template>
<script>
import WeCropper from './weCropper.js';
export default {
    props: {
        // 裁剪矩形框的样式,其中可包含的属性为lineWidth-边框宽度(单位rpx),color: 边框颜色,
        // mask-遮罩颜色,一般设置为一个rgba的透明度,如"rgba(0, 0, 0, 0.35)"
        boundStyle: {
            type: Object,
            default() {
                return {
                    lineWidth: 4,
                    borderColor: 'rgb(245, 245, 245)',
                    mask: 'rgba(0, 0, 0, 0.35)'
                };
            }
        }
        // // 裁剪框宽度,单位rpx
        // rectWidth: {
        //     type: [String, Number],
        //     default: 400
        // },
        // // 裁剪框高度,单位rpx
        // rectHeight: {
        //     type: [String, Number],
        //     default: 400
        // },
        // // 输出图片宽度,单位rpx
        // destWidth: {
        //     type: [String, Number],
        //     default: 400
        // },
        // // 输出图片高度,单位rpx
        // destHeight: {
        //     type: [String, Number],
        //     default: 400
        // },
        // // 输出的图片类型,如果发现裁剪的图片很大,可能是因为设置为了"png",改成"jpg"即可
        // fileType: {
        //     type: String,
        //     default: 'jpg',
        // },
        // // 生成的图片质量
        // // H5上无效,目前不考虑使用此参数
        // quality: {
        //     type: [Number, String],
        //     default: 1
        // }
    },
    data() {
        return {
            // 底部导航的高度
            bottomNavHeight: 50,
            originWidth: 200,
            width: 0,
            height: 0,
            cropperOpt: {
                id: 'cropper',
                targetId: 'targetCropper',
                pixelRatio: 1,
                width: 0,
                height: 0,
                scale: 2.5,
                zoom: 8,
                cut: {
                    x: (this.width - this.originWidth) / 2,
                    y: (this.height - this.originWidth) / 2,
                    width: this.originWidth,
                    height: this.originWidth
                },
                boundStyle: {
                    lineWidth: uni.upx2px(this.boundStyle.lineWidth),
                    mask: this.boundStyle.mask,
                    color: this.boundStyle.borderColor
                }
            },
            // 裁剪框和输出图片的尺寸,高度默认等于宽度
            // 输出图片宽度,单位px
            destWidth: 200,
            // 裁剪框宽度,单位px
            rectWidth: 200,
            // 输出的图片类型,如果'png'类型发现裁剪的图片太大,改成"jpg"即可
            fileType: 'jpg',
            src: '', // 选择的图片路径,用于在点击确定时,判断是否选择了图片
        };
    },
    onLoad(option) {
        let rectInfo = uni.getSystemInfoSync();
        this.width = rectInfo.windowWidth;
        this.height = rectInfo.windowHeight - this.bottomNavHeight;
        this.cropperOpt.width = this.width;
        this.cropperOpt.height = this.height;
        this.cropperOpt.pixelRatio = rectInfo.pixelRatio;
        if (option.destWidth) this.destWidth = option.destWidth;
        if (option.rectWidth) {
            let rectWidth = Number(option.rectWidth);
            this.cropperOpt.cut = {
                x: (this.width - rectWidth) / 2,
                y: (this.height - rectWidth) / 2,
                width: rectWidth,
                height: rectWidth
            };
        }
        this.rectWidth = option.rectWidth;
        if (option.fileType) this.fileType = option.fileType;
        // 初始化
        this.cropper = new WeCropper(this.cropperOpt)
            .on('ready', ctx => {
                // wecropper is ready for work!
            })
            .on('beforeImageLoad', ctx => {
                // before picture loaded, i can do something
            })
            .on('imageLoad', ctx => {
                // picture loaded
            })
            .on('beforeDraw', (ctx, instance) => {
                // before canvas draw,i can do something
            });
        // 设置导航栏样式,以免用户在page.json中没有设置为黑色背景
        uni.setNavigationBarColor({
            frontColor: '#ffffff',
            backgroundColor: '#000000'
        });
        uni.chooseImage({
            count: 1, // 默认9
            sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有
            sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
            success: res => {
                this.src = res.tempFilePaths[0];
                //  获取裁剪图片资源后,给data添加src属性及其值
                this.cropper.pushOrign(this.src);
            }
        });
    },
    methods: {
        touchStart(e) {
            this.cropper.touchStart(e);
        },
        touchMove(e) {
            this.cropper.touchMove(e);
        },
        touchEnd(e) {
            this.cropper.touchEnd(e);
        },
        getCropperImage(isPre = false) {
            if(!this.src) return this.$u.toast('请先选择图片再裁剪');
            let cropper_opt = {
                destHeight: Number(this.destWidth), // uni.canvasToTempFilePath要求这些参数为数值
                destWidth: Number(this.destWidth),
                fileType: this.fileType
            };
            this.cropper.getCropperImage(cropper_opt, (path, err) => {
                if (err) {
                    uni.showModal({
                        title: '温馨提示',
                        content: err.message
                    });
                } else {
                    if (isPre) {
                        uni.previewImage({
                            current: '', // 当前显示图片的 http 链接
                            urls: [path] // 需要预览的图片 http 链接列表
                        });
                    } else {
                        uni.$emit('uAvatarCropper', path);
                        this.$u.route({
                            type: 'back'
                        });
                    }
                }
            });
        },
        uploadTap() {
            const self = this;
            uni.chooseImage({
                count: 1, // 默认9
                sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
                sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
                success: (res) => {
                    self.src = res.tempFilePaths[0];
                    //  获取裁剪图片资源后,给data添加src属性及其值
                    self.cropper.pushOrign(this.src);
                }
            });
        }
    }
};
</script>
<style scoped lang="scss">
@import '../../libs/css/style.components.scss';
.content {
    background: rgba(255, 255, 255, 1);
}
.cropper {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 11;
}
.cropper-buttons {
    background-color: #000000;
    color: #eee;
}
.cropper-wrapper {
    position: relative;
    @include vue-flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    width: 100%;
    background-color: #000;
}
.cropper-buttons {
    width: 100vw;
    @include vue-flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    position: fixed;
    bottom: 0;
    left: 0;
    font-size: 28rpx;
}
.cropper-buttons .upload,
.cropper-buttons .getCropperImage {
    width: 50%;
    text-align: center;
}
.cropper-buttons .upload {
    text-align: left;
    padding-left: 50rpx;
}
.cropper-buttons .getCropperImage {
    text-align: right;
    padding-right: 50rpx;
}
</style>
uview-ui/components/u-avatar-cropper/weCropper.js
New file
@@ -0,0 +1,1265 @@
/**
 * we-cropper v1.3.9
 * (c) 2020 dlhandsome
 * @license MIT
 */
(function(global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
        typeof define === 'function' && define.amd ? define(factory) :
        (global.WeCropper = factory());
}(this, (function() {
    'use strict';
    var device = void 0;
    var TOUCH_STATE = ['touchstarted', 'touchmoved', 'touchended'];
    function firstLetterUpper(str) {
        return str.charAt(0).toUpperCase() + str.slice(1)
    }
    function setTouchState(instance) {
        var arg = [],
            len = arguments.length - 1;
        while (len-- > 0) arg[len] = arguments[len + 1];
        TOUCH_STATE.forEach(function(key, i) {
            if (arg[i] !== undefined) {
                instance[key] = arg[i];
            }
        });
    }
    function validator(instance, o) {
        Object.defineProperties(instance, o);
    }
    function getDevice() {
        if (!device) {
            device = uni.getSystemInfoSync();
        }
        return device
    }
    var tmp = {};
    var ref = getDevice();
    var pixelRatio = ref.pixelRatio;
    var DEFAULT = {
        id: {
            default: 'cropper',
            get: function get() {
                return tmp.id
            },
            set: function set(value) {
                if (typeof(value) !== 'string') {
                    console.error(("id:" + value + " is invalid"));
                }
                tmp.id = value;
            }
        },
        width: {
            default: 750,
            get: function get() {
                return tmp.width
            },
            set: function set(value) {
                if (typeof(value) !== 'number') {
                    console.error(("width:" + value + " is invalid"));
                }
                tmp.width = value;
            }
        },
        height: {
            default: 750,
            get: function get() {
                return tmp.height
            },
            set: function set(value) {
                if (typeof(value) !== 'number') {
                    console.error(("height:" + value + " is invalid"));
                }
                tmp.height = value;
            }
        },
        pixelRatio: {
            default: pixelRatio,
            get: function get() {
                return tmp.pixelRatio
            },
            set: function set(value) {
                if (typeof(value) !== 'number') {
                    console.error(("pixelRatio:" + value + " is invalid"));
                }
                tmp.pixelRatio = value;
            }
        },
        scale: {
            default: 2.5,
            get: function get() {
                return tmp.scale
            },
            set: function set(value) {
                if (typeof(value) !== 'number') {
                    console.error(("scale:" + value + " is invalid"));
                }
                tmp.scale = value;
            }
        },
        zoom: {
            default: 5,
            get: function get() {
                return tmp.zoom
            },
            set: function set(value) {
                if (typeof(value) !== 'number') {
                    console.error(("zoom:" + value + " is invalid"));
                } else if (value < 0 || value > 10) {
                    console.error("zoom should be ranged in 0 ~ 10");
                }
                tmp.zoom = value;
            }
        },
        src: {
            default: '',
            get: function get() {
                return tmp.src
            },
            set: function set(value) {
                if (typeof(value) !== 'string') {
                    console.error(("src:" + value + " is invalid"));
                }
                tmp.src = value;
            }
        },
        cut: {
            default: {},
            get: function get() {
                return tmp.cut
            },
            set: function set(value) {
                if (typeof(value) !== 'object') {
                    console.error(("cut:" + value + " is invalid"));
                }
                tmp.cut = value;
            }
        },
        boundStyle: {
            default: {},
            get: function get() {
                return tmp.boundStyle
            },
            set: function set(value) {
                if (typeof(value) !== 'object') {
                    console.error(("boundStyle:" + value + " is invalid"));
                }
                tmp.boundStyle = value;
            }
        },
        onReady: {
            default: null,
            get: function get() {
                return tmp.ready
            },
            set: function set(value) {
                tmp.ready = value;
            }
        },
        onBeforeImageLoad: {
            default: null,
            get: function get() {
                return tmp.beforeImageLoad
            },
            set: function set(value) {
                tmp.beforeImageLoad = value;
            }
        },
        onImageLoad: {
            default: null,
            get: function get() {
                return tmp.imageLoad
            },
            set: function set(value) {
                tmp.imageLoad = value;
            }
        },
        onBeforeDraw: {
            default: null,
            get: function get() {
                return tmp.beforeDraw
            },
            set: function set(value) {
                tmp.beforeDraw = value;
            }
        }
    };
    var ref$1 = getDevice();
    var windowWidth = ref$1.windowWidth;
    function prepare() {
        var self = this;
        // v1.4.0 版本中将不再自动绑定we-cropper实例
        self.attachPage = function() {
            var pages = getCurrentPages();
            // 获取到当前page上下文
            var pageContext = pages[pages.length - 1];
            // 把this依附在Page上下文的wecropper属性上,便于在page钩子函数中访问
            Object.defineProperty(pageContext, 'wecropper', {
                get: function get() {
                    console.warn(
                        'Instance will not be automatically bound to the page after v1.4.0\n\n' +
                        'Please use a custom instance name instead\n\n' +
                        'Example: \n' +
                        'this.mycropper = new WeCropper(options)\n\n' +
                        '// ...\n' +
                        'this.mycropper.getCropperImage()'
                    );
                    return self
                },
                configurable: true
            });
        };
        self.createCtx = function() {
            var id = self.id;
            var targetId = self.targetId;
            if (id) {
                self.ctx = self.ctx || uni.createCanvasContext(id);
                self.targetCtx = self.targetCtx || uni.createCanvasContext(targetId);
            } else {
                console.error("constructor: create canvas context failed, 'id' must be valuable");
            }
        };
        self.deviceRadio = windowWidth / 750;
    }
    var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !==
        'undefined' ? self : {};
    function createCommonjsModule(fn, module) {
        return module = {
            exports: {}
        }, fn(module, module.exports), module.exports;
    }
    var tools = createCommonjsModule(function(module, exports) {
        /**
         * String type check
         */
        exports.isStr = function(v) {
            return typeof v === 'string';
        };
        /**
         * Number type check
         */
        exports.isNum = function(v) {
            return typeof v === 'number';
        };
        /**
         * Array type check
         */
        exports.isArr = Array.isArray;
        /**
         * undefined type check
         */
        exports.isUndef = function(v) {
            return v === undefined;
        };
        exports.isTrue = function(v) {
            return v === true;
        };
        exports.isFalse = function(v) {
            return v === false;
        };
        /**
         * Function type check
         */
        exports.isFunc = function(v) {
            return typeof v === 'function';
        };
        /**
         * Quick object check - this is primarily used to tell
         * Objects from primitive values when we know the value
         * is a JSON-compliant type.
         */
        exports.isObj = exports.isObject = function(obj) {
            return obj !== null && typeof obj === 'object'
        };
        /**
         * Strict object type check. Only returns true
         * for plain JavaScript objects.
         */
        var _toString = Object.prototype.toString;
        exports.isPlainObject = function(obj) {
            return _toString.call(obj) === '[object Object]'
        };
        /**
         * Check whether the object has the property.
         */
        var hasOwnProperty = Object.prototype.hasOwnProperty;
        exports.hasOwn = function(obj, key) {
            return hasOwnProperty.call(obj, key)
        };
        /**
         * Perform no operation.
         * Stubbing args to make Flow happy without leaving useless transpiled code
         * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/)
         */
        exports.noop = function(a, b, c) {};
        /**
         * Check if val is a valid array index.
         */
        exports.isValidArrayIndex = function(val) {
            var n = parseFloat(String(val));
            return n >= 0 && Math.floor(n) === n && isFinite(val)
        };
    });
    var tools_7 = tools.isFunc;
    var tools_10 = tools.isPlainObject;
    var EVENT_TYPE = ['ready', 'beforeImageLoad', 'beforeDraw', 'imageLoad'];
    function observer() {
        var self = this;
        self.on = function(event, fn) {
            if (EVENT_TYPE.indexOf(event) > -1) {
                if (tools_7(fn)) {
                    event === 'ready' ?
                        fn(self) :
                        self[("on" + (firstLetterUpper(event)))] = fn;
                }
            } else {
                console.error(("event: " + event + " is invalid"));
            }
            return self
        };
    }
    function wxPromise(fn) {
        return function(obj) {
            var args = [],
                len = arguments.length - 1;
            while (len-- > 0) args[len] = arguments[len + 1];
            if (obj === void 0) obj = {};
            return new Promise(function(resolve, reject) {
                obj.success = function(res) {
                    resolve(res);
                };
                obj.fail = function(err) {
                    reject(err);
                };
                fn.apply(void 0, [obj].concat(args));
            })
        }
    }
    function draw(ctx, reserve) {
        if (reserve === void 0) reserve = false;
        return new Promise(function(resolve) {
            ctx.draw(reserve, resolve);
        })
    }
    var getImageInfo = wxPromise(uni.getImageInfo);
    var canvasToTempFilePath = wxPromise(uni.canvasToTempFilePath);
    var base64 = createCommonjsModule(function(module, exports) {
        /*! http://mths.be/base64 v0.1.0 by @mathias | MIT license */
        (function(root) {
            // Detect free variables `exports`.
            var freeExports = 'object' == 'object' && exports;
            // Detect free variable `module`.
            var freeModule = 'object' == 'object' && module &&
                module.exports == freeExports && module;
            // Detect free variable `global`, from Node.js or Browserified code, and use
            // it as `root`.
            var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal;
            if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) {
                root = freeGlobal;
            }
            /*--------------------------------------------------------------------------*/
            var InvalidCharacterError = function(message) {
                this.message = message;
            };
            InvalidCharacterError.prototype = new Error;
            InvalidCharacterError.prototype.name = 'InvalidCharacterError';
            var error = function(message) {
                // Note: the error messages used throughout this file match those used by
                // the native `atob`/`btoa` implementation in Chromium.
                throw new InvalidCharacterError(message);
            };
            var TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
            // http://whatwg.org/html/common-microsyntaxes.html#space-character
            var REGEX_SPACE_CHARACTERS = /[\t\n\f\r ]/g;
            // `decode` is designed to be fully compatible with `atob` as described in the
            // HTML Standard. http://whatwg.org/html/webappapis.html#dom-windowbase64-atob
            // The optimized base64-decoding algorithm used is based on @atk’s excellent
            // implementation. https://gist.github.com/atk/1020396
            var decode = function(input) {
                input = String(input)
                    .replace(REGEX_SPACE_CHARACTERS, '');
                var length = input.length;
                if (length % 4 == 0) {
                    input = input.replace(/==?$/, '');
                    length = input.length;
                }
                if (
                    length % 4 == 1 ||
                    // http://whatwg.org/C#alphanumeric-ascii-characters
                    /[^+a-zA-Z0-9/]/.test(input)
                ) {
                    error(
                        'Invalid character: the string to be decoded is not correctly encoded.'
                    );
                }
                var bitCounter = 0;
                var bitStorage;
                var buffer;
                var output = '';
                var position = -1;
                while (++position < length) {
                    buffer = TABLE.indexOf(input.charAt(position));
                    bitStorage = bitCounter % 4 ? bitStorage * 64 + buffer : buffer;
                    // Unless this is the first of a group of 4 characters…
                    if (bitCounter++ % 4) {
                        // …convert the first 8 bits to a single ASCII character.
                        output += String.fromCharCode(
                            0xFF & bitStorage >> (-2 * bitCounter & 6)
                        );
                    }
                }
                return output;
            };
            // `encode` is designed to be fully compatible with `btoa` as described in the
            // HTML Standard: http://whatwg.org/html/webappapis.html#dom-windowbase64-btoa
            var encode = function(input) {
                input = String(input);
                if (/[^\0-\xFF]/.test(input)) {
                    // Note: no need to special-case astral symbols here, as surrogates are
                    // matched, and the input is supposed to only contain ASCII anyway.
                    error(
                        'The string to be encoded contains characters outside of the ' +
                        'Latin1 range.'
                    );
                }
                var padding = input.length % 3;
                var output = '';
                var position = -1;
                var a;
                var b;
                var c;
                var buffer;
                // Make sure any padding is handled outside of the loop.
                var length = input.length - padding;
                while (++position < length) {
                    // Read three bytes, i.e. 24 bits.
                    a = input.charCodeAt(position) << 16;
                    b = input.charCodeAt(++position) << 8;
                    c = input.charCodeAt(++position);
                    buffer = a + b + c;
                    // Turn the 24 bits into four chunks of 6 bits each, and append the
                    // matching character for each of them to the output.
                    output += (
                        TABLE.charAt(buffer >> 18 & 0x3F) +
                        TABLE.charAt(buffer >> 12 & 0x3F) +
                        TABLE.charAt(buffer >> 6 & 0x3F) +
                        TABLE.charAt(buffer & 0x3F)
                    );
                }
                if (padding == 2) {
                    a = input.charCodeAt(position) << 8;
                    b = input.charCodeAt(++position);
                    buffer = a + b;
                    output += (
                        TABLE.charAt(buffer >> 10) +
                        TABLE.charAt((buffer >> 4) & 0x3F) +
                        TABLE.charAt((buffer << 2) & 0x3F) +
                        '='
                    );
                } else if (padding == 1) {
                    buffer = input.charCodeAt(position);
                    output += (
                        TABLE.charAt(buffer >> 2) +
                        TABLE.charAt((buffer << 4) & 0x3F) +
                        '=='
                    );
                }
                return output;
            };
            var base64 = {
                'encode': encode,
                'decode': decode,
                'version': '0.1.0'
            };
            // Some AMD build optimizers, like r.js, check for specific condition patterns
            // like the following:
            if (
                typeof undefined == 'function' &&
                typeof undefined.amd == 'object' &&
                undefined.amd
            ) {
                undefined(function() {
                    return base64;
                });
            } else if (freeExports && !freeExports.nodeType) {
                if (freeModule) { // in Node.js or RingoJS v0.8.0+
                    freeModule.exports = base64;
                } else { // in Narwhal or RingoJS v0.7.0-
                    for (var key in base64) {
                        base64.hasOwnProperty(key) && (freeExports[key] = base64[key]);
                    }
                }
            } else { // in Rhino or a web browser
                root.base64 = base64;
            }
        }(commonjsGlobal));
    });
    function makeURI(strData, type) {
        return 'data:' + type + ';base64,' + strData
    }
    function fixType(type) {
        type = type.toLowerCase().replace(/jpg/i, 'jpeg');
        var r = type.match(/png|jpeg|bmp|gif/)[0];
        return 'image/' + r
    }
    function encodeData(data) {
        var str = '';
        if (typeof data === 'string') {
            str = data;
        } else {
            for (var i = 0; i < data.length; i++) {
                str += String.fromCharCode(data[i]);
            }
        }
        return base64.encode(str)
    }
    /**
     * 获取图像区域隐含的像素数据
     * @param canvasId canvas标识
     * @param x 将要被提取的图像数据矩形区域的左上角 x 坐标
     * @param y 将要被提取的图像数据矩形区域的左上角 y 坐标
     * @param width 将要被提取的图像数据矩形区域的宽度
     * @param height 将要被提取的图像数据矩形区域的高度
     * @param done 完成回调
     */
    function getImageData(canvasId, x, y, width, height, done) {
        uni.canvasGetImageData({
            canvasId: canvasId,
            x: x,
            y: y,
            width: width,
            height: height,
            success: function success(res) {
                done(res, null);
            },
            fail: function fail(res) {
                done(null, res);
            }
        });
    }
    /**
     * 生成bmp格式图片
     * 按照规则生成图片响应头和响应体
     * @param oData 用来描述 canvas 区域隐含的像素数据 { data, width, height } = oData
     * @returns {*} base64字符串
     */
    function genBitmapImage(oData) {
        //
        // BITMAPFILEHEADER: http://msdn.microsoft.com/en-us/library/windows/desktop/dd183374(v=vs.85).aspx
        // BITMAPINFOHEADER: http://msdn.microsoft.com/en-us/library/dd183376.aspx
        //
        var biWidth = oData.width;
        var biHeight = oData.height;
        var biSizeImage = biWidth * biHeight * 3;
        var bfSize = biSizeImage + 54; // total header size = 54 bytes
        //
        //  typedef struct tagBITMAPFILEHEADER {
        //      WORD bfType;
        //      DWORD bfSize;
        //      WORD bfReserved1;
        //      WORD bfReserved2;
        //      DWORD bfOffBits;
        //  } BITMAPFILEHEADER;
        //
        var BITMAPFILEHEADER = [
            // WORD bfType -- The file type signature; must be "BM"
            0x42, 0x4D,
            // DWORD bfSize -- The size, in bytes, of the bitmap file
            bfSize & 0xff, bfSize >> 8 & 0xff, bfSize >> 16 & 0xff, bfSize >> 24 & 0xff,
            // WORD bfReserved1 -- Reserved; must be zero
            0, 0,
            // WORD bfReserved2 -- Reserved; must be zero
            0, 0,
            // DWORD bfOffBits -- The offset, in bytes, from the beginning of the BITMAPFILEHEADER structure to the bitmap bits.
            54, 0, 0, 0
        ];
        //
        //  typedef struct tagBITMAPINFOHEADER {
        //      DWORD biSize;
        //      LONG  biWidth;
        //      LONG  biHeight;
        //      WORD  biPlanes;
        //      WORD  biBitCount;
        //      DWORD biCompression;
        //      DWORD biSizeImage;
        //      LONG  biXPelsPerMeter;
        //      LONG  biYPelsPerMeter;
        //      DWORD biClrUsed;
        //      DWORD biClrImportant;
        //  } BITMAPINFOHEADER, *PBITMAPINFOHEADER;
        //
        var BITMAPINFOHEADER = [
            // DWORD biSize -- The number of bytes required by the structure
            40, 0, 0, 0,
            // LONG biWidth -- The width of the bitmap, in pixels
            biWidth & 0xff, biWidth >> 8 & 0xff, biWidth >> 16 & 0xff, biWidth >> 24 & 0xff,
            // LONG biHeight -- The height of the bitmap, in pixels
            biHeight & 0xff, biHeight >> 8 & 0xff, biHeight >> 16 & 0xff, biHeight >> 24 & 0xff,
            // WORD biPlanes -- The number of planes for the target device. This value must be set to 1
            1, 0,
            // WORD biBitCount -- The number of bits-per-pixel, 24 bits-per-pixel -- the bitmap
            // has a maximum of 2^24 colors (16777216, Truecolor)
            24, 0,
            // DWORD biCompression -- The type of compression, BI_RGB (code 0) -- uncompressed
            0, 0, 0, 0,
            // DWORD biSizeImage -- The size, in bytes, of the image. This may be set to zero for BI_RGB bitmaps
            biSizeImage & 0xff, biSizeImage >> 8 & 0xff, biSizeImage >> 16 & 0xff, biSizeImage >> 24 & 0xff,
            // LONG biXPelsPerMeter, unused
            0, 0, 0, 0,
            // LONG biYPelsPerMeter, unused
            0, 0, 0, 0,
            // DWORD biClrUsed, the number of color indexes of palette, unused
            0, 0, 0, 0,
            // DWORD biClrImportant, unused
            0, 0, 0, 0
        ];
        var iPadding = (4 - ((biWidth * 3) % 4)) % 4;
        var aImgData = oData.data;
        var strPixelData = '';
        var biWidth4 = biWidth << 2;
        var y = biHeight;
        var fromCharCode = String.fromCharCode;
        do {
            var iOffsetY = biWidth4 * (y - 1);
            var strPixelRow = '';
            for (var x = 0; x < biWidth; x++) {
                var iOffsetX = x << 2;
                strPixelRow += fromCharCode(aImgData[iOffsetY + iOffsetX + 2]) +
                    fromCharCode(aImgData[iOffsetY + iOffsetX + 1]) +
                    fromCharCode(aImgData[iOffsetY + iOffsetX]);
            }
            for (var c = 0; c < iPadding; c++) {
                strPixelRow += String.fromCharCode(0);
            }
            strPixelData += strPixelRow;
        } while (--y)
        var strEncoded = encodeData(BITMAPFILEHEADER.concat(BITMAPINFOHEADER)) + encodeData(strPixelData);
        return strEncoded
    }
    /**
     * 转换为图片base64
     * @param canvasId canvas标识
     * @param x 将要被提取的图像数据矩形区域的左上角 x 坐标
     * @param y 将要被提取的图像数据矩形区域的左上角 y 坐标
     * @param width 将要被提取的图像数据矩形区域的宽度
     * @param height 将要被提取的图像数据矩形区域的高度
     * @param type 转换图片类型
     * @param done 完成回调
     */
    function convertToImage(canvasId, x, y, width, height, type, done) {
        if (done === void 0) done = function() {};
        if (type === undefined) {
            type = 'png';
        }
        type = fixType(type);
        if (/bmp/.test(type)) {
            getImageData(canvasId, x, y, width, height, function(data, err) {
                var strData = genBitmapImage(data);
                tools_7(done) && done(makeURI(strData, 'image/' + type), err);
            });
        } else {
            console.error('暂不支持生成\'' + type + '\'类型的base64图片');
        }
    }
    var CanvasToBase64 = {
        convertToImage: convertToImage,
        // convertToPNG: function (width, height, done) {
        //   return convertToImage(width, height, 'png', done)
        // },
        // convertToJPEG: function (width, height, done) {
        //   return convertToImage(width, height, 'jpeg', done)
        // },
        // convertToGIF: function (width, height, done) {
        //   return convertToImage(width, height, 'gif', done)
        // },
        convertToBMP: function(ref, done) {
            if (ref === void 0) ref = {};
            var canvasId = ref.canvasId;
            var x = ref.x;
            var y = ref.y;
            var width = ref.width;
            var height = ref.height;
            if (done === void 0) done = function() {};
            return convertToImage(canvasId, x, y, width, height, 'bmp', done)
        }
    };
    function methods() {
        var self = this;
        var boundWidth = self.width; // 裁剪框默认宽度,即整个画布宽度
        var boundHeight = self.height; // 裁剪框默认高度,即整个画布高度
        var id = self.id;
        var targetId = self.targetId;
        var pixelRatio = self.pixelRatio;
        var ref = self.cut;
        var x = ref.x;
        if (x === void 0) x = 0;
        var y = ref.y;
        if (y === void 0) y = 0;
        var width = ref.width;
        if (width === void 0) width = boundWidth;
        var height = ref.height;
        if (height === void 0) height = boundHeight;
        self.updateCanvas = function(done) {
            if (self.croperTarget) {
                //  画布绘制图片
                self.ctx.drawImage(
                    self.croperTarget,
                    self.imgLeft,
                    self.imgTop,
                    self.scaleWidth,
                    self.scaleHeight
                );
            }
            tools_7(self.onBeforeDraw) && self.onBeforeDraw(self.ctx, self);
            self.setBoundStyle(self.boundStyle); //    设置边界样式
            self.ctx.draw(false, done);
            return self
        };
        self.pushOrigin = self.pushOrign = function(src) {
            self.src = src;
            tools_7(self.onBeforeImageLoad) && self.onBeforeImageLoad(self.ctx, self);
            return getImageInfo({
                    src: src
                })
                .then(function(res) {
                    var innerAspectRadio = res.width / res.height;
                    var customAspectRadio = width / height;
                    self.croperTarget = res.path;
                    if (innerAspectRadio < customAspectRadio) {
                        self.rectX = x;
                        self.baseWidth = width;
                        self.baseHeight = width / innerAspectRadio;
                        self.rectY = y - Math.abs((height - self.baseHeight) / 2);
                    } else {
                        self.rectY = y;
                        self.baseWidth = height * innerAspectRadio;
                        self.baseHeight = height;
                        self.rectX = x - Math.abs((width - self.baseWidth) / 2);
                    }
                    self.imgLeft = self.rectX;
                    self.imgTop = self.rectY;
                    self.scaleWidth = self.baseWidth;
                    self.scaleHeight = self.baseHeight;
                    self.update();
                    return new Promise(function(resolve) {
                        self.updateCanvas(resolve);
                    })
                })
                .then(function() {
                    tools_7(self.onImageLoad) && self.onImageLoad(self.ctx, self);
                })
        };
        self.removeImage = function() {
            self.src = '';
            self.croperTarget = '';
            return draw(self.ctx)
        };
        self.getCropperBase64 = function(done) {
            if (done === void 0) done = function() {};
            CanvasToBase64.convertToBMP({
                canvasId: id,
                x: x,
                y: y,
                width: width,
                height: height
            }, done);
        };
        self.getCropperImage = function(opt, fn) {
            var customOptions = opt;
            var canvasOptions = {
                canvasId: id,
                x: x,
                y: y,
                width: width,
                height: height
            };
            var task = function() {
                return Promise.resolve();
            };
            if (
                tools_10(customOptions) &&
                customOptions.original
            ) {
                // original mode
                task = function() {
                    self.targetCtx.drawImage(
                        self.croperTarget,
                        self.imgLeft * pixelRatio,
                        self.imgTop * pixelRatio,
                        self.scaleWidth * pixelRatio,
                        self.scaleHeight * pixelRatio
                    );
                    canvasOptions = {
                        canvasId: targetId,
                        x: x * pixelRatio,
                        y: y * pixelRatio,
                        width: width * pixelRatio,
                        height: height * pixelRatio
                    };
                    return draw(self.targetCtx)
                };
            }
            return task()
                .then(function() {
                    if (tools_10(customOptions)) {
                        canvasOptions = Object.assign({}, canvasOptions, customOptions);
                    }
                    if (tools_7(customOptions)) {
                        fn = customOptions;
                    }
                    var arg = canvasOptions.componentContext ?
                        [canvasOptions, canvasOptions.componentContext] :
                        [canvasOptions];
                    return canvasToTempFilePath.apply(null, arg)
                })
                .then(function(res) {
                    var tempFilePath = res.tempFilePath;
                    return tools_7(fn) ?
                        fn.call(self, tempFilePath, null) :
                        tempFilePath
                })
                .catch(function(err) {
                    if (tools_7(fn)) {
                        fn.call(self, null, err);
                    } else {
                        throw err
                    }
                })
        };
    }
    /**
     * 获取最新缩放值
     * @param oldScale 上一次触摸结束后的缩放值
     * @param oldDistance 上一次触摸结束后的双指距离
     * @param zoom 缩放系数
     * @param touch0 第一指touch对象
     * @param touch1 第二指touch对象
     * @returns {*}
     */
    var getNewScale = function(oldScale, oldDistance, zoom, touch0, touch1) {
        var xMove, yMove, newDistance;
        // 计算二指最新距离
        xMove = Math.round(touch1.x - touch0.x);
        yMove = Math.round(touch1.y - touch0.y);
        newDistance = Math.round(Math.sqrt(xMove * xMove + yMove * yMove));
        return oldScale + 0.001 * zoom * (newDistance - oldDistance)
    };
    function update() {
        var self = this;
        if (!self.src) {
            return
        }
        self.__oneTouchStart = function(touch) {
            self.touchX0 = Math.round(touch.x);
            self.touchY0 = Math.round(touch.y);
        };
        self.__oneTouchMove = function(touch) {
            var xMove, yMove;
            // 计算单指移动的距离
            if (self.touchended) {
                return self.updateCanvas()
            }
            xMove = Math.round(touch.x - self.touchX0);
            yMove = Math.round(touch.y - self.touchY0);
            var imgLeft = Math.round(self.rectX + xMove);
            var imgTop = Math.round(self.rectY + yMove);
            self.outsideBound(imgLeft, imgTop);
            self.updateCanvas();
        };
        self.__twoTouchStart = function(touch0, touch1) {
            var xMove, yMove, oldDistance;
            self.touchX1 = Math.round(self.rectX + self.scaleWidth / 2);
            self.touchY1 = Math.round(self.rectY + self.scaleHeight / 2);
            // 计算两指距离
            xMove = Math.round(touch1.x - touch0.x);
            yMove = Math.round(touch1.y - touch0.y);
            oldDistance = Math.round(Math.sqrt(xMove * xMove + yMove * yMove));
            self.oldDistance = oldDistance;
        };
        self.__twoTouchMove = function(touch0, touch1) {
            var oldScale = self.oldScale;
            var oldDistance = self.oldDistance;
            var scale = self.scale;
            var zoom = self.zoom;
            self.newScale = getNewScale(oldScale, oldDistance, zoom, touch0, touch1);
            //  设定缩放范围
            self.newScale <= 1 && (self.newScale = 1);
            self.newScale >= scale && (self.newScale = scale);
            self.scaleWidth = Math.round(self.newScale * self.baseWidth);
            self.scaleHeight = Math.round(self.newScale * self.baseHeight);
            var imgLeft = Math.round(self.touchX1 - self.scaleWidth / 2);
            var imgTop = Math.round(self.touchY1 - self.scaleHeight / 2);
            self.outsideBound(imgLeft, imgTop);
            self.updateCanvas();
        };
        self.__xtouchEnd = function() {
            self.oldScale = self.newScale;
            self.rectX = self.imgLeft;
            self.rectY = self.imgTop;
        };
    }
    var handle = {
        //  图片手势初始监测
        touchStart: function touchStart(e) {
            var self = this;
            var ref = e.touches;
            var touch0 = ref[0];
            var touch1 = ref[1];
            if (!self.src) {
                return
            }
            setTouchState(self, true, null, null);
            // 计算第一个触摸点的位置,并参照改点进行缩放
            self.__oneTouchStart(touch0);
            // 两指手势触发
            if (e.touches.length >= 2) {
                self.__twoTouchStart(touch0, touch1);
            }
        },
        //  图片手势动态缩放
        touchMove: function touchMove(e) {
            var self = this;
            var ref = e.touches;
            var touch0 = ref[0];
            var touch1 = ref[1];
            if (!self.src) {
                return
            }
            setTouchState(self, null, true);
            // 单指手势时触发
            if (e.touches.length === 1) {
                self.__oneTouchMove(touch0);
            }
            // 两指手势触发
            if (e.touches.length >= 2) {
                self.__twoTouchMove(touch0, touch1);
            }
        },
        touchEnd: function touchEnd(e) {
            var self = this;
            if (!self.src) {
                return
            }
            setTouchState(self, false, false, true);
            self.__xtouchEnd();
        }
    };
    function cut() {
        var self = this;
        var boundWidth = self.width; // 裁剪框默认宽度,即整个画布宽度
        var boundHeight = self.height;
        // 裁剪框默认高度,即整个画布高度
        var ref = self.cut;
        var x = ref.x;
        if (x === void 0) x = 0;
        var y = ref.y;
        if (y === void 0) y = 0;
        var width = ref.width;
        if (width === void 0) width = boundWidth;
        var height = ref.height;
        if (height === void 0) height = boundHeight;
        /**
         * 设置边界
         * @param imgLeft 图片左上角横坐标值
         * @param imgTop 图片左上角纵坐标值
         */
        self.outsideBound = function(imgLeft, imgTop) {
            self.imgLeft = imgLeft >= x ?
                x :
                self.scaleWidth + imgLeft - x <= width ?
                x + width - self.scaleWidth :
                imgLeft;
            self.imgTop = imgTop >= y ?
                y :
                self.scaleHeight + imgTop - y <= height ?
                y + height - self.scaleHeight :
                imgTop;
        };
        /**
         * 设置边界样式
         * @param color    边界颜色
         */
        self.setBoundStyle = function(ref) {
            if (ref === void 0) ref = {};
            var color = ref.color;
            if (color === void 0) color = '#04b00f';
            var mask = ref.mask;
            if (mask === void 0) mask = 'rgba(0, 0, 0, 0.3)';
            var lineWidth = ref.lineWidth;
            if (lineWidth === void 0) lineWidth = 1;
            var half = lineWidth / 2;
            var boundOption = [{
                    start: {
                        x: x - half,
                        y: y + 10 - half
                    },
                    step1: {
                        x: x - half,
                        y: y - half
                    },
                    step2: {
                        x: x + 10 - half,
                        y: y - half
                    }
                },
                {
                    start: {
                        x: x - half,
                        y: y + height - 10 + half
                    },
                    step1: {
                        x: x - half,
                        y: y + height + half
                    },
                    step2: {
                        x: x + 10 - half,
                        y: y + height + half
                    }
                },
                {
                    start: {
                        x: x + width - 10 + half,
                        y: y - half
                    },
                    step1: {
                        x: x + width + half,
                        y: y - half
                    },
                    step2: {
                        x: x + width + half,
                        y: y + 10 - half
                    }
                },
                {
                    start: {
                        x: x + width + half,
                        y: y + height - 10 + half
                    },
                    step1: {
                        x: x + width + half,
                        y: y + height + half
                    },
                    step2: {
                        x: x + width - 10 + half,
                        y: y + height + half
                    }
                }
            ];
            // 绘制半透明层
            self.ctx.beginPath();
            self.ctx.setFillStyle(mask);
            self.ctx.fillRect(0, 0, x, boundHeight);
            self.ctx.fillRect(x, 0, width, y);
            self.ctx.fillRect(x, y + height, width, boundHeight - y - height);
            self.ctx.fillRect(x + width, 0, boundWidth - x - width, boundHeight);
            self.ctx.fill();
            boundOption.forEach(function(op) {
                self.ctx.beginPath();
                self.ctx.setStrokeStyle(color);
                self.ctx.setLineWidth(lineWidth);
                self.ctx.moveTo(op.start.x, op.start.y);
                self.ctx.lineTo(op.step1.x, op.step1.y);
                self.ctx.lineTo(op.step2.x, op.step2.y);
                self.ctx.stroke();
            });
        };
    }
    var version = "1.3.9";
    var WeCropper = function WeCropper(params) {
        var self = this;
        var _default = {};
        validator(self, DEFAULT);
        Object.keys(DEFAULT).forEach(function(key) {
            _default[key] = DEFAULT[key].default;
        });
        Object.assign(self, _default, params);
        self.prepare();
        self.attachPage();
        self.createCtx();
        self.observer();
        self.cutt();
        self.methods();
        self.init();
        self.update();
        return self
    };
    WeCropper.prototype.init = function init() {
        var self = this;
        var src = self.src;
        self.version = version;
        typeof self.onReady === 'function' && self.onReady(self.ctx, self);
        if (src) {
            self.pushOrign(src);
        } else {
            self.updateCanvas();
        }
        setTouchState(self, false, false, false);
        self.oldScale = 1;
        self.newScale = 1;
        return self
    };
    Object.assign(WeCropper.prototype, handle);
    WeCropper.prototype.prepare = prepare;
    WeCropper.prototype.observer = observer;
    WeCropper.prototype.methods = methods;
    WeCropper.prototype.cutt = cut;
    WeCropper.prototype.update = update;
    return WeCropper;
})));
uview-ui/components/u-avatar/u-avatar.vue
New file
@@ -0,0 +1,244 @@
<template>
    <view class="u-avatar" :style="[wrapStyle]" @tap="click">
        <image
            @error="loadError"
            :style="[imgStyle]"
            class="u-avatar__img"
            v-if="!uText && avatar"
            :src="avatar"
            :mode="imgMode"
        ></image>
        <text class="u-line-1" v-else-if="uText" :style="{
            fontSize: '38rpx'
        }">{{uText}}</text>
        <slot v-else></slot>
        <view class="u-avatar__sex" v-if="showSex" :class="['u-avatar__sex--' + sexIcon]" :style="[uSexStyle]">
            <u-icon :name="sexIcon" size="20"></u-icon>
        </view>
        <view class="u-avatar__level" v-if="showLevel" :style="[uLevelStyle]">
            <u-icon :name="levelIcon" size="20"></u-icon>
        </view>
    </view>
</template>
<script>
    let base64Avatar = "data:image/jpg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMraHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjMtYzAxMSA2Ni4xNDU2NjEsIDIwMTIvMDIvMDYtMTQ6NTY6MjcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjREMEQwRkY0RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjREMEQwRkY1RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NEQwRDBGRjJGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NEQwRDBGRjNGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAGBAQEBQQGBQUGCQYFBgkLCAYGCAsMCgoLCgoMEAwMDAwMDBAMDg8QDw4MExMUFBMTHBsbGxwfHx8fHx8fHx8fAQcHBw0MDRgQEBgaFREVGh8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx//wAARCADIAMgDAREAAhEBAxEB/8QAcQABAQEAAwEBAAAAAAAAAAAAAAUEAQMGAgcBAQAAAAAAAAAAAAAAAAAAAAAQAAIBAwICBgkDBQAAAAAAAAABAhEDBCEFMVFBYXGREiKBscHRMkJSEyOh4XLxYjNDFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A/fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHbHFyZ/Dam+yLA+Z2L0Pjtyj2poD4AAAAAAAAAAAAAAAAAAAAAAAAKWFs9y6lcvvwQeqj8z9wFaziY1n/HbUX9XF97A7QAGXI23EvJ1goyfzR0YEfN269jeZ+a03pNe0DIAAAAAAAAAAAAAAAAAAAACvtO3RcVkXlWutuL9YFYAAAAAOJRjKLjJVi9GmB5/csH/mu1h/in8PU+QGMAAAAAAAAAAAAAAAAAAaMDG/6MmMH8C80+xAelSSVFolwQAAAAAAAHVlWI37ErUulaPk+hgeYnCUJuElSUXRrrQHAAAAAAAAAAAAAAAAABa2Oz4bM7r4zdF2ICmAAAAAAAAAg7zZ8GX41wuJP0rRgYAAAAAAAAAAAAAAAAAD0m2R8ODaXU33tsDSAAAAAAAAAlb9HyWZcnJd9PcBHAAAAAAAAAAAAAAAAAPS7e64Vn+KA0AAAAAAAAAJm+v8Ftf3ewCKAAAAAAAAAAAAAAAAAX9muqeGo9NttP06+0DcAAAAAAAAAjb7dTu2ra+VOT9P8AQCWAAAAAAAAAAAAAAAAAUNmyPt5Ltv4bui/kuAF0AAAAAAADiUlGLlJ0SVW+oDzOXfd/Ind6JPRdS0QHSAAAAAAAAAAAAAAAAAE2nVaNcGB6Lbs6OTao9LsF51z60BrAAAAAABJ3jOVHjW3r/sa9QEgAAAAAAAAAAAAAAAAAAAPu1duWriuW34ZR4MC9hbnZyEoy8l36XwfYBsAAADaSq9EuLAlZ+7xSdrGdW9Hc5dgEdtt1erfFgAAAAAAAAAAAAAAAAADVjbblX6NR8MH80tEBRs7HYivyzlN8lovaBPzduvY0m6eK10TXtAyAarO55lpJK54orolr+4GqO/Xaea1FvqbXvA+Z77kNeW3GPbV+4DJfzcm/pcm3H6Vou5AdAFLC2ed2Pjv1txa8sV8T6wOL+yZEKu1JXFy4MDBOE4ScZxcZLinoB8gAAAAAAAAAAAB242LeyJ+C3GvN9C7QLmJtePYpKS+5c+p8F2IDYAANJqj1T4oCfk7Nj3G5Wn9qXJax7gJ93Z82D8sVNc4v30A6Xg5i42Z+iLfqARwcyT0sz9MWvWBps7LlTf5Grce9/oBTxdtxseklHxT+uWr9AGoAB138ezfj4bsFJdD6V2MCPm7RdtJzs1uW1xXzL3gTgAAAAAAAAADRhYc8q74I6RWs5ckB6GxYtWLat21SK731sDsAAAAAAAAAAAAAAAASt021NO/YjrxuQXT1oCOAAAAAAABzGLlJRSq26JAelwsWONYjbXxcZvmwO8AAAAAAAAAAAAAAAAAAef3TEWPkVivx3NY9T6UBiAAAAAABo2+VmGXblddIJ8eivRUD0oAAAAAAAAAAAAAAAAAAAYt4tKeFKVNYNSXfRgefAAAAAAAAr7VuSSWPedKaW5v1MCsAAAAAAAAAAAAAAAAAAIe6bj96Ts2n+JPzSXzP3ATgAAAAAAAAFbbt1UUrOQ9FpC4/UwK6aaqtU+DAAAAAAAAAAAAAAA4lKMIuUmoxWrb4ARNx3R3q2rLpa4Sl0y/YCcAAAAAAAAAAANmFud7G8r89r6X0dgFvGzLGRGtuWvTF6NAdwAAAAAAAAAAAy5W442PVN+K59EePp5ARMvOv5MvO6QXCC4AZwAAAAAAAAAAAAAcxlKLUotprg1owN+PvORborq+7Hnwl3gUbO74VzRydt8pKn68ANcJwmqwkpLmnUDkAAAAfNy9atqtyagut0AxXt5xIV8Fbj6lRd7Am5G65V6qUvtwfyx94GMAAAAAAAAAAAAAAAAAAAOU2nVOj5gdsc3LiqRvTpyqwOxbnnrhdfpSfrQB7pnv/AGvuS9gHXPMy5/Fem1yq0v0A6W29XqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//Z";
    /**
     * avatar 头像
     * @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
     * @tutorial https://www.uviewui.com/components/avatar.html
     * @property {String} bg-color 背景颜色,一般显示文字时用(默认#ffffff)
     * @property {String} src 头像路径,如加载失败,将会显示默认头像
     * @property {String Number} size 头像尺寸,可以为指定字符串(large, default, mini),或者数值,单位rpx(默认default)
     * @property {String} mode 显示类型,见上方说明(默认circle)
     * @property {String} sex-icon 性别图标,man-男,woman-女(默认man)
     * @property {String} level-icon 等级图标(默认level)
     * @property {String} sex-bg-color 性别图标背景颜色
     * @property {String} level-bg-color 等级图标背景颜色
     * @property {String} show-sex 是否显示性别图标(默认false)
     * @property {String} show-level 是否显示等级图标(默认false)
     * @property {String} img-mode 头像图片的裁剪类型,与uni的image组件的mode参数一致,如效果达不到需求,可尝试传widthFix值(默认aspectFill)
     * @property {String} index 用户传递的标识符值,如果是列表循环,可穿v-for的index值
     * @event {Function} click 头像被点击
     * @example <u-avatar :src="src"></u-avatar>
     */
    export default {
        name: 'u-avatar',
        props: {
            // 背景颜色
            bgColor: {
                type: String,
                default: 'transparent'
            },
            // 头像路径
            src: {
                type: String,
                default: ''
            },
            // 尺寸,large-大,default-中等,mini-小,如果为数值,则单位为rpx
            // 宽度等于高度
            size: {
                type: [String, Number],
                default: 'default'
            },
            // 头像模型,square-带圆角方形,circle-圆形
            mode: {
                type: String,
                default: 'circle'
            },
            // 文字内容
            text: {
                type: String,
                default: ''
            },
            // 图片的裁剪模型
            imgMode: {
                type: String,
                default: 'aspectFill'
            },
            // 标识符
            index: {
                type: [String, Number],
                default: ''
            },
            // 右上角性别角标,man-男,woman-女
            sexIcon: {
                type: String,
                default: 'man'
            },
            // 右下角的等级图标
            levelIcon: {
                type: String,
                default: 'level'
            },
            // 右下角等级图标背景颜色
            levelBgColor: {
                type: String,
                default: ''
            },
            // 右上角性别图标的背景颜色
            sexBgColor: {
                type: String,
                default: ''
            },
            // 是否显示性别图标
            showSex: {
                type: Boolean,
                default: false
            },
            // 是否显示等级图标
            showLevel: {
                type: Boolean,
                default: false
            }
        },
        data() {
            return {
                error: false,
                // 头像的地址,因为如果加载错误,需要赋值为默认图片,props值无法修改,所以需要一个中间值
                avatar: this.src ? this.src : base64Avatar,
            }
        },
        watch: {
            src(n) {
                // 用户可能会在头像加载失败时,再次修改头像值,所以需要重新赋值
                if(!n) {
                    // 如果传入null或者'',或者undefined,显示默认头像
                    this.avatar = base64Avatar;
                    this.error = true;
                } else {
                    this.avatar = n;
                    this.error = false;
                }
            }
        },
        computed: {
            wrapStyle() {
                let style = {};
                style.height = this.size == 'large' ? '120rpx' : this.size == 'default' ?
                '90rpx' : this.size == 'mini' ? '70rpx' : this.size + 'rpx';
                style.width = style.height;
                style.flex = `0 0 ${style.height}`;
                style.backgroundColor = this.bgColor;
                style.borderRadius = this.mode == 'circle' ? '500px' : '5px';
                if(this.text) style.padding = `0 6rpx`;
                return style;
            },
            imgStyle() {
                let style = {};
                style.borderRadius = this.mode == 'circle' ? '500px' : '5px';
                return style;
            },
            // 取字符串的第一个字符
            uText() {
                return String(this.text)[0];
            },
            // 性别图标的自定义样式
            uSexStyle() {
                let style = {};
                if(this.sexBgColor) style.backgroundColor = this.sexBgColor;
                return style;
            },
            // 等级图标的自定义样式
            uLevelStyle() {
                let style = {};
                if(this.levelBgColor) style.backgroundColor = this.levelBgColor;
                return style;
            }
        },
        methods: {
            // 图片加载错误时,显示默认头像
            loadError() {
                this.error = true;
                this.avatar = base64Avatar;
            },
            click() {
                this.$emit('click', this.index);
            }
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-avatar {
        /* #ifndef APP-NVUE */
        display: inline-flex;
        /* #endif */
        align-items: center;
        justify-content: center;
        font-size: 28rpx;
        color: $u-content-color;
        border-radius: 10px;
        position: relative;
        &__img {
            width: 100%;
            height: 100%;
        }
        &__sex {
            position: absolute;
            width: 32rpx;
            color: #ffffff;
            height: 32rpx;
            @include vue-flex;
            justify-content: center;
            align-items: center;
            border-radius: 100rpx;
            top: 5%;
            z-index: 1;
            right: -7%;
            border: 1px #ffffff solid;
            &--man {
                background-color: $u-type-primary;
            }
            &--woman {
                background-color: $u-type-error;
            }
            &--none {
                background-color: $u-type-warning;
            }
        }
        &__level {
            position: absolute;
            width: 32rpx;
            color: #ffffff;
            height: 32rpx;
            @include vue-flex;
            justify-content: center;
            align-items: center;
            border-radius: 100rpx;
            bottom: 5%;
            z-index: 1;
            right: -7%;
            border: 1px #ffffff solid;
            background-color: $u-type-warning;
        }
    }
</style>
uview-ui/components/u-back-top/u-back-top.vue
New file
@@ -0,0 +1,153 @@
<template>
    <view @tap="backToTop" class="u-back-top" :class="['u-back-top--mode--' + mode]" :style="[{
        bottom: bottom + 'rpx',
        right: right + 'rpx',
        borderRadius: mode == 'circle' ? '10000rpx' : '8rpx',
        zIndex: uZIndex,
        opacity: opacity
    }, customStyle]">
        <view class="u-back-top__content" v-if="!$slots.default && !$slots.$default">
            <u-icon @click="backToTop" :name="icon" :custom-style="iconStyle"></u-icon>
            <view class="u-back-top__content__tips">
                {{tips}}
            </view>
        </view>
        <slot v-else />
    </view>
</template>
<script>
    export default {
        name: 'u-back-top',
        props: {
            // 返回顶部的形状,circle-圆形,square-方形
            mode: {
                type: String,
                default: 'circle'
            },
            // 自定义图标
            icon: {
                type: String,
                default: 'arrow-upward'
            },
            // 提示文字
            tips: {
                type: String,
                default: ''
            },
            // 返回顶部滚动时间
            duration: {
                type: [Number, String],
                default: 100
            },
            // 滚动距离
            scrollTop: {
                type: [Number, String],
                default: 0
            },
            // 距离顶部多少距离显示,单位rpx
            top: {
                type: [Number, String],
                default: 400
            },
            // 返回顶部按钮到底部的距离,单位rpx
            bottom: {
                type: [Number, String],
                default: 200
            },
            // 返回顶部按钮到右边的距离,单位rpx
            right: {
                type: [Number, String],
                default: 40
            },
            // 层级
            zIndex: {
                type: [Number, String],
                default: '9'
            },
            // 图标的样式,对象形式
            iconStyle: {
                type: Object,
                default() {
                    return {
                        color: '#909399',
                        fontSize: '38rpx'
                    }
                }
            },
            // 整个组件的样式
            customStyle: {
                type: Object,
                default() {
                    return {}
                }
            }
        },
        watch: {
            showBackTop(nVal, oVal) {
                // 当组件的显示与隐藏状态发生跳变时,修改组件的层级和不透明度
                // 让组件有显示和消失的动画效果,如果用v-if控制组件状态,将无设置动画效果
                if(nVal) {
                    this.uZIndex = this.zIndex;
                    this.opacity = 1;
                } else {
                    this.uZIndex = -1;
                    this.opacity = 0;
                }
            }
        },
        computed: {
            showBackTop() {
                // 由于scrollTop为页面的滚动距离,默认为px单位,这里将用于传入的top(rpx)值
                // 转为px用于比较,如果滚动条到顶的距离大于设定的距离,就显示返回顶部的按钮
                return this.scrollTop > uni.upx2px(this.top);
            },
        },
        data() {
            return {
                // 不透明度,为了让组件有一个显示和隐藏的过渡动画
                opacity: 0,
                // 组件的z-index值,隐藏时设置为-1,就会看不到
                uZIndex: -1
            }
        },
        methods: {
            backToTop() {
                uni.pageScrollTo({
                    scrollTop: 0,
                    duration: this.duration
                });
            }
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-back-top {
        width: 80rpx;
        height: 80rpx;
        position: fixed;
        z-index: 9;
        @include vue-flex;
        flex-direction: column;
        justify-content: center;
        background-color: #E1E1E1;
        color: $u-content-color;
        align-items: center;
        transition: opacity 0.4s;
        &__content {
            @include vue-flex;
            flex-direction: column;
            align-items: center;
            &__tips {
                font-size: 24rpx;
                transform: scale(0.8);
                line-height: 1;
            }
        }
    }
</style>
uview-ui/components/u-badge/u-badge.vue
New file
@@ -0,0 +1,216 @@
<template>
    <view v-if="show" class="u-badge" :class="[
            isDot ? 'u-badge-dot' : '',
            size == 'mini' ? 'u-badge-mini' : '',
            type ? 'u-badge--bg--' + type : ''
        ]" :style="[{
            top: offset[0] + 'rpx',
            right: offset[1] + 'rpx',
            fontSize: fontSize + 'rpx',
            position: absolute ? 'absolute' : 'static',
            color: color,
            backgroundColor: bgColor
        }, boxStyle]"
    >
        {{showText}}
    </view>
</template>
<script>
    /**
     * badge 角标
     * @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
     * @tutorial https://www.uviewui.com/components/badge.html
     * @property {String Number} count 展示的数字,大于 overflowCount 时显示为 ${overflowCount}+,为0且show-zero为false时隐藏
     * @property {Boolean} is-dot 不展示数字,只有一个小点(默认false)
     * @property {Boolean} absolute 组件是否绝对定位,为true时,offset参数才有效(默认true)
     * @property {String Number} overflow-count 展示封顶的数字值(默认99)
     * @property {String} type 使用预设的背景颜色(默认error)
     * @property {Boolean} show-zero 当数值为 0 时,是否展示 Badge(默认false)
     * @property {String} size Badge的尺寸,设为mini会得到小一号的Badge(默认default)
     * @property {Array} offset 设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,单位rpx。absolute为true时有效(默认[20, 20])
     * @property {String} color 字体颜色(默认#ffffff)
     * @property {String} bgColor 背景颜色,优先级比type高,如设置,type参数会失效
     * @property {Boolean} is-center 组件中心点是否和父组件右上角重合,优先级比offset高,如设置,offset参数会失效(默认false)
     * @example <u-badge type="error" count="7"></u-badge>
     */
    export default {
        name: 'u-badge',
        props: {
            // primary,warning,success,error,info
            type: {
                type: String,
                default: 'error'
            },
            // default, mini
            size: {
                type: String,
                default: 'default'
            },
            //是否是圆点
            isDot: {
                type: Boolean,
                default: false
            },
            // 显示的数值内容
            count: {
                type: [Number, String],
            },
            // 展示封顶的数字值
            overflowCount: {
                type: Number,
                default: 99
            },
            // 当数值为 0 时,是否展示 Badge
            showZero: {
                type: Boolean,
                default: false
            },
            // 位置偏移
            offset: {
                type: Array,
                default: () => {
                    return [20, 20]
                }
            },
            // 是否开启绝对定位,开启了offset才会起作用
            absolute: {
                type: Boolean,
                default: true
            },
            // 字体大小
            fontSize: {
                type: [String, Number],
                default: '24'
            },
            // 字体演示
            color: {
                type: String,
                default: '#ffffff'
            },
            // badge的背景颜色
            bgColor: {
                type: String,
                default: ''
            },
            // 是否让badge组件的中心点和父组件右上角重合,配置的话,offset将会失效
            isCenter: {
                type: Boolean,
                default: false
            }
        },
        computed: {
            // 是否将badge中心与父组件右上角重合
            boxStyle() {
                let style = {};
                if(this.isCenter) {
                    style.top = 0;
                    style.right = 0;
                    // Y轴-50%,意味着badge向上移动了badge自身高度一半,X轴50%,意味着向右移动了自身宽度一半
                    style.transform = "translateY(-50%) translateX(50%)";
                } else {
                    style.top = this.offset[0] + 'rpx';
                    style.right = this.offset[1] + 'rpx';
                    style.transform = "translateY(0) translateX(0)";
                }
                // 如果尺寸为mini,后接上scal()
                if(this.size == 'mini') {
                    style.transform = style.transform + " scale(0.8)";
                }
                return style;
            },
            // isDot类型时,不显示文字
            showText() {
                if(this.isDot) return '';
                else {
                    if(this.count > this.overflowCount) return `${this.overflowCount}+`;
                    else return this.count;
                }
            },
            // 是否显示组件
            show() {
                // 如果count的值为0,并且showZero设置为false,不显示组件
                if(this.count == 0 && this.showZero == false) return false;
                else return true;
            }
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-badge {
        /* #ifndef APP-NVUE */
        display: inline-flex;
        /* #endif */
        justify-content: center;
        align-items: center;
        line-height: 24rpx;
        padding: 4rpx 8rpx;
        border-radius: 100rpx;
        z-index: 9;
        &--bg--primary {
            background-color: $u-type-primary;
        }
        &--bg--error {
            background-color: $u-type-error;
        }
        &--bg--success {
            background-color: $u-type-success;
        }
        &--bg--info {
            background-color: $u-type-info;
        }
        &--bg--warning {
            background-color: $u-type-warning;
        }
    }
    .u-badge-dot {
        height: 16rpx;
        width: 16rpx;
        border-radius: 100rpx;
        line-height: 1;
    }
    .u-badge-mini {
        transform: scale(0.8);
        transform-origin: center center;
    }
    // .u-primary {
    //     background: $u-type-primary;
    //     color: #fff;
    // }
    // .u-error {
    //     background: $u-type-error;
    //     color: #fff;
    // }
    // .u-warning {
    //     background: $u-type-warning;
    //     color: #fff;
    // }
    // .u-success {
    //     background: $u-type-success;
    //     color: #fff;
    // }
    // .u-black {
    //     background: #585858;
    //     color: #fff;
    // }
    .u-info {
        background-color: $u-type-info;
        color: #fff;
    }
</style>
uview-ui/components/u-button/u-button.vue
New file
@@ -0,0 +1,596 @@
<template>
    <button
        id="u-wave-btn"
        class="u-btn u-line-1 u-fix-ios-appearance"
        :class="[
            'u-size-' + size,
            plain ? 'u-btn--' + type + '--plain' : '',
            loading ? 'u-loading' : '',
            shape == 'circle' ? 'u-round-circle' : '',
            hairLine ? showHairLineBorder : 'u-btn--bold-border',
            'u-btn--' + type,
            disabled ? `u-btn--${type}--disabled` : '',
        ]"
        :hover-start-time="Number(hoverStartTime)"
        :hover-stay-time="Number(hoverStayTime)"
        :disabled="disabled"
        :form-type="formType"
        :open-type="openType"
        :app-parameter="appParameter"
        :hover-stop-propagation="hoverStopPropagation"
        :send-message-title="sendMessageTitle"
        send-message-path="sendMessagePath"
        :lang="lang"
        :data-name="dataName"
        :session-from="sessionFrom"
        :send-message-img="sendMessageImg"
        :show-message-card="showMessageCard"
        @getphonenumber="getphonenumber"
        @getuserinfo="getuserinfo"
        @error="error"
        @opensetting="opensetting"
        @launchapp="launchapp"
        :style="[customStyle, {
            overflow: ripple ? 'hidden' : 'visible'
        }]"
        @tap.stop="click($event)"
        :hover-class="getHoverClass"
        :loading="loading"
    >
        <slot></slot>
        <view
            v-if="ripple"
            class="u-wave-ripple"
            :class="[waveActive ? 'u-wave-active' : '']"
            :style="{
                top: rippleTop + 'px',
                left: rippleLeft + 'px',
                width: fields.targetWidth + 'px',
                height: fields.targetWidth + 'px',
                'background-color': rippleBgColor || 'rgba(0, 0, 0, 0.15)'
            }"
        ></view>
    </button>
</template>
<script>
/**
 * button 按钮
 * @description Button 按钮
 * @tutorial https://www.uviewui.com/components/button.html
 * @property {String} size 按钮的大小
 * @property {Boolean} ripple 是否开启点击水波纹效果
 * @property {String} ripple-bg-color 水波纹的背景色,ripple为true时有效
 * @property {String} type 按钮的样式类型
 * @property {Boolean} plain 按钮是否镂空,背景色透明
 * @property {Boolean} disabled 是否禁用
 * @property {Boolean} hair-line 是否显示按钮的细边框(默认true)
 * @property {Boolean} shape 按钮外观形状,见文档说明
 * @property {Boolean} loading 按钮名称前是否带 loading 图标(App-nvue 平台,在 ios 上为雪花,Android上为圆圈)
 * @property {String} form-type 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
 * @property {String} open-type 开放能力
 * @property {String} data-name 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
 * @property {String} hover-class 指定按钮按下去的样式类。当 hover-class="none" 时,没有点击态效果(App-nvue 平台暂不支持)
 * @property {Number} hover-start-time 按住后多久出现点击态,单位毫秒
 * @property {Number} hover-stay-time 手指松开后点击态保留时间,单位毫秒
 * @property {Object} custom-style 对按钮的自定义样式,对象形式,见文档说明
 * @event {Function} click 按钮点击
 * @event {Function} getphonenumber open-type="getPhoneNumber"时有效
 * @event {Function} getuserinfo 用户点击该按钮时,会返回获取到的用户信息,从返回参数的detail中获取到的值同uni.getUserInfo
 * @event {Function} error 当使用开放能力时,发生错误的回调
 * @event {Function} opensetting 在打开授权设置页并关闭后回调
 * @event {Function} launchapp 打开 APP 成功的回调
 * @example <u-button>月落</u-button>
 */
export default {
    name: 'u-button',
    props: {
        // 是否细边框
        hairLine: {
            type: Boolean,
            default: true
        },
        // 按钮的预置样式,default,primary,error,warning,success
        type: {
            type: String,
            default: 'default'
        },
        // 按钮尺寸,default,medium,mini
        size: {
            type: String,
            default: 'default'
        },
        // 按钮形状,circle(两边为半圆),square(带圆角)
        shape: {
            type: String,
            default: 'square'
        },
        // 按钮是否镂空
        plain: {
            type: Boolean,
            default: false
        },
        // 是否禁止状态
        disabled: {
            type: Boolean,
            default: false
        },
        // 是否加载中
        loading: {
            type: Boolean,
            default: false
        },
        // 开放能力,具体请看uniapp稳定关于button组件部分说明
        // https://uniapp.dcloud.io/component/button
        openType: {
            type: String,
            default: ''
        },
        // 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
        // 取值为submit(提交表单),reset(重置表单)
        formType: {
            type: String,
            default: ''
        },
        // 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效
        // 只微信小程序、QQ小程序有效
        appParameter: {
            type: String,
            default: ''
        },
        // 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效
        hoverStopPropagation: {
            type: Boolean,
            default: false
        },
        // 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。只微信小程序有效
        lang: {
            type: String,
            default: 'en'
        },
        // 会话来源,open-type="contact"时有效。只微信小程序有效
        sessionFrom: {
            type: String,
            default: ''
        },
        // 会话内消息卡片标题,open-type="contact"时有效
        // 默认当前标题,只微信小程序有效
        sendMessageTitle: {
            type: String,
            default: ''
        },
        // 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效
        // 默认当前分享路径,只微信小程序有效
        sendMessagePath: {
            type: String,
            default: ''
        },
        // 会话内消息卡片图片,open-type="contact"时有效
        // 默认当前页面截图,只微信小程序有效
        sendMessageImg: {
            type: String,
            default: ''
        },
        // 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,
        // 用户点击后可以快速发送小程序消息,open-type="contact"时有效
        showMessageCard: {
            type: Boolean,
            default: false
        },
        // 手指按(触摸)按钮时按钮时的背景颜色
        hoverBgColor: {
            type: String,
            default: ''
        },
        // 水波纹的背景颜色
        rippleBgColor: {
            type: String,
            default: ''
        },
        // 是否开启水波纹效果
        ripple: {
            type: Boolean,
            default: false
        },
        // 按下的类名
        hoverClass: {
            type: String,
            default: ''
        },
        // 自定义样式,对象形式
        customStyle: {
            type: Object,
            default() {
                return {};
            }
        },
        // 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
        dataName: {
            type: String,
            default: ''
        },
        // 节流,一定时间内只能触发一次
        throttleTime: {
            type: [String, Number],
            default: 1000
        },
        // 按住后多久出现点击态,单位毫秒
        hoverStartTime: {
            type: [String, Number],
            default: 20
        },
        // 手指松开后点击态保留时间,单位毫秒
        hoverStayTime: {
            type: [String, Number],
            default: 150
        },
    },
    computed: {
        // 当没有传bgColor变量时,按钮按下去的颜色类名
        getHoverClass() {
            // 如果开启水波纹效果,则不启用hover-class效果
            if (this.loading || this.disabled || this.ripple || this.hoverClass) return '';
            let hoverClass = '';
            hoverClass = this.plain ? 'u-' + this.type + '-plain-hover' : 'u-' + this.type + '-hover';
            return hoverClass;
        },
        // 在'primary', 'success', 'error', 'warning'类型下,不显示边框,否则会造成四角有毛刺现象
        showHairLineBorder() {
            if (['primary', 'success', 'error', 'warning'].indexOf(this.type) >= 0 && !this.plain) {
                return '';
            } else {
                return 'u-hairline-border';
            }
        }
    },
    data() {
        return {
            rippleTop: 0, // 水波纹的起点Y坐标到按钮上边界的距离
            rippleLeft: 0, // 水波纹起点X坐标到按钮左边界的距离
            fields: {}, // 波纹按钮节点信息
            waveActive: false // 激活水波纹
        };
    },
    methods: {
        // 按钮点击
        click(e) {
            // 进行节流控制,每this.throttle毫秒内,只在开始处执行
            this.$u.throttle(() => {
                // 如果按钮时disabled和loading状态,不触发水波纹效果
                if (this.loading === true || this.disabled === true) return;
                // 是否开启水波纹效果
                if (this.ripple) {
                    // 每次点击时,移除上一次的类,再次添加,才能触发动画效果
                    this.waveActive = false;
                    this.$nextTick(function() {
                        this.getWaveQuery(e);
                    });
                }
                this.$emit('click', e);
            }, this.throttleTime);
        },
        // 查询按钮的节点信息
        getWaveQuery(e) {
            this.getElQuery().then(res => {
                // 查询返回的是一个数组节点
                let data = res[0];
                // 查询不到节点信息,不操作
                if (!data.width || !data.width) return;
                // 水波纹的最终形态是一个正方形(通过border-radius让其变为一个圆形),这里要保证正方形的边长等于按钮的最长边
                // 最终的方形(变换后的圆形)才能覆盖整个按钮
                data.targetWidth = data.height > data.width ? data.height : data.width;
                if (!data.targetWidth) return;
                this.fields = data;
                let touchesX = '',
                    touchesY = '';
                // #ifdef MP-BAIDU
                touchesX = e.changedTouches[0].clientX;
                touchesY = e.changedTouches[0].clientY;
                // #endif
                // #ifdef MP-ALIPAY
                touchesX = e.detail.clientX;
                touchesY = e.detail.clientY;
                // #endif
                // #ifndef MP-BAIDU || MP-ALIPAY
                touchesX = e.touches[0].clientX;
                touchesY = e.touches[0].clientY;
                // #endif
                // 获取触摸点相对于按钮上边和左边的x和y坐标,原理是通过屏幕的触摸点(touchesY),减去按钮的上边界data.top
                // 但是由于`transform-origin`默认是center,所以这里再减去半径才是水波纹view应该的位置
                // 总的来说,就是把水波纹的矩形(变换后的圆形)的中心点,移动到我们的触摸点位置
                this.rippleTop = touchesY - data.top - data.targetWidth / 2;
                this.rippleLeft = touchesX - data.left - data.targetWidth / 2;
                this.$nextTick(() => {
                    this.waveActive = true;
                });
            });
        },
        // 获取节点信息
        getElQuery() {
            return new Promise(resolve => {
                let queryInfo = '';
                // 获取元素节点信息,请查看uniapp相关文档
                // https://uniapp.dcloud.io/api/ui/nodes-info?id=nodesrefboundingclientrect
                queryInfo = uni.createSelectorQuery().in(this);
                //#ifdef MP-ALIPAY
                queryInfo = uni.createSelectorQuery();
                //#endif
                queryInfo.select('.u-btn').boundingClientRect();
                queryInfo.exec(data => {
                    resolve(data);
                });
            });
        },
        // 下面为对接uniapp官方按钮开放能力事件回调的对接
        getphonenumber(res) {
            this.$emit('getphonenumber', res);
        },
        getuserinfo(res) {
            this.$emit('getuserinfo', res);
        },
        error(res) {
            this.$emit('error', res);
        },
        opensetting(res) {
            this.$emit('opensetting', res);
        },
        launchapp(res) {
            this.$emit('launchapp', res);
        }
    }
};
</script>
<style scoped lang="scss">
@import '../../libs/css/style.components.scss';
.u-btn::after {
    border: none;
}
.u-btn {
    position: relative;
    border: 0;
    //border-radius: 10rpx;
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
    // 避免边框某些场景可能被“裁剪”,不能设置为hidden
    overflow: visible;
    line-height: 1;
    @include vue-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0 40rpx;
    z-index: 1;
    box-sizing: border-box;
    transition: all 0.15s;
    &--bold-border {
        border: 1px solid #ffffff;
    }
    &--default {
        color: $u-content-color;
        border-color: #c0c4cc;
        background-color: #ffffff;
    }
    &--primary {
        color: #ffffff;
        border-color: $u-type-primary;
        background-color: $u-type-primary;
    }
    &--success {
        color: #ffffff;
        border-color: $u-type-success;
        background-color: $u-type-success;
    }
    &--error {
        color: #ffffff;
        border-color: $u-type-error;
        background-color: $u-type-error;
    }
    &--warning {
        color: #ffffff;
        border-color: $u-type-warning;
        background-color: $u-type-warning;
    }
    &--default--disabled {
        color: #ffffff;
        border-color: #e4e7ed;
        background-color: #ffffff;
    }
    &--primary--disabled {
        color: #ffffff!important;
        border-color: $u-type-primary-disabled!important;
        background-color: $u-type-primary-disabled!important;
    }
    &--success--disabled {
        color: #ffffff!important;
        border-color: $u-type-success-disabled!important;
        background-color: $u-type-success-disabled!important;
    }
    &--error--disabled {
        color: #ffffff!important;
        border-color: $u-type-error-disabled!important;
        background-color: $u-type-error-disabled!important;
    }
    &--warning--disabled {
        color: #ffffff!important;
        border-color: $u-type-warning-disabled!important;
        background-color: $u-type-warning-disabled!important;
    }
    &--primary--plain {
        color: $u-type-primary!important;
        border-color: $u-type-primary-disabled!important;
        background-color: $u-type-primary-light!important;
    }
    &--success--plain {
        color: $u-type-success!important;
        border-color: $u-type-success-disabled!important;
        background-color: $u-type-success-light!important;
    }
    &--error--plain {
        color: $u-type-error!important;
        border-color: $u-type-error-disabled!important;
        background-color: $u-type-error-light!important;
    }
    &--warning--plain {
        color: $u-type-warning!important;
        border-color: $u-type-warning-disabled!important;
        background-color: $u-type-warning-light!important;
    }
}
.u-hairline-border:after {
    content: ' ';
    position: absolute;
    pointer-events: none;
    // 设置为border-box,意味着下面的scale缩小为0.5,实际上缩小的是伪元素的内容(border-box意味着内容不含border)
    box-sizing: border-box;
    // 中心点作为变形(scale())的原点
    -webkit-transform-origin: 0 0;
    transform-origin: 0 0;
    left: 0;
    top: 0;
    width: 199.8%;
    height: 199.7%;
    -webkit-transform: scale(0.5, 0.5);
    transform: scale(0.5, 0.5);
    border: 1px solid currentColor;
    z-index: 1;
}
.u-wave-ripple {
    z-index: 0;
    position: absolute;
    border-radius: 100%;
    background-clip: padding-box;
    pointer-events: none;
    user-select: none;
    transform: scale(0);
    opacity: 1;
    transform-origin: center;
}
.u-wave-ripple.u-wave-active {
    opacity: 0;
    transform: scale(2);
    transition: opacity 1s linear, transform 0.4s linear;
}
.u-round-circle {
    border-radius: 100rpx;
}
.u-round-circle::after {
    border-radius: 100rpx;
}
.u-loading::after {
    background-color: hsla(0, 0%, 100%, 0.35);
}
.u-size-default {
    font-size: 30rpx;
    height: 80rpx;
    line-height: 80rpx;
}
.u-size-medium {
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
    width: auto;
    font-size: 26rpx;
    height: 70rpx;
    line-height: 70rpx;
    padding: 0 80rpx;
}
.u-size-mini {
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
    width: auto;
    font-size: 22rpx;
    padding-top: 1px;
    height: 50rpx;
    line-height: 50rpx;
    padding: 0 20rpx;
}
.u-primary-plain-hover {
    color: #ffffff !important;
    background: $u-type-primary-dark !important;
}
.u-default-plain-hover {
    color: $u-type-primary-dark !important;
    background: $u-type-primary-light !important;
}
.u-success-plain-hover {
    color: #ffffff !important;
    background: $u-type-success-dark !important;
}
.u-warning-plain-hover {
    color: #ffffff !important;
    background: $u-type-warning-dark !important;
}
.u-error-plain-hover {
    color: #ffffff !important;
    background: $u-type-error-dark !important;
}
.u-info-plain-hover {
    color: #ffffff !important;
    background: $u-type-info-dark !important;
}
.u-default-hover {
    color: $u-type-primary-dark !important;
    border-color: $u-type-primary-dark !important;
    background-color: $u-type-primary-light !important;
}
.u-primary-hover {
    background: $u-type-primary-dark !important;
    color: #fff;
}
.u-success-hover {
    background: $u-type-success-dark !important;
    color: #fff;
}
.u-info-hover {
    background: $u-type-info-dark !important;
    color: #fff;
}
.u-warning-hover {
    background: $u-type-warning-dark !important;
    color: #fff;
}
.u-error-hover {
    background: $u-type-error-dark !important;
    color: #fff;
}
</style>
uview-ui/components/u-calendar/u-calendar.vue
New file
@@ -0,0 +1,639 @@
<template>
    <u-popup closeable :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto"
     :safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex" :border-radius="borderRadius" :closeable="closeable">
        <view class="u-calendar">
            <view class="u-calendar__header">
                <view class="u-calendar__header__text" v-if="!$slots['tooltip']">
                    {{toolTip}}
                </view>
                <slot v-else name="tooltip" />
            </view>
            <view class="u-calendar__action u-flex u-row-center">
                <view class="u-calendar__action__icon">
                    <u-icon v-if="changeYear" name="arrow-left-double" :color="yearArrowColor" @click="changeYearHandler(0)"></u-icon>
                </view>
                <view class="u-calendar__action__icon">
                    <u-icon v-if="changeMonth" name="arrow-left" :color="monthArrowColor" @click="changeMonthHandler(0)"></u-icon>
                </view>
                <view class="u-calendar__action__text">{{ showTitle }}</view>
                <view class="u-calendar__action__icon">
                    <u-icon v-if="changeMonth" name="arrow-right" :color="monthArrowColor" @click="changeMonthHandler(1)"></u-icon>
                </view>
                <view class="u-calendar__action__icon">
                    <u-icon v-if="changeYear" name="arrow-right-double" :color="yearArrowColor" @click="changeYearHandler(1)"></u-icon>
                </view>
            </view>
            <view class="u-calendar__week-day">
                <view class="u-calendar__week-day__text" v-for="(item, index) in weekDayZh" :key="index">{{item}}</view>
            </view>
            <view class="u-calendar__content">
                <!-- 前置空白部分 -->
                <block v-for="(item, index) in weekdayArr" :key="index">
                    <view class="u-calendar__content__item"></view>
                </block>
                <view class="u-calendar__content__item" :class="{
                    'u-hover-class':openDisAbled(year,month,index+1),
                    'u-calendar__content--start-date': (mode == 'range' && startDate==`${year}-${month}-${index+1}`) || mode== 'date',
                    'u-calendar__content--end-date':(mode== 'range' && endDate==`${year}-${month}-${index+1}`) || mode == 'date'
                }" :style="{backgroundColor: getColor(index,1)}" v-for="(item, index) in daysArr" :key="index"
                 @tap="dateClick(index)">
                    <view class="u-calendar__content__item__inner" :style="{color: getColor(index,2)}">
                        <view>{{ index + 1 }}</view>
                    </view>
                    <view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && startDate==`${year}-${month}-${index+1}` && startDate!=endDate">{{startText}}</view>
                    <view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && endDate==`${year}-${month}-${index+1}`">{{endText}}</view>
                </view>
                <view class="u-calendar__content__bg-month">{{month}}</view>
            </view>
            <view class="u-calendar__bottom">
                <view class="u-calendar__bottom__choose">
                    <text>{{mode == 'date' ? activeDate : startDate}}</text>
                    <text v-if="endDate">至{{endDate}}</text>
                </view>
                <view class="u-calendar__bottom__btn">
                    <u-button :type="btnType" shape="circle" size="default" @click="btnFix(false)">确定</u-button>
                </view>
            </view>
        </view>
    </u-popup>
</template>
<script>
    /**
     * calendar 日历
     * @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中。
     * @tutorial http://uviewui.com/components/calendar.html
     * @property {String} mode 选择日期的模式,date-为单个日期,range-为选择日期范围
     * @property {Boolean} v-model 布尔值变量,用于控制日历的弹出与收起
     * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
     * @property {Boolean} change-year 是否显示顶部的切换年份方向的按钮(默认true)
     * @property {Boolean} change-month 是否显示顶部的切换月份方向的按钮(默认true)
     * @property {String Number} max-year 可切换的最大年份(默认2050)
     * @property {String Number} min-year 可切换的最小年份(默认1950)
     * @property {String Number} min-date 最小可选日期(默认1950-01-01)
     * @property {String Number} max-date 最大可选日期(默认当前日期)
     * @property {String Number} 弹窗顶部左右两边的圆角值,单位rpx(默认20)
     * @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭日历(默认true)
     * @property {String} month-arrow-color 月份切换按钮箭头颜色(默认#606266)
     * @property {String} year-arrow-color 年份切换按钮箭头颜色(默认#909399)
     * @property {String} color 日期字体的默认颜色(默认#303133)
     * @property {String} active-bg-color 起始/结束日期按钮的背景色(默认#2979ff)
     * @property {String Number} z-index 弹出时的z-index值(默认10075)
     * @property {String} active-color 起始/结束日期按钮的字体颜色(默认#ffffff)
     * @property {String} range-bg-color 起始/结束日期之间的区域的背景颜色(默认rgba(41,121,255,0.13))
     * @property {String} range-color 选择范围内字体颜色(默认#2979ff)
     * @property {String} start-text 起始日期底部的提示文字(默认 '开始')
     * @property {String} end-text 结束日期底部的提示文字(默认 '结束')
     * @property {String} btn-type 底部确定按钮的主题(默认 'primary')
     * @property {String} toolTip 顶部提示文字,如设置名为tooltip的slot,此参数将失效(默认 '选择日期')
     * @property {Boolean} closeable 是否显示右上角的关闭图标(默认true)
     * @example <u-calendar v-model="show" :mode="mode"></u-calendar>
     */
    export default {
        name: 'u-calendar',
        props: {
            safeAreaInsetBottom: {
                type: Boolean,
                default: false
            },
            // 是否允许通过点击遮罩关闭Picker
            maskCloseAble: {
                type: Boolean,
                default: true
            },
            // 通过双向绑定控制组件的弹出与收起
            value: {
                type: Boolean,
                default: false
            },
            // 弹出的z-index值
            zIndex: {
                type: [String, Number],
                default: 0
            },
            // 是否允许切换年份
            changeYear: {
                type: Boolean,
                default: true
            },
            // 是否允许切换月份
            changeMonth: {
                type: Boolean,
                default: true
            },
            // date-单个日期选择,range-开始日期+结束日期选择
            mode: {
                type: String,
                default: 'date'
            },
            // 可切换的最大年份
            maxYear: {
                type: [Number, String],
                default: 2050
            },
            // 可切换的最小年份
            minYear: {
                type: [Number, String],
                default: 1950
            },
            // 最小可选日期(不在范围内日期禁用不可选)
            minDate: {
                type: [Number, String],
                default: '1950-01-01'
            },
            /**
             * 最大可选日期
             * 默认最大值为今天,之后的日期不可选
             * 2030-12-31
             * */
            maxDate: {
                type: [Number, String],
                default: ''
            },
            // 弹窗顶部左右两边的圆角值
            borderRadius: {
                type: [String, Number],
                default: 20
            },
            // 月份切换按钮箭头颜色
            monthArrowColor: {
                type: String,
                default: '#606266'
            },
            // 年份切换按钮箭头颜色
            yearArrowColor: {
                type: String,
                default: '#909399'
            },
            // 默认日期字体颜色
            color: {
                type: String,
                default: '#303133'
            },
            // 选中|起始结束日期背景色
            activeBgColor: {
                type: String,
                default: '#2979ff'
            },
            // 选中|起始结束日期字体颜色
            activeColor: {
                type: String,
                default: '#ffffff'
            },
            // 范围内日期背景色
            rangeBgColor: {
                type: String,
                default: 'rgba(41,121,255,0.13)'
            },
            // 范围内日期字体颜色
            rangeColor: {
                type: String,
                default: '#2979ff'
            },
            // mode=range时生效,起始日期自定义文案
            startText: {
                type: String,
                default: '开始'
            },
            // mode=range时生效,结束日期自定义文案
            endText: {
                type: String,
                default: '结束'
            },
            //按钮样式类型
            btnType: {
                type: String,
                default: 'primary'
            },
            // 当前选中日期带选中效果
            isActiveCurrent: {
                type: Boolean,
                default: true
            },
            // 切换年月是否触发事件 mode=date时生效
            isChange: {
                type: Boolean,
                default: false
            },
            // 是否显示右上角的关闭图标
            closeable: {
                type: Boolean,
                default: true
            },
            // 顶部的提示文字
            toolTip: {
                type: String,
                default: '选择日期'
            }
        },
        data() {
            return {
                // 星期几,值为1-7
                weekday: 1,
                weekdayArr:[],
                // 当前月有多少天
                days: 0,
                daysArr:[],
                showTitle: '',
                year: 2020,
                month: 0,
                day: 0,
                startYear: 0,
                startMonth: 0,
                startDay: 0,
                endYear: 0,
                endMonth: 0,
                endDay: 0,
                today: '',
                activeDate: '',
                startDate: '',
                endDate: '',
                isStart: true,
                min: null,
                max: null,
                weekDayZh: ['日', '一', '二', '三', '四', '五', '六']
            };
        },
        computed: {
            dataChange() {
                return `${this.mode}-${this.minDate}-${this.maxDate}`;
            },
            uZIndex() {
                // 如果用户有传递z-index值,优先使用
                return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
            }
        },
        watch: {
            dataChange(val) {
                this.init()
            }
        },
        created() {
            this.init()
        },
        methods: {
            getColor(index, type) {
                let color = type == 1 ? '' : this.color;
                let day = index + 1
                let date = `${this.year}-${this.month}-${day}`
                let timestamp = new Date(date.replace(/\-/g, '/')).getTime();
                let start = this.startDate.replace(/\-/g, '/')
                let end = this.endDate.replace(/\-/g, '/')
                if ((this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) {
                    color = type == 1 ? this.activeBgColor : this.activeColor;
                } else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) {
                    color = type == 1 ? this.rangeBgColor : this.rangeColor;
                }
                return color;
            },
            init() {
                let now = new Date();
                this.year = now.getFullYear();
                this.month = now.getMonth() + 1;
                this.day = now.getDate();
                this.today = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`;
                this.activeDate = this.today;
                this.min = this.initDate(this.minDate);
                this.max = this.initDate(this.maxDate || this.today);
                this.startDate = "";
                this.startYear = 0;
                this.startMonth = 0;
                this.startDay = 0;
                this.endYear = 0;
                this.endMonth = 0;
                this.endDay = 0;
                this.endDate = "";
                this.isStart = true;
                this.changeData();
            },
            //日期处理
            initDate(date) {
                let fdate = date.split('-');
                return {
                    year: Number(fdate[0] || 1920),
                    month: Number(fdate[1] || 1),
                    day: Number(fdate[2] || 1)
                }
            },
            openDisAbled: function(year, month, day) {
                let bool = true;
                let date = `${year}/${month}/${day}`;
                // let today = this.today.replace(/\-/g, '/');
                let min = `${this.min.year}/${this.min.month}/${this.min.day}`;
                let max = `${this.max.year}/${this.max.month}/${this.max.day}`;
                let timestamp = new Date(date).getTime();
                if (timestamp >= new Date(min).getTime() && timestamp <= new Date(max).getTime()) {
                    bool = false;
                }
                return bool;
            },
            generateArray: function(start, end) {
                return Array.from(new Array(end + 1).keys()).slice(start);
            },
            formatNum: function(num) {
                return num < 10 ? '0' + num : num + '';
            },
            //一个月有多少天
            getMonthDay(year, month) {
                let days = new Date(year, month, 0).getDate();
                return days;
            },
            getWeekday(year, month) {
                let date = new Date(`${year}/${month}/01 00:00:00`);
                return date.getDay();
            },
            checkRange(year) {
                let overstep = false;
                if (year < this.minYear || year > this.maxYear) {
                    uni.showToast({
                        title: "日期超出范围啦~",
                        icon: 'none'
                    })
                    overstep = true;
                }
                return overstep;
            },
            changeMonthHandler(isAdd) {
                if (isAdd) {
                    let month = this.month + 1;
                    let year = month > 12 ? this.year + 1 : this.year;
                    if (!this.checkRange(year)) {
                        this.month = month > 12 ? 1 : month;
                        this.year = year;
                        this.changeData();
                    }
                } else {
                    let month = this.month - 1;
                    let year = month < 1 ? this.year - 1 : this.year;
                    if (!this.checkRange(year)) {
                        this.month = month < 1 ? 12 : month;
                        this.year = year;
                        this.changeData();
                    }
                }
            },
            changeYearHandler(isAdd) {
                let year = isAdd ? this.year + 1 : this.year - 1;
                if (!this.checkRange(year)) {
                    this.year = year;
                    this.changeData();
                }
            },
            changeData() {
                this.days = this.getMonthDay(this.year, this.month);
                this.daysArr=this.generateArray(1,this.days)
                this.weekday = this.getWeekday(this.year, this.month);
                this.weekdayArr=this.generateArray(1,this.weekday)
                this.showTitle = `${this.year}年${this.month}月`;
                if (this.isChange && this.mode == 'date') {
                    this.btnFix(true);
                }
            },
            dateClick: function(day) {
                day += 1;
                if (!this.openDisAbled(this.year, this.month, day)) {
                    this.day = day;
                    let date = `${this.year}-${this.month}-${day}`;
                    if (this.mode == 'date') {
                        this.activeDate = date;
                    } else {
                        let compare = new Date(date.replace(/\-/g, '/')).getTime() < new Date(this.startDate.replace(/\-/g, '/')).getTime()
                        if (this.isStart || compare) {
                            this.startDate = date;
                            this.startYear = this.year;
                            this.startMonth = this.month;
                            this.startDay = this.day;
                            this.endYear = 0;
                            this.endMonth = 0;
                            this.endDay = 0;
                            this.endDate = "";
                            this.activeDate = "";
                            this.isStart = false;
                        } else {
                            this.endDate = date;
                            this.endYear = this.year;
                            this.endMonth = this.month;
                            this.endDay = this.day;
                            this.isStart = true;
                        }
                    }
                }
            },
            close() {
                // 修改通过v-model绑定的父组件变量的值为false,从而隐藏日历弹窗
                this.$emit('input', false);
            },
            getWeekText(date) {
                date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`);
                let week = date.getDay();
                return '星期' + ['日', '一', '二', '三', '四', '五', '六'][week];
            },
            btnFix(show) {
                if (!show) {
                    this.close();
                }
                if (this.mode == 'date') {
                    let arr = this.activeDate.split('-')
                    let year = this.isChange ? this.year : Number(arr[0]);
                    let month = this.isChange ? this.month : Number(arr[1]);
                    let day = this.isChange ? this.day : Number(arr[2]);
                    //当前月有多少天
                    let days = this.getMonthDay(year, month);
                    let result = `${year}-${this.formatNum(month)}-${this.formatNum(day)}`;
                    let weekText = this.getWeekText(result);
                    let isToday = false;
                    if (`${year}-${month}-${day}` == this.today) {
                        //今天
                        isToday = true;
                    }
                    this.$emit('change', {
                        year: year,
                        month: month,
                        day: day,
                        days: days,
                        result: result,
                        week: weekText,
                        isToday: isToday,
                        // switch: show //是否是切换年月操作
                    });
                } else {
                    if (!this.startDate || !this.endDate) return;
                    let startMonth = this.formatNum(this.startMonth);
                    let startDay = this.formatNum(this.startDay);
                    let startDate = `${this.startYear}-${startMonth}-${startDay}`;
                    let startWeek = this.getWeekText(startDate)
                    let endMonth = this.formatNum(this.endMonth);
                    let endDay = this.formatNum(this.endDay);
                    let endDate = `${this.endYear}-${endMonth}-${endDay}`;
                    let endWeek = this.getWeekText(endDate);
                    this.$emit('change', {
                        startYear: this.startYear,
                        startMonth: this.startMonth,
                        startDay: this.startDay,
                        startDate: startDate,
                        startWeek: startWeek,
                        endYear: this.endYear,
                        endMonth: this.endMonth,
                        endDay: this.endDay,
                        endDate: endDate,
                        endWeek: endWeek
                    });
                }
            }
        }
    };
</script>
<style scoped lang="scss">
    @import "../../libs/css/style.components.scss";
    .u-calendar {
        color: $u-content-color;
        &__header {
            width: 100%;
            box-sizing: border-box;
            font-size: 30rpx;
            background-color: #fff;
            color: $u-main-color;
            &__text {
                margin-top: 30rpx;
                padding: 0 60rpx;
                @include vue-flex;
                justify-content: center;
                align-items: center;
            }
        }
        &__action {
            padding: 40rpx 0 40rpx 0;
            &__icon {
                margin: 0 16rpx;
            }
            &__text {
                padding: 0 16rpx;
                color: $u-main-color;
                font-size: 32rpx;
                line-height: 32rpx;
                font-weight: bold;
            }
        }
        &__week-day {
            @include vue-flex;
            align-items: center;
            justify-content: center;
            padding: 6px 0;
            overflow: hidden;
            &__text {
                flex: 1;
                text-align: center;
            }
        }
        &__content {
            width: 100%;
            @include vue-flex;
            flex-wrap: wrap;
            padding: 6px 0;
            box-sizing: border-box;
            background-color: #fff;
            position: relative;
            &--end-date {
                border-top-right-radius: 8rpx;
                border-bottom-right-radius: 8rpx;
            }
            &--start-date {
                border-top-left-radius: 8rpx;
                border-bottom-left-radius: 8rpx;
            }
            &__item {
                width: 14.2857%;
                @include vue-flex;
                align-items: center;
                justify-content: center;
                padding: 6px 0;
                overflow: hidden;
                position: relative;
                z-index: 2;
                &__inner {
                    height: 84rpx;
                    @include vue-flex;
                    align-items: center;
                    justify-content: center;
                    flex-direction: column;
                    font-size: 32rpx;
                    position: relative;
                    border-radius: 50%;
                    &__desc {
                        width: 100%;
                        font-size: 24rpx;
                        line-height: 24rpx;
                        transform: scale(0.75);
                        transform-origin: center center;
                        position: absolute;
                        left: 0;
                        text-align: center;
                        bottom: 2rpx;
                    }
                }
                &__tips {
                    width: 100%;
                    font-size: 24rpx;
                    line-height: 24rpx;
                    position: absolute;
                    left: 0;
                    transform: scale(0.8);
                    transform-origin: center center;
                    text-align: center;
                    bottom: 8rpx;
                    z-index: 2;
                }
            }
            &__bg-month {
                position: absolute;
                font-size: 130px;
                line-height: 130px;
                left: 50%;
                top: 50%;
                transform: translate(-50%, -50%);
                color: #e4e7ed;
                z-index: 1;
            }
        }
        &__bottom {
            width: 100%;
            @include vue-flex;
            align-items: center;
            justify-content: center;
            flex-direction: column;
            background-color: #fff;
            padding: 0 40rpx 30rpx;
            box-sizing: border-box;
            font-size: 24rpx;
            color: $u-tips-color;
            &__choose {
                height: 50rpx;
            }
            &__btn {
                width: 100%;
            }
        }
    }
</style>
uview-ui/components/u-car-keyboard/u-car-keyboard.vue
New file
@@ -0,0 +1,257 @@
<template>
    <view class="u-keyboard" @touchmove.stop.prevent="() => {}">
        <view class="u-keyboard-grids">
            <block>
                <view class="u-keyboard-grids-item" v-for="(group, i) in abc ? EngKeyBoardList : areaList" :key="i">
                    <view :hover-stay-time="100" @tap="carInputClick(i, j)" hover-class="u-carinput-hover" class="u-keyboard-grids-btn"
                     v-for="(item, j) in group" :key="j">
                        {{ item }}
                    </view>
                </view>
                <view @touchstart="backspaceClick" @touchend="clearTimer" :hover-stay-time="100" class="u-keyboard-back"
                 hover-class="u-hover-class">
                    <u-icon :size="38" name="backspace" :bold="true"></u-icon>
                </view>
                <view :hover-stay-time="100" class="u-keyboard-change" hover-class="u-carinput-hover" @tap="changeCarInputMode">
                    <text class="zh" :class="[!abc ? 'active' : 'inactive']">中</text>
                    /
                    <text class="en" :class="[abc ? 'active' : 'inactive']">英</text>
                </view>
            </block>
        </view>
    </view>
</template>
<script>
    export default {
        name: "u-keyboard",
        props: {
            // 是否打乱键盘按键的顺序
            random: {
                type: Boolean,
                default: false
            }
        },
        data() {
            return {
                // 车牌输入时,abc=true为输入车牌号码,bac=false为输入省份中文简称
                abc: false
            };
        },
        computed: {
            areaList() {
                let data = [
                    '京',
                    '沪',
                    '粤',
                    '津',
                    '冀',
                    '豫',
                    '云',
                    '辽',
                    '黑',
                    '湘',
                    '皖',
                    '鲁',
                    '苏',
                    '浙',
                    '赣',
                    '鄂',
                    '桂',
                    '甘',
                    '晋',
                    '陕',
                    '蒙',
                    '吉',
                    '闽',
                    '贵',
                    '渝',
                    '川',
                    '青',
                    '琼',
                    '宁',
                    '挂',
                    '藏',
                    '港',
                    '澳',
                    '新',
                    '使',
                    '学'
                ];
                let tmp = [];
                // 打乱顺序
                if (this.random) data = this.$u.randomArray(data);
                // 切割成二维数组
                tmp[0] = data.slice(0, 10);
                tmp[1] = data.slice(10, 20);
                tmp[2] = data.slice(20, 30);
                tmp[3] = data.slice(30, 36);
                return tmp;
            },
            EngKeyBoardList() {
                let data = [
                    1,
                    2,
                    3,
                    4,
                    5,
                    6,
                    7,
                    8,
                    9,
                    0,
                    'Q',
                    'W',
                    'E',
                    'R',
                    'T',
                    'Y',
                    'U',
                    'I',
                    'O',
                    'P',
                    'A',
                    'S',
                    'D',
                    'F',
                    'G',
                    'H',
                    'J',
                    'K',
                    'L',
                    'Z',
                    'X',
                    'C',
                    'V',
                    'B',
                    'N',
                    'M'
                ];
                let tmp = [];
                if (this.random) data = this.$u.randomArray(data);
                tmp[0] = data.slice(0, 10);
                tmp[1] = data.slice(10, 20);
                tmp[2] = data.slice(20, 30);
                tmp[3] = data.slice(30, 36);
                return tmp;
            }
        },
        methods: {
            // 点击键盘按钮
            carInputClick(i, j) {
                let value = '';
                // 不同模式,获取不同数组的值
                if (this.abc) value = this.EngKeyBoardList[i][j];
                else value = this.areaList[i][j];
                this.$emit('change', value);
            },
            // 修改汽车牌键盘的输入模式,中文|英文
            changeCarInputMode() {
                this.abc = !this.abc;
            },
            // 点击退格键
            backspaceClick() {
                this.$emit('backspace');
                clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
                this.timer = null;
                this.timer = setInterval(() => {
                    this.$emit('backspace');
                }, 250);
            },
            clearTimer() {
                clearInterval(this.timer);
                this.timer = null;
            },
        }
    };
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-keyboard-grids {
        background: rgb(215, 215, 217);
        padding: 24rpx 0;
        position: relative;
    }
    .u-keyboard-grids-item {
        @include vue-flex;
        align-items: center;
        justify-content: center;
    }
    .u-keyboard-grids-btn {
        text-decoration: none;
        width: 62rpx;
        flex: 0 0 64rpx;
        height: 80rpx;
        /* #ifndef APP-NVUE */
        display: inline-flex;
        /* #endif */
        font-size: 36rpx;
        text-align: center;
        line-height: 80rpx;
        background-color: #fff;
        margin: 8rpx 5rpx;
        border-radius: 8rpx;
        box-shadow: 0 2rpx 0rpx #888992;
        font-weight: 500;
        justify-content: center;
    }
    .u-carinput-hover {
        background-color: rgb(185, 188, 195) !important;
    }
    .u-keyboard-back {
        position: absolute;
        width: 96rpx;
        right: 22rpx;
        bottom: 32rpx;
        height: 80rpx;
        background-color: rgb(185, 188, 195);
        @include vue-flex;
        align-items: center;
        border-radius: 8rpx;
        justify-content: center;
        box-shadow: 0 2rpx 0rpx #888992;
    }
    .u-keyboard-change {
        font-size: 24rpx;
        box-shadow: 0 2rpx 0rpx #888992;
        position: absolute;
        width: 96rpx;
        left: 22rpx;
        line-height: 1;
        bottom: 32rpx;
        height: 80rpx;
        background-color: #ffffff;
        @include vue-flex;
        align-items: center;
        border-radius: 8rpx;
        justify-content: center;
    }
    .u-keyboard-change .inactive.zh {
        transform: scale(0.85) translateY(-10rpx);
    }
    .u-keyboard-change .inactive.en {
        transform: scale(0.85) translateY(10rpx);
    }
    .u-keyboard-change .active {
        color: rgb(237, 112, 64);
        font-size: 30rpx;
    }
    .u-keyboard-change .zh {
        transform: translateY(-10rpx);
    }
    .u-keyboard-change .en {
        transform: translateY(10rpx);
    }
</style>
uview-ui/components/u-card/u-card.vue
New file
@@ -0,0 +1,299 @@
<template>
    <view
        class="u-card"
        @tap.stop="click"
        :class="{ 'u-border': border, 'u-card-full': full, 'u-card--border': borderRadius > 0 }"
        :style="{
            borderRadius: borderRadius + 'rpx',
            margin: margin,
            boxShadow: boxShadow
        }"
    >
        <view
            v-if="showHead"
            class="u-card__head"
            :style="[{padding: padding + 'rpx'}, headStyle]"
            :class="{
                'u-border-bottom': headBorderBottom
            }"
            @tap="headClick"
        >
            <view v-if="!$slots.head" class="u-flex u-row-between">
                <view class="u-card__head--left u-flex u-line-1" v-if="title">
                    <image
                        :src="thumb"
                        class="u-card__head--left__thumb"
                        mode="aspectfull"
                        v-if="thumb"
                        :style="{
                            height: thumbWidth + 'rpx',
                            width: thumbWidth + 'rpx',
                            borderRadius: thumbCircle ? '100rpx' : '6rpx'
                        }"
                    ></image>
                    <text
                        class="u-card__head--left__title u-line-1"
                        :style="{
                            fontSize: titleSize + 'rpx',
                            color: titleColor
                        }"
                    >
                        {{ title }}
                    </text>
                </view>
                <view class="u-card__head--right u-line-1" v-if="subTitle">
                    <text
                        class="u-card__head__title__text"
                        :style="{
                            fontSize: subTitleSize + 'rpx',
                            color: subTitleColor
                        }"
                    >
                        {{ subTitle }}
                    </text>
                </view>
            </view>
            <slot name="head" v-else />
        </view>
        <view @tap="bodyClick" class="u-card__body" :style="[{padding: padding + 'rpx'}, bodyStyle]"><slot name="body" /></view>
        <view
            v-if="showFoot"
            class="u-card__foot"
             @tap="footClick"
            :style="[{padding: $slots.foot ? padding + 'rpx' : 0}, footStyle]"
            :class="{
                'u-border-top': footBorderTop
            }"
        >
            <slot name="foot" />
        </view>
    </view>
</template>
<script>
/**
 * card 卡片
 * @description 卡片组件一般用于多个列表条目,且风格统一的场景
 * @tutorial https://www.uviewui.com/components/card.html
 * @property {Boolean} full 卡片与屏幕两侧是否留空隙(默认false)
 * @property {String} title 头部左边的标题
 * @property {String} title-color 标题颜色(默认#303133)
 * @property {String | Number} title-size 标题字体大小,单位rpx(默认30)
 * @property {String} sub-title 头部右边的副标题
 * @property {String} sub-title-color 副标题颜色(默认#909399)
 * @property {String | Number} sub-title-size 副标题字体大小(默认26)
 * @property {Boolean} border 是否显示边框(默认true)
 * @property {String | Number} index 用于标识点击了第几个卡片
 * @property {String} box-shadow 卡片外围阴影,字符串形式(默认none)
 * @property {String} margin 卡片与屏幕两边和上下元素的间距,需带单位,如"30rpx 20rpx"(默认30rpx)
 * @property {String | Number} border-radius 卡片整体的圆角值,单位rpx(默认16)
 * @property {Object} head-style 头部自定义样式,对象形式
 * @property {Object} body-style 中部自定义样式,对象形式
 * @property {Object} foot-style 底部自定义样式,对象形式
 * @property {Boolean} head-border-bottom 是否显示头部的下边框(默认true)
 * @property {Boolean} foot-border-top 是否显示底部的上边框(默认true)
 * @property {Boolean} show-head 是否显示头部(默认true)
 * @property {Boolean} show-head 是否显示尾部(默认true)
 * @property {String} thumb 缩略图路径,如设置将显示在标题的左边,不建议使用相对路径
 * @property {String | Number} thumb-width 缩略图的宽度,高等于宽,单位rpx(默认60)
 * @property {Boolean} thumb-circle 缩略图是否为圆形(默认false)
 * @event {Function} click 整个卡片任意位置被点击时触发
 * @event {Function} head-click 卡片头部被点击时触发
 * @event {Function} body-click 卡片主体部分被点击时触发
 * @event {Function} foot-click 卡片底部部分被点击时触发
 * @example <u-card padding="30" title="card"></u-card>
 */
export default {
    name: 'u-card',
    props: {
        // 与屏幕两侧是否留空隙
        full: {
            type: Boolean,
            default: false
        },
        // 标题
        title: {
            type: String,
            default: ''
        },
        // 标题颜色
        titleColor: {
            type: String,
            default: '#303133'
        },
        // 标题字体大小,单位rpx
        titleSize: {
            type: [Number, String],
            default: '30'
        },
        // 副标题
        subTitle: {
            type: String,
            default: ''
        },
        // 副标题颜色
        subTitleColor: {
            type: String,
            default: '#909399'
        },
        // 副标题字体大小,单位rpx
        subTitleSize: {
            type: [Number, String],
            default: '26'
        },
        // 是否显示外部边框,只对full=false时有效(卡片与边框有空隙时)
        border: {
            type: Boolean,
            default: true
        },
        // 用于标识点击了第几个
        index: {
            type: [Number, String, Object],
            default: ''
        },
        // 用于隔开上下左右的边距,带单位的写法,如:"30rpx 30rpx","20rpx 20rpx 30rpx 30rpx"
        margin: {
            type: String,
            default: '30rpx'
        },
        // card卡片的圆角
        borderRadius: {
            type: [Number, String],
            default: '16'
        },
        // 头部自定义样式,对象形式
        headStyle: {
            type: Object,
            default() {
                return {};
            }
        },
        // 主体自定义样式,对象形式
        bodyStyle: {
            type: Object,
            default() {
                return {};
            }
        },
        // 底部自定义样式,对象形式
        footStyle: {
            type: Object,
            default() {
                return {};
            }
        },
        // 头部是否下边框
        headBorderBottom: {
            type: Boolean,
            default: true
        },
        // 底部是否有上边框
        footBorderTop: {
            type: Boolean,
            default: true
        },
        // 标题左边的缩略图
        thumb: {
            type: String,
            default: ''
        },
        // 缩略图宽高,单位rpx
        thumbWidth: {
            type: [String, Number],
            default: '60'
        },
        // 缩略图是否为圆形
        thumbCircle: {
            type: Boolean,
            default: false
        },
        // 给head,body,foot的内边距
        padding: {
            type: [String, Number],
            default: '30'
        },
        // 是否显示头部
        showHead: {
            type: Boolean,
            default: true
        },
        // 是否显示尾部
        showFoot: {
            type: Boolean,
            default: true
        },
        // 卡片外围阴影,字符串形式
        boxShadow: {
            type: String,
            default: 'none'
        }
    },
    data() {
        return {};
    },
    methods: {
        click() {
            this.$emit('click', this.index);
        },
        headClick() {
            this.$emit('head-click', this.index);
        },
        bodyClick() {
            this.$emit('body-click', this.index);
        },
        footClick() {
            this.$emit('foot-click', this.index);
        }
    }
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-card {
    position: relative;
    overflow: hidden;
    font-size: 28rpx;
    background-color: #ffffff;
    box-sizing: border-box;
    &-full {
        // 如果是与屏幕之间不留空隙,应该设置左右边距为0
        margin-left: 0 !important;
        margin-right: 0 !important;
        width: 100%;
    }
    &--border:after {
        border-radius: 16rpx;
    }
    &__head {
        &--left {
            color: $u-main-color;
            &__thumb {
                margin-right: 16rpx;
            }
            &__title {
                max-width: 400rpx;
            }
        }
        &--right {
            color: $u-tips-color;
            margin-left: 6rpx;
        }
    }
    &__body {
        color: $u-content-color;
    }
    &__foot {
        color: $u-tips-color;
    }
}
</style>
uview-ui/components/u-cell-group/u-cell-group.vue
New file
@@ -0,0 +1,70 @@
<template>
    <view class="u-cell-box">
        <view class="u-cell-title" v-if="title" :style="[titleStyle]">
            {{title}}
        </view>
        <view class="u-cell-item-box" :class="{'u-border-bottom u-border-top': border}">
            <slot />
        </view>
    </view>
</template>
<script>
    /**
     * cellGroup 单元格父组件Group
     * @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-item
     * @tutorial https://www.uviewui.com/components/cell.html
     * @property {String} title 分组标题
     * @property {Boolean} border 是否显示外边框(默认true)
     * @property {Object} title-style 分组标题的的样式,对象形式,如{'font-size': '24rpx'} 或 {'fontSize': '24rpx'}
     * @example <u-cell-group title="设置喜好">
     */
    export default {
        name: "u-cell-group",
        props: {
            // 分组标题
            title: {
                type: String,
                default: ''
            },
            // 是否显示分组list上下边框
            border: {
                type: Boolean,
                default: true
            },
            // 分组标题的样式,对象形式,注意驼峰属性写法
            // 类似 {'font-size': '24rpx'} 和 {'fontSize': '24rpx'}
            titleStyle: {
                type: Object,
                default () {
                    return {};
                }
            }
        },
        data() {
            return {
                index: 0,
            }
        },
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-cell-box {
        width: 100%;
    }
    .u-cell-title {
        padding: 30rpx 32rpx 10rpx 32rpx;
        font-size: 30rpx;
        text-align: left;
        color: $u-tips-color;
    }
    .u-cell-item-box {
        background-color: #FFFFFF;
        flex-direction: row;
    }
</style>
uview-ui/components/u-cell-item/u-cell-item.vue
New file
@@ -0,0 +1,316 @@
<template>
    <view
        @tap="click"
        class="u-cell"
        :class="{ 'u-border-bottom': borderBottom, 'u-border-top': borderTop, 'u-col-center': center, 'u-cell--required': required }"
        hover-stay-time="150"
        :hover-class="hoverClass"
        :style="{
            backgroundColor: bgColor
        }"
    >
        <u-icon :size="iconSize" :name="icon" v-if="icon" :custom-style="iconStyle" class="u-cell__left-icon-wrap"></u-icon>
        <view class="u-flex" v-else>
            <slot name="icon"></slot>
        </view>
        <view
            class="u-cell_title"
            :style="[
                {
                    width: titleWidth ? titleWidth + 'rpx' : 'auto'
                },
                titleStyle
            ]"
        >
            <block v-if="title !== ''">{{ title }}</block>
            <slot name="title" v-else></slot>
            <view class="u-cell__label" v-if="label || $slots.label" :style="[labelStyle]">
                <block v-if="label !== ''">{{ label }}</block>
                <slot name="label" v-else></slot>
            </view>
        </view>
        <view class="u-cell__value" :style="[valueStyle]">
            <block class="u-cell__value" v-if="value !== ''">{{ value }}</block>
            <slot v-else></slot>
        </view>
        <view class="u-flex u-cell_right" v-if="$slots['right-icon']">
            <slot name="right-icon"></slot>
        </view>
        <u-icon v-if="arrow" name="arrow-right" :style="[arrowStyle]" class="u-icon-wrap u-cell__right-icon-wrap"></u-icon>
    </view>
</template>
<script>
/**
 * cellItem 单元格Item
 * @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-group使用
 * @tutorial https://www.uviewui.com/components/cell.html
 * @property {String} title 左侧标题
 * @property {String} icon 左侧图标名,只支持uView内置图标,见Icon 图标
 * @property {Object} icon-style 左边图标的样式,对象形式
 * @property {String} value 右侧内容
 * @property {String} label 标题下方的描述信息
 * @property {Boolean} border-bottom 是否显示cell的下边框(默认true)
 * @property {Boolean} border-top 是否显示cell的上边框(默认false)
 * @property {Boolean} center 是否使内容垂直居中(默认false)
 * @property {String} hover-class 是否开启点击反馈,none为无效果(默认true)
 * // @property {Boolean} border-gap border-bottom为true时,Cell列表中间的条目的下边框是否与左边有一个间隔(默认true)
 * @property {Boolean} arrow 是否显示右侧箭头(默认true)
 * @property {Boolean} required 箭头方向,可选值(默认right)
 * @property {Boolean} arrow-direction 是否显示左边表示必填的星号(默认false)
 * @property {Object} title-style 标题样式,对象形式
 * @property {Object} value-style 右侧内容样式,对象形式
 * @property {Object} label-style 标题下方描述信息的样式,对象形式
 * @property {String} bg-color 背景颜色(默认transparent)
 * @property {String Number} index 用于在click事件回调中返回,标识当前是第几个Item
 * @property {String Number} title-width 标题的宽度,单位rpx
 * @example <u-cell-item icon="integral-fill" title="会员等级" value="新版本"></u-cell-item>
 */
export default {
    name: 'u-cell-item',
    props: {
        // 左侧图标名称(只能uView内置图标),或者图标src
        icon: {
            type: String,
            default: ''
        },
        // 左侧标题
        title: {
            type: [String, Number],
            default: ''
        },
        // 右侧内容
        value: {
            type: [String, Number],
            default: ''
        },
        // 标题下方的描述信息
        label: {
            type: [String, Number],
            default: ''
        },
        // 是否显示下边框
        borderBottom: {
            type: Boolean,
            default: true
        },
        // 是否显示上边框
        borderTop: {
            type: Boolean,
            default: false
        },
        // 多个cell中,中间的cell显示下划线时,下划线是否给一个到左边的距离
        // 1.4.0版本废除此参数,默认边框由border-top和border-bottom提供,此参数会造成干扰
        // borderGap: {
        //     type: Boolean,
        //     default: true
        // },
        // 是否开启点击反馈,即点击时cell背景为灰色,none为无效果
        hoverClass: {
            type: String,
            default: 'u-cell-hover'
        },
        // 是否显示右侧箭头
        arrow: {
            type: Boolean,
            default: true
        },
        // 内容是否垂直居中
        center: {
            type: Boolean,
            default: false
        },
        // 是否显示左边表示必填的星号
        required: {
            type: Boolean,
            default: false
        },
        // 标题的宽度,单位rpx
        titleWidth: {
            type: [Number, String],
            default: ''
        },
        // 右侧箭头方向,可选值:right|up|down,默认为right
        arrowDirection: {
            type: String,
            default: 'right'
        },
        // 控制标题的样式
        titleStyle: {
            type: Object,
            default() {
                return {};
            }
        },
        // 右侧显示内容的样式
        valueStyle: {
            type: Object,
            default() {
                return {};
            }
        },
        // 描述信息的样式
        labelStyle: {
            type: Object,
            default() {
                return {};
            }
        },
        // 背景颜色
        bgColor: {
            type: String,
            default: 'transparent'
        },
        // 用于识别被点击的是第几个cell
        index: {
            type: [String, Number],
            default: ''
        },
        // 是否使用lable插槽
        useLabelSlot: {
            type: Boolean,
            default: false
        },
        // 左边图标的大小,单位rpx,只对传入icon字段时有效
        iconSize: {
            type: [Number, String],
            default: 34
        },
        // 左边图标的样式,对象形式
        iconStyle: {
            type: Object,
            default() {
                return {}
            }
        },
    },
    data() {
        return {
        };
    },
    computed: {
        arrowStyle() {
            let style = {};
            if (this.arrowDirection == 'up') style.transform = 'rotate(-90deg)';
            else if (this.arrowDirection == 'down') style.transform = 'rotate(90deg)';
            else style.transform = 'rotate(0deg)';
            return style;
        }
    },
    methods: {
        click() {
            this.$emit('click', this.index);
        }
    }
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-cell {
    @include vue-flex;
    align-items: center;
    position: relative;
    /* #ifndef APP-NVUE */
    box-sizing: border-box;
    /* #endif */
    width: 100%;
    padding: 26rpx 32rpx;
    font-size: 28rpx;
    line-height: 54rpx;
    color: $u-content-color;
    background-color: #fff;
    text-align: left;
}
.u-cell_title {
    font-size: 28rpx;
}
.u-cell__left-icon-wrap {
    margin-right: 10rpx;
    font-size: 32rpx;
}
.u-cell__right-icon-wrap {
    margin-left: 10rpx;
    color: #969799;
    font-size: 28rpx;
}
.u-cell__left-icon-wrap,
.u-cell__right-icon-wrap {
    @include vue-flex;
    align-items: center;
    height: 48rpx;
}
.u-cell-border:after {
    position: absolute;
    /* #ifndef APP-NVUE */
    box-sizing: border-box;
    content: ' ';
    pointer-events: none;
    border-bottom: 1px solid $u-border-color;
    /* #endif */
    right: 0;
    left: 0;
    top: 0;
    transform: scaleY(0.5);
}
.u-cell-border {
    position: relative;
}
.u-cell__label {
    margin-top: 6rpx;
    font-size: 26rpx;
    line-height: 36rpx;
    color: $u-tips-color;
    /* #ifndef APP-NVUE */
    word-wrap: break-word;
    /* #endif */
}
.u-cell__value {
    overflow: hidden;
    text-align: right;
    /* #ifndef APP-NVUE */
    vertical-align: middle;
    /* #endif */
    color: $u-tips-color;
    font-size: 26rpx;
}
.u-cell__title,
.u-cell__value {
    flex: 1;
}
.u-cell--required {
    /* #ifndef APP-NVUE */
    overflow: visible;
    /* #endif */
    @include vue-flex;
    align-items: center;
}
.u-cell--required:before {
    position: absolute;
    /* #ifndef APP-NVUE */
    content: '*';
    /* #endif */
    left: 8px;
    margin-top: 4rpx;
    font-size: 14px;
    color: $u-type-error;
}
.u-cell_right {
    line-height: 1;
}
</style>
uview-ui/components/u-checkbox-group/u-checkbox-group.vue
New file
@@ -0,0 +1,123 @@
<template>
    <view class="u-checkbox-group u-clearfix">
        <slot></slot>
    </view>
</template>
<script>
    import Emitter from '../../libs/util/emitter.js';
    /**
     * checkboxGroup 开关选择器父组件Group
     * @description 复选框组件一般用于需要多个选择的场景,该组件功能完整,使用方便
     * @tutorial https://www.uviewui.com/components/checkbox.html
     * @property {String Number} max 最多能选中多少个checkbox(默认999)
     * @property {String Number} size 组件整体的大小,单位rpx(默认40)
     * @property {Boolean} disabled 是否禁用所有checkbox(默认false)
     * @property {String Number} icon-size 图标大小,单位rpx(默认20)
     * @property {Boolean} label-disabled 是否禁止点击文本操作checkbox(默认false)
     * @property {String} width 宽度,需带单位
     * @property {String} width 宽度,需带单位
     * @property {String} shape 外观形状,shape-方形,circle-圆形(默认circle)
     * @property {Boolean} wrap 是否每个checkbox都换行(默认false)
     * @property {String} active-color 选中时的颜色,应用到所有子Checkbox组件(默认#2979ff)
     * @event {Function} change 任一个checkbox状态发生变化时触发,回调为一个对象
     * @example <u-checkbox-group></u-checkbox-group>
     */
    export default {
        name: 'u-checkbox-group',
        mixins: [Emitter],
        props: {
            // 最多能选中多少个checkbox
            max: {
                type: [Number, String],
                default: 999
            },
            // 所有选中项的 name
            // value: {
            //     default: Array,
            //     default() {
            //         return []
            //     }
            // },
            // 是否禁用所有复选框
            disabled: {
                type: Boolean,
                default: false
            },
            // 在表单内提交时的标识符
            name: {
                type: [Boolean, String],
                default: ''
            },
            // 是否禁止点击提示语选中复选框
            labelDisabled: {
                type: Boolean,
                default: false
            },
            // 形状,square为方形,circle为原型
            shape: {
                type: String,
                default: 'square'
            },
            // 选中状态下的颜色
            activeColor: {
                type: String,
                default: '#2979ff'
            },
            // 组件的整体大小
            size: {
                type: [String, Number],
                default: 34
            },
            // 每个checkbox占u-checkbox-group的宽度
            width: {
                type: String,
                default: 'auto'
            },
            // 是否每个checkbox都换行
            wrap: {
                type: Boolean,
                default: false
            },
            // 图标的大小,单位rpx
            iconSize: {
                type: [String, Number],
                default: 20
            },
        },
        data() {
            return {
            }
        },
        created() {
            // 如果将children定义在data中,在微信小程序会造成循环引用而报错
            this.children = [];
        },
        methods: {
            emitEvent() {
                let values = [];
                this.children.map(val => {
                    if(val.value) values.push(val.name);
                })
                this.$emit('change', values);
                // 发出事件,用于在表单组件中嵌入checkbox的情况,进行验证
                // 由于头条小程序执行迟钝,故需要用几十毫秒的延时
                setTimeout(() => {
                    // 将当前的值发送到 u-form-item 进行校验
                    this.dispatch('u-form-item', 'on-form-change', values);
                }, 60)
            }
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-checkbox-group {
        /* #ifndef MP || APP-NVUE */
        display: inline-flex;
        flex-wrap: wrap;
        /* #endif */
    }
</style>
uview-ui/components/u-checkbox/u-checkbox.vue
New file
@@ -0,0 +1,284 @@
<template>
    <view class="u-checkbox" :style="[checkboxStyle]">
        <view class="u-checkbox__icon-wrap" @tap="toggle" :class="[iconClass]" :style="[iconStyle]">
            <u-icon class="u-checkbox__icon-wrap__icon" name="checkbox-mark" :size="checkboxIconSize" :color="iconColor"/>
        </view>
        <view class="u-checkbox__label" @tap="onClickLabel" :style="{
            fontSize: $u.addUnit(labelSize)
        }">
            <slot />
        </view>
    </view>
</template>
<script>
    /**
     * checkbox 复选框
     * @description 该组件需要搭配checkboxGroup组件使用,以便用户进行操作时,获得当前复选框组的选中情况。
     * @tutorial https://www.uviewui.com/components/checkbox.html
     * @property {String Number} icon-size 图标大小,单位rpx(默认20)
     * @property {String Number} label-size label字体大小,单位rpx(默认28)
     * @property {String Number} name checkbox组件的标示符
     * @property {String} shape 形状,见官网说明(默认circle)
     * @property {Boolean} disabled 是否禁用
     * @property {Boolean} label-disabled 是否禁止点击文本操作checkbox
     * @property {String} active-color 选中时的颜色,如设置CheckboxGroup的active-color将失效
     * @event {Function} change 某个checkbox状态发生变化时触发,回调为一个对象
     * @example <u-checkbox v-model="checked" :disabled="false">天涯</u-checkbox>
     */
    export default {
        name: "u-checkbox",
        props: {
            // checkbox的名称
            name: {
                type: [String, Number],
                default: ''
            },
            // 形状,square为方形,circle为原型
            shape: {
                type: String,
                default: ''
            },
            // 是否为选中状态
            value: {
                type: Boolean,
                default: false
            },
            // 是否禁用
            disabled: {
                type: [String, Boolean],
                default: ''
            },
            // 是否禁止点击提示语选中复选框
            labelDisabled: {
                type: [String, Boolean],
                default: ''
            },
            // 选中状态下的颜色,如设置此值,将会覆盖checkboxGroup的activeColor值
            activeColor: {
                type: String,
                default: ''
            },
            // 图标的大小,单位rpx
            iconSize: {
                type: [String, Number],
                default: ''
            },
            // label的字体大小,rpx单位
            labelSize: {
                type: [String, Number],
                default: ''
            },
            // 组件的整体大小
            size: {
                type: [String, Number],
                default: ''
            },
        },
        data() {
            return {
                parentDisabled: false,
                newParams: {},
            };
        },
        created() {
            // 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
            this.parent = this.$u.$parent.call(this, 'u-checkbox-group');
            // 如果存在u-checkbox-group,将本组件的this塞进父组件的children中
            this.parent && this.parent.children.push(this);
        },
        computed: {
            // 是否禁用,如果父组件u-checkbox-group禁用的话,将会忽略子组件的配置
            isDisabled() {
                return this.disabled !== '' ? this.disabled : this.parent ? this.parent.disabled : false;
            },
            // 是否禁用label点击
            isLabelDisabled() {
                return this.labelDisabled !== '' ? this.labelDisabled : this.parent ? this.parent.labelDisabled : false;
            },
            // 组件尺寸,对应size的值,默认值为34rpx
            checkboxSize() {
                return this.size ? this.size : (this.parent ? this.parent.size : 34);
            },
            // 组件的勾选图标的尺寸,默认20
            checkboxIconSize() {
                return this.iconSize ? this.iconSize : (this.parent ? this.parent.iconSize : 20);
            },
            // 组件选中激活时的颜色
            elActiveColor() {
                return this.activeColor ? this.activeColor : (this.parent ? this.parent.activeColor : 'primary');
            },
            // 组件的形状
            elShape() {
                return this.shape ? this.shape : (this.parent ? this.parent.shape : 'square');
            },
            iconStyle() {
                let style = {};
                // 既要判断是否手动禁用,还要判断用户v-model绑定的值,如果绑定为false,那么也无法选中
                if (this.elActiveColor && this.value && !this.isDisabled) {
                    style.borderColor = this.elActiveColor;
                    style.backgroundColor = this.elActiveColor;
                }
                style.width = this.$u.addUnit(this.checkboxSize);
                style.height = this.$u.addUnit(this.checkboxSize);
                return style;
            },
            // checkbox内部的勾选图标,如果选中状态,为白色,否则为透明色即可
            iconColor() {
                return this.value ? '#ffffff' : 'transparent';
            },
            iconClass() {
                let classes = [];
                classes.push('u-checkbox__icon-wrap--' + this.elShape);
                if (this.value == true) classes.push('u-checkbox__icon-wrap--checked');
                if (this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled');
                if (this.value && this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled--checked');
                // 支付宝小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
                return classes.join(' ');
            },
            checkboxStyle() {
                let style = {};
                if(this.parent && this.parent.width) {
                    style.width = this.parent.width;
                    // #ifdef MP
                    // 各家小程序因为它们特殊的编译结构,使用float布局
                    style.float = 'left';
                    // #endif
                    // #ifndef MP
                    // H5和APP使用flex布局
                    style.flex = `0 0 ${this.parent.width}`;
                    // #endif
                }
                if(this.parent && this.parent.wrap) {
                    style.width = '100%';
                    // #ifndef MP
                    // H5和APP使用flex布局,将宽度设置100%,即可自动换行
                    style.flex = '0 0 100%';
                    // #endif
                }
                return style;
            }
        },
        methods: {
            onClickLabel() {
                if (!this.isLabelDisabled && !this.isDisabled) {
                    this.setValue();
                }
            },
            toggle() {
                if (!this.isDisabled) {
                    this.setValue();
                }
            },
            emitEvent() {
                this.$emit('change', {
                    value: !this.value,
                    name: this.name
                })
                // 执行父组件u-checkbox-group的事件方法
                // 等待下一个周期再执行,因为this.$emit('input')作用于父组件,再反馈到子组件内部,需要时间
                setTimeout(() => {
                    if(this.parent && this.parent.emitEvent) this.parent.emitEvent();
                }, 80);
            },
            // 设置input的值,这里通过input事件,设置通过v-model绑定的组件的值
            setValue() {
                // 判断是否超过了可选的最大数量
                let checkedNum = 0;
                if(this.parent && this.parent.children) {
                    // 只要父组件的某一个子元素的value为true,就加1(已有的选中数量)
                    this.parent.children.map(val => {
                        if (val.value) checkedNum++;
                    })
                }
                // 如果原来为选中状态,那么可以取消
                if (this.value == true) {
                    this.emitEvent();
                    this.$emit('input', !this.value);
                } else {
                    // 如果超出最多可选项,提示
                    if(this.parent && checkedNum >= this.parent.max) {
                        return this.$u.toast(`最多可选${this.parent.max}项`);
                    }
                    // 如果原来为未选中状态,需要选中的数量少于父组件中设置的max值,才可以选中
                    this.emitEvent();
                    this.$emit('input', !this.value);
                }
            }
        }
    };
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-checkbox {
        /* #ifndef APP-NVUE */
        display: inline-flex;
        /* #endif */
        align-items: center;
        overflow: hidden;
        user-select: none;
        line-height: 1.8;
        &__icon-wrap {
            color: $u-content-color;
            flex: none;
            display: -webkit-flex;
            @include vue-flex;
            align-items: center;
            justify-content: center;
            box-sizing: border-box;
            width: 42rpx;
            height: 42rpx;
            color: transparent;
            text-align: center;
            transition-property: color, border-color, background-color;
            font-size: 20px;
            border: 1px solid #c8c9cc;
            transition-duration: 0.2s;
            /* #ifdef MP-TOUTIAO */
            // 头条小程序兼容性问题,需要设置行高为0,否则图标偏下
            &__icon {
                line-height: 0;
            }
            /* #endif */
            &--circle {
                border-radius: 100%;
            }
            &--square {
                border-radius: 6rpx;
            }
            &--checked {
                color: #fff;
                background-color: $u-type-primary;
                border-color: $u-type-primary;
            }
            &--disabled {
                background-color: #ebedf0;
                border-color: #c8c9cc;
            }
            &--disabled--checked {
                color: #c8c9cc !important;
            }
        }
        &__label {
            word-wrap: break-word;
            margin-left: 10rpx;
            margin-right: 24rpx;
            color: $u-content-color;
            font-size: 30rpx;
            &--disabled {
                color: #c8c9cc;
            }
        }
    }
</style>
uview-ui/components/u-circle-progress/u-circle-progress.vue
New file
@@ -0,0 +1,220 @@
<template>
    <view
        class="u-circle-progress"
        :style="{
            width: widthPx + 'px',
            height: widthPx + 'px',
            backgroundColor: bgColor
        }"
    >
        <!-- 支付宝小程序不支持canvas-id属性,必须用id属性 -->
        <canvas
            class="u-canvas-bg"
            :canvas-id="elBgId"
            :id="elBgId"
            :style="{
                width: widthPx + 'px',
                height: widthPx + 'px'
            }"
        ></canvas>
        <canvas
            class="u-canvas"
            :canvas-id="elId"
            :id="elId"
            :style="{
                width: widthPx + 'px',
                height: widthPx + 'px'
            }"
        ></canvas>
        <slot></slot>
    </view>
</template>
<script>
/**
 * circleProgress 环形进度条
 * @description 展示操作或任务的当前进度,比如上传文件,是一个圆形的进度条。注意:此组件的percent值只能动态增加,不能动态减少。
 * @tutorial https://www.uviewui.com/components/circleProgress.html
 * @property {String Number} percent 圆环进度百分比值,为数值类型,0-100
 * @property {String} inactive-color 圆环的底色,默认为灰色(该值无法动态变更)(默认#ececec)
 * @property {String} active-color 圆环激活部分的颜色(该值无法动态变更)(默认#19be6b)
 * @property {String Number} width 整个圆环组件的宽度,高度默认等于宽度值,单位rpx(默认200)
 * @property {String Number} border-width 圆环的边框宽度,单位rpx(默认14)
 * @property {String Number} duration 整个圆环执行一圈的时间,单位ms(默认呢1500)
 * @property {String} type 如设置,active-color值将会失效
 * @property {String} bg-color 整个组件背景颜色,默认为白色
 * @example <u-circle-progress active-color="#2979ff" :percent="80"></u-circle-progress>
 */
export default {
    name: 'u-circle-progress',
    props: {
        // 圆环进度百分比值
        percent: {
            type: Number,
            default: 0,
            // 限制值在0到100之间
            validator: val => {
                return val >= 0 && val <= 100;
            }
        },
        // 底部圆环的颜色(灰色的圆环)
        inactiveColor: {
            type: String,
            default: '#ececec'
        },
        // 圆环激活部分的颜色
        activeColor: {
            type: String,
            default: '#19be6b'
        },
        // 圆环线条的宽度,单位rpx
        borderWidth: {
            type: [Number, String],
            default: 14
        },
        // 整个圆形的宽度,单位rpx
        width: {
            type: [Number, String],
            default: 200
        },
        // 整个圆环执行一圈的时间,单位ms
        duration: {
            type: [Number, String],
            default: 1500
        },
        // 主题类型
        type: {
            type: String,
            default: ''
        },
        // 整个圆环进度区域的背景色
        bgColor: {
            type: String,
            default: '#ffffff'
        }
    },
    data() {
        return {
            // #ifdef MP-WEIXIN
            elBgId: 'uCircleProgressBgId', // 微信小程序中不能使用this.$u.guid()形式动态生成id值,否则会报错
            elId: 'uCircleProgressElId',
            // #endif
            // #ifndef MP-WEIXIN
            elBgId: this.$u.guid(), // 非微信端的时候,需用动态的id,否则一个页面多个圆形进度条组件数据会混乱
            elId: this.$u.guid(),
            // #endif
            widthPx: uni.upx2px(this.width), // 转成px后的整个组件的背景宽度
            borderWidthPx: uni.upx2px(this.borderWidth), // 转成px后的圆环的宽度
            startAngle: -Math.PI / 2, // canvas画圆的起始角度,默认为3点钟方向,定位到12点钟方向
            progressContext: null, // 活动圆的canvas上下文
            newPercent: 0, // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
            oldPercent: 0 // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
        };
    },
    watch: {
        percent(nVal, oVal = 0) {
            if (nVal > 100) nVal = 100;
            if (nVal < 0) oVal = 0;
            // 此值其实等于this.percent,命名一个新
            this.newPercent = nVal;
            this.oldPercent = oVal;
            setTimeout(() => {
                // 无论是百分比值增加还是减少,需要操作还是原来的旧的百分比值
                // 将此值减少或者新增到新的百分比值
                this.drawCircleByProgress(oVal);
            }, 50);
        }
    },
    created() {
        // 赋值,用于加载后第一个画圆使用
        this.newPercent = this.percent;
        this.oldPercent = 0;
    },
    computed: {
        // 有type主题时,优先起作用
        circleColor() {
            if (['success', 'error', 'info', 'primary', 'warning'].indexOf(this.type) >= 0) return this.$u.color[this.type];
            else return this.activeColor;
        }
    },
    mounted() {
        // 在h5端,必须要做一点延时才起作用,this.$nextTick()无效(HX2.4.7)
        setTimeout(() => {
            this.drawProgressBg();
            this.drawCircleByProgress(this.oldPercent);
        }, 50);
    },
    methods: {
        drawProgressBg() {
            let ctx = uni.createCanvasContext(this.elBgId, this);
            ctx.setLineWidth(this.borderWidthPx); // 设置圆环宽度
            ctx.setStrokeStyle(this.inactiveColor); // 线条颜色
            ctx.beginPath(); // 开始描绘路径
            // 设置一个原点(110,110),半径为100的圆的路径到当前路径
            let radius = this.widthPx / 2;
            ctx.arc(radius, radius, radius - this.borderWidthPx, 0, 2 * Math.PI, false);
            ctx.stroke(); // 对路径进行描绘
            ctx.draw();
        },
        drawCircleByProgress(progress) {
            // 第一次操作进度环时将上下文保存到了this.data中,直接使用即可
            let ctx = this.progressContext;
            if (!ctx) {
                ctx = uni.createCanvasContext(this.elId, this);
                this.progressContext = ctx;
            }
            // 表示进度的两端为圆形
            ctx.setLineCap('round');
            // 设置线条的宽度和颜色
            ctx.setLineWidth(this.borderWidthPx);
            ctx.setStrokeStyle(this.circleColor);
            // 将总过渡时间除以100,得出每修改百分之一进度所需的时间
            let time = Math.floor(this.duration / 100);
            // 结束角的计算依据为:将2π分为100份,乘以当前的进度值,得出终止点的弧度值,加起始角,为整个圆从默认的
            // 3点钟方向开始画图,转为更好理解的12点钟方向开始作图,这需要起始角和终止角同时加上this.startAngle值
            let endAngle = ((2 * Math.PI) / 100) * progress + this.startAngle;
            ctx.beginPath();
            // 半径为整个canvas宽度的一半
            let radius = this.widthPx / 2;
            ctx.arc(radius, radius, radius - this.borderWidthPx, this.startAngle, endAngle, false);
            ctx.stroke();
            ctx.draw();
            // 如果变更后新值大于旧值,意味着增大了百分比
            if (this.newPercent > this.oldPercent) {
                // 每次递增百分之一
                progress++;
                // 如果新增后的值,大于需要设置的值百分比值,停止继续增加
                if (progress > this.newPercent) return;
            } else {
                // 同理于上面
                progress--;
                if (progress < this.newPercent) return;
            }
            setTimeout(() => {
                // 定时器,每次操作间隔为time值,为了让进度条有动画效果
                this.drawCircleByProgress(progress);
            }, time);
        }
    }
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-circle-progress {
    position: relative;
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
    align-items: center;
    justify-content: center;
}
.u-canvas-bg {
    position: absolute;
}
.u-canvas {
    position: absolute;
}
</style>
uview-ui/components/u-circle-progress/u-line-progress/u-line-progress.vue
New file
@@ -0,0 +1,147 @@
<template>
    <view class="u-progress" :style="{
        borderRadius: round ? '100rpx' : 0,
        height: height + 'rpx',
        backgroundColor: inactiveColor
    }">
        <view :class="[
            type ? `u-type-${type}-bg` : '',
            striped ? 'u-striped' : '',
            striped && stripedActive ? 'u-striped-active' : ''
        ]" class="u-active" :style="[progressStyle]">
            <slot v-if="$slots.default || $slots.$default" />
            <block v-else-if="showPercent">
                {{percent + '%'}}
            </block>
        </view>
    </view>
</template>
<script>
    /**
     * lineProgress 线型进度条
     * @description 展示操作或任务的当前进度,比如上传文件,是一个线形的进度条。
     * @tutorial https://www.uviewui.com/components/lineProgress.html
     * @property {String Number} percent 进度条百分比值,为数值类型,0-100
     * @property {Boolean} round 进度条两端是否为半圆(默认true)
     * @property {String} type 如设置,active-color值将会失效
     * @property {String} active-color 进度条激活部分的颜色(默认#19be6b)
     * @property {String} inactive-color 进度条的底色(默认#ececec)
     * @property {Boolean} show-percent 是否在进度条内部显示当前的百分比值数值(默认true)
     * @property {String Number} height 进度条的高度,单位rpx(默认28)
     * @property {Boolean} striped 是否显示进度条激活部分的条纹(默认false)
     * @property {Boolean} striped-active 条纹是否具有动态效果(默认false)
     * @example <u-line-progress :percent="70" :show-percent="true"></u-line-progress>
     */
    export default {
        name: "u-line-progress",
        props: {
            // 两端是否显示半圆形
            round: {
                type: Boolean,
                default: true
            },
            // 主题颜色
            type: {
                type: String,
                default: ''
            },
            // 激活部分的颜色
            activeColor: {
                type: String,
                default: '#19be6b'
            },
            inactiveColor: {
                type: String,
                default: '#ececec'
            },
            // 进度百分比,数值
            percent: {
                type: Number,
                default: 0
            },
            // 是否在进度条内部显示百分比的值
            showPercent: {
                type: Boolean,
                default: true
            },
            // 进度条的高度,单位rpx
            height: {
                type: [Number, String],
                default: 28
            },
            // 是否显示条纹
            striped: {
                type: Boolean,
                default: false
            },
            // 条纹是否显示活动状态
            stripedActive: {
                type: Boolean,
                default: false
            }
        },
        data() {
            return {
            }
        },
        computed: {
            progressStyle() {
                let style = {};
                style.width = this.percent + '%';
                if(this.activeColor) style.backgroundColor = this.activeColor;
                return style;
            }
        },
        methods: {
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-progress {
        overflow: hidden;
        height: 15px;
        /* #ifndef APP-NVUE */
        display: inline-flex;
        /* #endif */
        align-items: center;
        width: 100%;
        border-radius: 100rpx;
    }
    .u-active {
        width: 0;
        height: 100%;
        align-items: center;
        @include vue-flex;
        justify-items: flex-end;
        justify-content: space-around;
        font-size: 20rpx;
        color: #ffffff;
        transition: all 0.4s ease;
    }
    .u-striped {
        background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
        background-size: 39px 39px;
    }
    .u-striped-active {
        animation: progress-stripes 2s linear infinite;
    }
    @keyframes progress-stripes {
        0% {
            background-position: 0 0;
        }
        100% {
            background-position: 39px 0;
        }
    }
</style>
uview-ui/components/u-col/u-col.vue
New file
@@ -0,0 +1,156 @@
<template>
    <view class="u-col" :class="[
        'u-col-' + span
    ]" :style="{
        padding: `0 ${Number(gutter)/2 + 'rpx'}`,
        marginLeft: 100 / 12 * offset + '%',
        flex: `0 0 ${100 / 12 * span}%`,
        alignItems: uAlignItem,
        justifyContent: uJustify,
        textAlign: textAlign
    }"
     @tap="click">
        <slot></slot>
    </view>
</template>
<script>
    /**
     * col 布局单元格
     * @description 通过基础的 12 分栏,迅速简便地创建布局(搭配<u-row>使用)
     * @tutorial https://www.uviewui.com/components/layout.html
     * @property {String Number} span 栅格占据的列数,总12等分(默认0)
     * @property {String} text-align 文字水平对齐方式(默认left)
     * @property {String Number} offset 分栏左边偏移,计算方式与span相同(默认0)
     * @example <u-col span="3"><view class="demo-layout bg-purple"></view></u-col>
     */
    export default {
        name: "u-col",
        props: {
            // 占父容器宽度的多少等分,总分为12份
            span: {
                type: [Number, String],
                default: 12
            },
            // 指定栅格左侧的间隔数(总12栏)
            offset: {
                type: [Number, String],
                default: 0
            },
            // 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`)
            justify: {
                type: String,
                default: 'start'
            },
            // 垂直对齐方式,可选值为top、center、bottom
            align: {
                type: String,
                default: 'center'
            },
            // 文字对齐方式
            textAlign: {
                type: String,
                default: 'left'
            },
            // 是否阻止事件传播
            stop: {
                type: Boolean,
                default: true
            }
        },
        data() {
            return {
                gutter: 20, // 给col添加间距,左右边距各占一半,从父组件u-row获取
            }
        },
        created() {
            this.parent = false;
        },
        mounted() {
            // 获取父组件实例,并赋值给对应的参数
            this.parent = this.$u.$parent.call(this, 'u-row');
            if (this.parent) {
                this.gutter = this.parent.gutter;
            }
        },
        computed: {
            uJustify() {
                if (this.justify == 'end' || this.justify == 'start') return 'flex-' + this.justify;
                else if (this.justify == 'around' || this.justify == 'between') return 'space-' + this.justify;
                else return this.justify;
            },
            uAlignItem() {
                if (this.align == 'top') return 'flex-start';
                if (this.align == 'bottom') return 'flex-end';
                else return this.align;
            }
        },
        methods: {
            click(e) {
                this.$emit('click');
            }
        }
    }
</script>
<style lang="scss">
    @import "../../libs/css/style.components.scss";
    .u-col {
        /* #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO */
        float: left;
        /* #endif */
    }
    .u-col-0 {
        width: 0;
    }
    .u-col-1 {
        width: calc(100%/12);
    }
    .u-col-2 {
        width: calc(100%/12 * 2);
    }
    .u-col-3 {
        width: calc(100%/12 * 3);
    }
    .u-col-4 {
        width: calc(100%/12 * 4);
    }
    .u-col-5 {
        width: calc(100%/12 * 5);
    }
    .u-col-6 {
        width: calc(100%/12 * 6);
    }
    .u-col-7 {
        width: calc(100%/12 * 7);
    }
    .u-col-8 {
        width: calc(100%/12 * 8);
    }
    .u-col-9 {
        width: calc(100%/12 * 9);
    }
    .u-col-10 {
        width: calc(100%/12 * 10);
    }
    .u-col-11 {
        width: calc(100%/12 * 11);
    }
    .u-col-12 {
        width: calc(100%/12 * 12);
    }
</style>
uview-ui/components/u-collapse-item/u-collapse-item.vue
New file
@@ -0,0 +1,204 @@
<template>
    <view class="u-collapse-item" :style="[itemStyle]">
        <view :hover-stay-time="200" class="u-collapse-head" @tap.stop="headClick" :hover-class="hoverClass" :style="[headStyle]">
            <block v-if="!$slots['title-all']">
                <view v-if="!$slots['title']" class="u-collapse-title u-line-1" :style="[{ textAlign: align ? align : 'left' },
                    isShow && activeStyle && !arrow ? activeStyle : '']">
                    {{ title }}
                </view>
                <slot v-else name="title" />
                <view class="u-icon-wrap">
                    <u-icon v-if="arrow" :color="arrowColor" :class="{ 'u-arrow-down-icon-active': isShow }"
                     class="u-arrow-down-icon" name="arrow-down"></u-icon>
                </view>
            </block>
            <slot v-else name="title-all" />
        </view>
        <view class="u-collapse-body" :style="[{
                height: isShow ? height + 'px' : '0'
            }]">
            <view class="u-collapse-content" :id="elId" :style="[bodyStyle]">
                <slot></slot>
            </view>
        </view>
    </view>
</template>
<script>
    /**
     * collapseItem 手风琴Item
     * @description 通过折叠面板收纳内容区域(搭配u-collapse使用)
     * @tutorial https://www.uviewui.com/components/collapse.html
     * @property {String} title 面板标题
     * @property {String Number} index 主要用于事件的回调,标识那个Item被点击
     * @property {Boolean} disabled 面板是否可以打开或收起(默认false)
     * @property {Boolean} open 设置某个面板的初始状态是否打开(默认false)
     * @property {String Number} name 唯一标识符,如不设置,默认用当前collapse-item的索引值
     * @property {String} align 标题的对齐方式(默认left)
     * @property {Object} active-style 不显示箭头时,可以添加当前选择的collapse-item活动样式,对象形式
     * @event {Function} change 某个item被打开或者收起时触发
     * @example <u-collapse-item :title="item.head" v-for="(item, index) in itemList" :key="index">{{item.body}}</u-collapse-item>
     */
    export default {
        name: "u-collapse-item",
        props: {
            // 标题
            title: {
                type: String,
                default: ''
            },
            // 标题的对齐方式
            align: {
                type: String,
                default: 'left'
            },
            // 是否可以点击收起
            disabled: {
                type: Boolean,
                default: false
            },
            // collapse显示与否
            open: {
                type: Boolean,
                default: false
            },
            // 唯一标识符
            name: {
                type: [Number, String],
                default: ''
            },
            //活动样式
            activeStyle: {
                type: Object,
                default () {
                    return {}
                }
            },
            // 标识当前为第几个
            index: {
                type: [String, Number],
                default: ''
            }
        },
        data() {
            return {
                isShow: false,
                elId: this.$u.guid(),
                height: 0, // body内容的高度
                headStyle: {}, // 头部样式,对象形式
                bodyStyle: {}, // 主体部分样式
                itemStyle: {}, // 每个item的整体样式
                arrowColor: '', // 箭头的颜色
                hoverClass: '', // 头部按下时的效果样式类
                arrow: true, // 是否显示右侧箭头
            };
        },
        watch: {
            open(val) {
                this.isShow = val;
            }
        },
        created() {
            this.parent = false;
            // 获取u-collapse的信息,放在u-collapse是为了方便,不用每个u-collapse-item写一遍
            this.isShow = this.open;
        },
        methods: {
            // 异步获取内容,或者动态修改了内容时,需要重新初始化
            init() {
                this.parent = this.$u.$parent.call(this, 'u-collapse');
                if(this.parent) {
                    this.nameSync = this.name ? this.name : this.parent.childrens.length;
                    this.parent.childrens.push(this);
                    this.headStyle = this.parent.headStyle;
                    this.bodyStyle = this.parent.bodyStyle;
                    this.arrowColor = this.parent.arrowColor;
                    this.hoverClass = this.parent.hoverClass;
                    this.arrow = this.parent.arrow;
                    this.itemStyle = this.parent.itemStyle;
                }
                this.$nextTick(() => {
                    this.queryRect();
                });
            },
            // 点击collapsehead头部
            headClick() {
                if (this.disabled) return;
                if (this.parent && this.parent.accordion == true) {
                    this.parent.childrens.map(val => {
                        // 自身不设置为false,因为后面有this.isShow = !this.isShow;处理了
                        if (this != val) {
                            val.isShow = false;
                        }
                    });
                }
                this.isShow = !this.isShow;
                // 触发本组件的事件
                this.$emit('change', {
                    index: this.index,
                    show: this.isShow
                })
                // 只有在打开时才发出事件
                if (this.isShow) this.parent && this.parent.onChange();
                this.$forceUpdate();
            },
            // 查询内容高度
            queryRect() {
                // $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://www.uviewui.com/js/getRect.html
                // 组件内部一般用this.$uGetRect,对外的为this.$u.getRect,二者功能一致,名称不同
                this.$uGetRect('#' + this.elId).then(res => {
                    this.height = res.height;
                })
            }
        },
        mounted() {
            this.init();
        }
    };
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-collapse-head {
        position: relative;
        @include vue-flex;
        justify-content: space-between;
        align-items: center;
        color: $u-main-color;
        font-size: 30rpx;
        line-height: 1;
        padding: 24rpx 0;
        text-align: left;
    }
    .u-collapse-title {
        flex: 1;
        overflow: hidden;
    }
    .u-arrow-down-icon {
        transition: all 0.3s;
        margin-right: 20rpx;
        margin-left: 14rpx;
    }
    .u-arrow-down-icon-active {
        transform: rotate(180deg);
        transform-origin: center center;
    }
    .u-collapse-body {
        overflow: hidden;
        transition: all 0.3s;
    }
    .u-collapse-content {
        overflow: hidden;
        font-size: 28rpx;
        color: $u-tips-color;
        text-align: left;
    }
</style>
uview-ui/components/u-collapse/u-collapse.vue
New file
@@ -0,0 +1,99 @@
<template>
    <view class="u-collapse">
        <slot />
    </view>
</template>
<script>
    /**
     * collapse 手风琴
     * @description 通过折叠面板收纳内容区域
     * @tutorial https://www.uviewui.com/components/collapse.html
     * @property {Boolean} accordion 是否手风琴模式(默认true)
     * @property {Boolean} arrow 是否显示标题右侧的箭头(默认true)
     * @property {String} arrow-color 标题右侧箭头的颜色(默认#909399)
     * @property {Object} head-style 标题自定义样式,对象形式
     * @property {Object} body-style 主体自定义样式,对象形式
     * @property {String} hover-class 样式类名,按下时有效(默认u-hover-class)
     * @event {Function} change 当前激活面板展开时触发(如果是手风琴模式,参数activeNames类型为String,否则为Array)
     * @example <u-collapse></u-collapse>
     */
    export default {
        name:"u-collapse",
        props: {
            // 是否手风琴模式
            accordion: {
                type: Boolean,
                default: true
            },
            // 头部的样式
            headStyle: {
                type: Object,
                default () {
                    return {}
                }
            },
            // 主体的样式
            bodyStyle: {
                type: Object,
                default () {
                    return {}
                }
            },
            // 每一个item的样式
            itemStyle: {
                type: Object,
                default () {
                    return {}
                }
            },
            // 是否显示右侧的箭头
            arrow: {
                type: Boolean,
                default: true
            },
            // 箭头的颜色
            arrowColor: {
                type: String,
                default: '#909399'
            },
            // 标题部分按压时的样式类,"none"为无效果
            hoverClass: {
                type: String,
                default: 'u-hover-class'
            }
        },
        created() {
            this.childrens = []
        },
        data() {
            return {
            }
        },
        methods: {
            // 重新初始化一次内部的所有子元素的高度计算,用于异步获取数据渲染的情况
            init() {
                this.childrens.forEach((vm, index) => {
                    vm.init();
                })
            },
            // collapse item被点击,由collapse item调用父组件方法
            onChange() {
                let activeItem = [];
                this.childrens.forEach((vm, index) => {
                    if (vm.isShow) {
                        activeItem.push(vm.nameSync);
                    }
                })
                // 如果是手风琴模式,只有一个匹配结果,也即activeItem长度为1,将其转为字符串
                if (this.accordion) activeItem = activeItem.join('');
                this.$emit('change', activeItem);
            }
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
</style>
uview-ui/components/u-column-notice/u-column-notice.vue
New file
@@ -0,0 +1,237 @@
<template>
    <view
        class="u-notice-bar"
        :style="{
            background: computeBgColor,
            padding: padding
        }"
        :class="[
            type ? `u-type-${type}-light-bg` : ''
        ]"
    >
        <view class="u-icon-wrap">
            <u-icon class="u-left-icon" v-if="volumeIcon" name="volume-fill" :size="volumeSize" :color="computeColor"></u-icon>
        </view>
        <swiper :disable-touch="disableTouch" @change="change" :autoplay="autoplay && playState == 'play'" :vertical="vertical" circular :interval="duration" class="u-swiper">
            <swiper-item v-for="(item, index) in list" :key="index" class="u-swiper-item">
                <view
                    class="u-news-item u-line-1"
                    :style="[textStyle]"
                    @tap="click(index)"
                    :class="['u-type-' + type]"
                >
                    {{ item }}
                </view>
            </swiper-item>
        </swiper>
        <view class="u-icon-wrap">
            <u-icon @click="getMore" class="u-right-icon" v-if="moreIcon" name="arrow-right" :size="26" :color="computeColor"></u-icon>
            <u-icon @click="close" class="u-right-icon" v-if="closeIcon" name="close" :size="24" :color="computeColor"></u-icon>
        </view>
    </view>
</template>
<script>
export default {
    props: {
        // 显示的内容,数组
        list: {
            type: Array,
            default() {
                return [];
            }
        },
        // 显示的主题,success|error|primary|info|warning
        type: {
            type: String,
            default: 'warning'
        },
        // 是否显示左侧的音量图标
        volumeIcon: {
            type: Boolean,
            default: true
        },
        // 是否显示右侧的右箭头图标
        moreIcon: {
            type: Boolean,
            default: false
        },
        // 是否显示右侧的关闭图标
        closeIcon: {
            type: Boolean,
            default: false
        },
        // 是否自动播放
        autoplay: {
            type: Boolean,
            default: true
        },
        // 文字颜色,各图标也会使用文字颜色
        color: {
            type: String,
            default: ''
        },
        // 背景颜色
        bgColor: {
            type: String,
            default: ''
        },
        // 滚动方向,row-水平滚动,column-垂直滚动
        direction: {
            type: String,
            default: 'row'
        },
        // 是否显示
        show: {
            type: Boolean,
            default: true
        },
        // 字体大小,单位rpx
        fontSize: {
            type: [Number, String],
            default: 26
        },
        // 滚动一个周期的时间长,单位ms
        duration: {
            type: [Number, String],
            default: 2000
        },
        // 音量喇叭的大小
        volumeSize: {
            type: [Number, String],
            default: 34
        },
        // 水平滚动时的滚动速度,即每秒滚动多少rpx,这有利于控制文字无论多少时,都能有一个恒定的速度
        speed: {
            type: Number,
            default: 160
        },
        // 水平滚动时,是否采用衔接形式滚动
        isCircular: {
            type: Boolean,
            default: true
        },
        // 滚动方向,horizontal-水平滚动,vertical-垂直滚动
        mode: {
            type: String,
            default: 'horizontal'
        },
        // 播放状态,play-播放,paused-暂停
        playState: {
            type: String,
            default: 'play'
        },
        // 是否禁止用手滑动切换
        // 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序
        disableTouch: {
            type: Boolean,
            default: true
        },
        // 通知的边距
        padding: {
            type: [Number, String],
            default: '18rpx 24rpx'
        }
    },
    computed: {
        // 计算字体颜色,如果没有自定义的,就用uview主题颜色
        computeColor() {
            if (this.color) return this.color;
            // 如果是无主题,就默认使用content-color
            else if(this.type == 'none') return '#606266';
            else return this.type;
        },
        // 文字内容的样式
        textStyle() {
            let style = {};
            if (this.color) style.color = this.color;
            else if(this.type == 'none') style.color = '#606266';
            style.fontSize = this.fontSize + 'rpx';
            return style;
        },
        // 垂直或者水平滚动
        vertical() {
            if(this.mode == 'horizontal') return false;
            else return true;
        },
        // 计算背景颜色
        computeBgColor() {
            if (this.bgColor) return this.bgColor;
            else if(this.type == 'none') return 'transparent';
        }
    },
    data() {
        return {
            // animation: false
        };
    },
    methods: {
        // 点击通告栏
        click(index) {
            this.$emit('click', index);
        },
        // 点击关闭按钮
        close() {
            this.$emit('close');
        },
        // 点击更多箭头按钮
        getMore() {
            this.$emit('getMore');
        },
        change(e) {
            let index = e.detail.current;
            if(index == this.list.length - 1) {
                this.$emit('end');
            }
        }
    }
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-notice-bar {
    width: 100%;
    @include vue-flex;
    align-items: center;
    justify-content: center;
    flex-wrap: nowrap;
    padding: 18rpx 24rpx;
    overflow: hidden;
}
.u-swiper {
    font-size: 26rpx;
    height: 32rpx;
    @include vue-flex;
    align-items: center;
    flex: 1;
    margin-left: 12rpx;
}
.u-swiper-item {
    @include vue-flex;
    align-items: center;
    overflow: hidden;
}
.u-news-item {
    overflow: hidden;
}
.u-right-icon {
    margin-left: 12rpx;
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
    align-items: center;
}
.u-left-icon {
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
    align-items: center;
}
</style>
uview-ui/components/u-count-down/u-count-down.vue
New file
@@ -0,0 +1,318 @@
<template>
    <view class="u-countdown">
        <view class="u-countdown-item" :style="[itemStyle]" v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))">
            <view class="u-countdown-time" :style="[letterStyle]">
                {{ d }}
            </view>
        </view>
        <view
            class="u-countdown-colon"
            :style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
            v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))"
        >
            {{ separator == 'colon' ? ':' : '天' }}
        </view>
        <view class="u-countdown-item" :style="[itemStyle]" v-if="showHours">
            <view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
                {{ h }}
            </view>
        </view>
        <view
            class="u-countdown-colon"
            :style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
            v-if="showHours"
        >
            {{ separator == 'colon' ? ':' : '时' }}
        </view>
        <view class="u-countdown-item" :style="[itemStyle]" v-if="showMinutes">
            <view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
                {{ i }}
            </view>
        </view>
        <view
            class="u-countdown-colon"
            :style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
            v-if="showMinutes"
        >
            {{ separator == 'colon' ? ':' : '分' }}
        </view>
        <view class="u-countdown-item" :style="[itemStyle]" v-if="showSeconds">
            <view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
                {{ s }}
            </view>
        </view>
        <view
            class="u-countdown-colon"
            :style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
            v-if="showSeconds && separator == 'zh'"
        >
            秒
        </view>
    </view>
</template>
<script>
/**
 * countDown 倒计时
 * @description 该组件一般使用于某个活动的截止时间上,通过数字的变化,给用户明确的时间感受,提示用户进行某一个行为操作。
 * @tutorial https://www.uviewui.com/components/countDown.html
 * @property {String Number} timestamp 倒计时,单位为秒
 * @property {Boolean} autoplay 是否自动开始倒计时,如果为false,需手动调用开始方法。见官网说明(默认true)
 * @property {String} separator 分隔符,colon为英文冒号,zh为中文(默认colon)
 * @property {String Number} separator-size 分隔符的字体大小,单位rpx(默认30)
 * @property {String} separator-color 分隔符的颜色(默认#303133)
 * @property {String Number} font-size 倒计时字体大小,单位rpx(默认30)
 * @property {Boolean} show-border 是否显示倒计时数字的边框(默认false)
 * @property {Boolean} hide-zero-day 当"天"的部分为0时,隐藏该字段 (默认true)
 * @property {String} border-color 数字边框的颜色(默认#303133)
 * @property {String} bg-color 倒计时数字的背景颜色(默认#ffffff)
 * @property {String} color 倒计时数字的颜色(默认#303133)
 * @property {String} height 数字高度值(宽度等同此值),设置边框时看情况是否需要设置此值,单位rpx(默认auto)
 * @property {Boolean} show-days 是否显示倒计时的"天"部分(默认true)
 * @property {Boolean} show-hours 是否显示倒计时的"时"部分(默认true)
 * @property {Boolean} show-minutes 是否显示倒计时的"分"部分(默认true)
 * @property {Boolean} show-seconds 是否显示倒计时的"秒"部分(默认true)
 * @event {Function} end 倒计时结束
 * @event {Function} change 每秒触发一次,回调为当前剩余的倒计秒数
 * @example <u-count-down ref="uCountDown" :timestamp="86400" :autoplay="false"></u-count-down>
 */
export default {
    name: 'u-count-down',
    props: {
        // 倒计时的时间,秒为单位
        timestamp: {
            type: [Number, String],
            default: 0
        },
        // 是否自动开始倒计时
        autoplay: {
            type: Boolean,
            default: true
        },
        // 用英文冒号(colon)或者中文(zh)当做分隔符,false的时候为中文,如:"11:22"或"11时22秒"
        separator: {
            type: String,
            default: 'colon'
        },
        // 分隔符的大小,单位rpx
        separatorSize: {
            type: [Number, String],
            default: 30
        },
        // 分隔符颜色
        separatorColor: {
            type: String,
            default: "#303133"
        },
        // 字体颜色
        color: {
            type: String,
            default: '#303133'
        },
        // 字体大小,单位rpx
        fontSize: {
            type: [Number, String],
            default: 30
        },
        // 背景颜色
        bgColor: {
            type: String,
            default: '#fff'
        },
        // 数字框高度,单位rpx
        height: {
            type: [Number, String],
            default: 'auto'
        },
        // 是否显示数字框
        showBorder: {
            type: Boolean,
            default: false
        },
        // 边框颜色
        borderColor: {
            type: String,
            default: '#303133'
        },
        // 是否显示秒
        showSeconds: {
            type: Boolean,
            default: true
        },
        // 是否显示分钟
        showMinutes: {
            type: Boolean,
            default: true
        },
        // 是否显示小时
        showHours: {
            type: Boolean,
            default: true
        },
        // 是否显示“天”
        showDays: {
            type: Boolean,
            default: true
        },
        // 当"天"的部分为0时,不显示
        hideZeroDay: {
            type: Boolean,
            default: false
        }
    },
    watch: {
        // 监听时间戳的变化
        timestamp(newVal, oldVal) {
            // 如果倒计时间发生变化,清除定时器,重新开始倒计时
            this.clearTimer();
            this.start();
        }
    },
    data() {
        return {
            d: '00', // 天的默认值
            h: '00', // 小时的默认值
            i: '00', // 分钟的默认值
            s: '00', // 秒的默认值
            timer: null ,// 定时器
            seconds: 0, // 记录不停倒计过程中变化的秒数
        };
    },
    computed: {
        // 倒计时item的样式,item为分别的时分秒部分的数字
        itemStyle() {
            let style = {};
            if(this.height) {
                style.height = this.height + 'rpx';
                style.width = this.height + 'rpx';
            }
            if(this.showBorder) {
                style.borderStyle = 'solid';
                style.borderColor = this.borderColor;
                style.borderWidth = '1px';
            }
            if(this.bgColor) {
                style.backgroundColor = this.bgColor;
            }
            return style;
        },
        // 倒计时数字的样式
        letterStyle() {
            let style = {};
            if(this.fontSize) style.fontSize = this.fontSize +  'rpx';
            if(this.color) style.color = this.color;
            return style;
        }
    },
    mounted() {
        // 如果自动倒计时
        this.autoplay && this.timestamp && this.start();
    },
    methods: {
        // 倒计时
        start() {
            // 避免可能出现的倒计时重叠情况
            this.clearTimer();
            if (this.timestamp <= 0) return;
            this.seconds = Number(this.timestamp);
            this.formatTime(this.seconds);
            this.timer = setInterval(() => {
                this.seconds--;
                // 发出change事件
                this.$emit('change', this.seconds);
                if (this.seconds < 0) {
                    return this.end();
                }
                this.formatTime(this.seconds);
            }, 1000);
        },
        // 格式化时间
        formatTime(seconds) {
            // 小于等于0的话,结束倒计时
            seconds <= 0 && this.end();
            let [day, hour, minute, second] = [0, 0, 0, 0];
            day = Math.floor(seconds / (60 * 60 * 24));
            // 判断是否显示“天”参数,如果不显示,将天部分的值,加入到小时中
            // hour为给后面计算秒和分等用的(基于显示天的前提下计算)
            hour = Math.floor(seconds / (60 * 60)) - day * 24;
            // showHour为需要显示的小时
            let showHour = null;
            if(this.showDays) {
                showHour = hour;
            } else {
                // 如果不显示天数,将“天”部分的时间折算到小时中去
                showHour = Math.floor(seconds / (60 * 60));
            }
            minute = Math.floor(seconds / 60) - hour * 60 - day * 24 * 60;
            second = Math.floor(seconds) - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60;
            // 如果小于10,在前面补上一个"0"
            showHour = showHour < 10 ? '0' + showHour : showHour;
            minute = minute < 10 ? '0' + minute : minute;
            second = second < 10 ? '0' + second : second;
            day = day < 10 ? '0' + day : day;
            this.d = day;
            this.h = showHour;
            this.i = minute;
            this.s = second;
        },
        // 停止倒计时
        end() {
            this.clearTimer();
            this.$emit('end', {});
        },
        // 清除定时器
        clearTimer() {
            if(this.timer) {
                // 清除定时器
                clearInterval(this.timer);
                this.timer = null;
            }
        }
    },
    beforeDestroy() {
        clearInterval(this.timer);
        this.timer = null;
    }
};
</script>
<style scoped lang="scss">
    @import "../../libs/css/style.components.scss";
    .u-countdown {
        /* #ifndef APP-NVUE */
        display: inline-flex;
        /* #endif */
        align-items: center;
    }
    .u-countdown-item {
        @include vue-flex;
        align-items: center;
        justify-content: center;
        padding: 2rpx;
        border-radius: 6rpx;
        white-space: nowrap;
        transform: translateZ(0);
    }
    .u-countdown-time {
        margin: 0;
        padding: 0;
        line-height: 1;
    }
    .u-countdown-colon {
        @include vue-flex;
        justify-content: center;
        padding: 0 5rpx;
        line-height: 1;
        align-items: center;
        padding-bottom: 4rpx;
    }
    .u-countdown-scale {
        transform: scale(0.9);
        transform-origin: center center;
    }
</style>
uview-ui/components/u-count-to/u-count-to.vue
New file
@@ -0,0 +1,241 @@
<template>
    <view
        class="u-count-num"
        :style="{
            fontSize: fontSize + 'rpx',
            fontWeight: bold ? 'bold' : 'normal',
            color: color
        }"
    >
        {{ displayValue }}
    </view>
</template>
<script>
/**
 * countTo 数字滚动
 * @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。
 * @tutorial https://www.uviewui.com/components/countTo.html
 * @property {String Number} start-val 开始值
 * @property {String Number} end-val 结束值
 * @property {String Number} duration 滚动过程所需的时间,单位ms(默认2000)
 * @property {Boolean} autoplay 是否自动开始滚动(默认true)
 * @property {String Number} decimals 要显示的小数位数,见官网说明(默认0)
 * @property {Boolean} use-easing 滚动结束时,是否缓动结尾,见官网说明(默认true)
 * @property {String} separator 千位分隔符,见官网说明
 * @property {String} color 字体颜色(默认#303133)
 * @property {String Number} font-size 字体大小,单位rpx(默认50)
 * @property {Boolean} bold 字体是否加粗(默认false)
 * @event {Function} end 数值滚动到目标值时触发
 * @example <u-count-to ref="uCountTo" :end-val="endVal" :autoplay="autoplay"></u-count-to>
 */
export default {
    name: 'u-count-to',
    props: {
        // 开始的数值,默认从0增长到某一个数
        startVal: {
            type: [Number, String],
            default: 0
        },
        // 要滚动的目标数值,必须
        endVal: {
            type: [Number, String],
            default: 0,
            required: true
        },
        // 滚动到目标数值的动画持续时间,单位为毫秒(ms)
        duration: {
            type: [Number, String],
            default: 2000
        },
        // 设置数值后是否自动开始滚动
        autoplay: {
            type: Boolean,
            default: true
        },
        // 要显示的小数位数
        decimals: {
            type: [Number, String],
            default: 0
        },
        // 是否在即将到达目标数值的时候,使用缓慢滚动的效果
        useEasing: {
            type: Boolean,
            default: true
        },
        // 十进制分割
        decimal: {
            type: [Number, String],
            default: '.'
        },
        // 字体颜色
        color: {
            type: String,
            default: '#303133'
        },
        // 字体大小
        fontSize: {
            type: [Number, String],
            default: 50
        },
        // 是否加粗字体
        bold: {
            type: Boolean,
            default: false
        },
        // 千位分隔符,类似金额的分割(¥23,321.05中的",")
        separator: {
            type: String,
            default: ''
        }
    },
    data() {
        return {
            localStartVal: this.startVal,
            displayValue: this.formatNumber(this.startVal),
            printVal: null,
            paused: false, // 是否暂停
            localDuration: Number(this.duration),
            startTime: null, // 开始的时间
            timestamp: null, // 时间戳
            remaining: null, // 停留的时间
            rAF: null,
            lastTime: 0 // 上一次的时间
        };
    },
    computed: {
        countDown() {
            return this.startVal > this.endVal;
        }
    },
    watch: {
        startVal() {
            this.autoplay && this.start();
        },
        endVal() {
            this.autoplay && this.start();
        }
    },
    mounted() {
        this.autoplay && this.start();
    },
    methods: {
        easingFn(t, b, c, d) {
            return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
        },
        requestAnimationFrame(callback) {
            const currTime = new Date().getTime();
            // 为了使setTimteout的尽可能的接近每秒60帧的效果
            const timeToCall = Math.max(0, 16 - (currTime - this.lastTime));
            const id = setTimeout(() => {
                callback(currTime + timeToCall);
            }, timeToCall);
            this.lastTime = currTime + timeToCall;
            return id;
        },
        cancelAnimationFrame(id) {
            clearTimeout(id);
        },
        // 开始滚动数字
        start() {
            this.localStartVal = this.startVal;
            this.startTime = null;
            this.localDuration = this.duration;
            this.paused = false;
            this.rAF = this.requestAnimationFrame(this.count);
        },
        // 暂定状态,重新再开始滚动;或者滚动状态下,暂停
        reStart() {
            if (this.paused) {
                this.resume();
                this.paused = false;
            } else {
                this.stop();
                this.paused = true;
            }
        },
        // 暂停
        stop() {
            this.cancelAnimationFrame(this.rAF);
        },
        // 重新开始(暂停的情况下)
        resume() {
            this.startTime = null;
            this.localDuration = this.remaining;
            this.localStartVal = this.printVal;
            this.requestAnimationFrame(this.count);
        },
        // 重置
        reset() {
            this.startTime = null;
            this.cancelAnimationFrame(this.rAF);
            this.displayValue = this.formatNumber(this.startVal);
        },
        count(timestamp) {
            if (!this.startTime) this.startTime = timestamp;
            this.timestamp = timestamp;
            const progress = timestamp - this.startTime;
            this.remaining = this.localDuration - progress;
            if (this.useEasing) {
                if (this.countDown) {
                    this.printVal = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration);
                } else {
                    this.printVal = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration);
                }
            } else {
                if (this.countDown) {
                    this.printVal = this.localStartVal - (this.localStartVal - this.endVal) * (progress / this.localDuration);
                } else {
                    this.printVal = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration);
                }
            }
            if (this.countDown) {
                this.printVal = this.printVal < this.endVal ? this.endVal : this.printVal;
            } else {
                this.printVal = this.printVal > this.endVal ? this.endVal : this.printVal;
            }
            this.displayValue = this.formatNumber(this.printVal);
            if (progress < this.localDuration) {
                this.rAF = this.requestAnimationFrame(this.count);
            } else {
                this.$emit('end');
            }
        },
        // 判断是否数字
        isNumber(val) {
            return !isNaN(parseFloat(val));
        },
        formatNumber(num) {
            // 将num转为Number类型,因为其值可能为字符串数值,调用toFixed会报错
            num = Number(num);
            num = num.toFixed(Number(this.decimals));
            num += '';
            const x = num.split('.');
            let x1 = x[0];
            const x2 = x.length > 1 ? this.decimal + x[1] : '';
            const rgx = /(\d+)(\d{3})/;
            if (this.separator && !this.isNumber(this.separator)) {
                while (rgx.test(x1)) {
                    x1 = x1.replace(rgx, '$1' + this.separator + '$2');
                }
            }
            return x1 + x2;
        },
        destroyed() {
            this.cancelAnimationFrame(this.rAF);
        }
    }
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-count-num {
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
    text-align: center;
}
</style>
uview-ui/components/u-divider/u-divider.vue
New file
@@ -0,0 +1,153 @@
<template>
    <view class="u-divider" :style="{
        height: height == 'auto' ? 'auto' : height + 'rpx',
        backgroundColor: bgColor,
        marginBottom: marginBottom + 'rpx',
        marginTop: marginTop + 'rpx'
    }" @tap="click">
        <view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
        <view v-if="useSlot" class="u-divider-text" :style="{
            color: color,
            fontSize: fontSize + 'rpx'
        }"><slot /></view>
        <view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
    </view>
</template>
<script>
/**
 * divider 分割线
 * @description 区隔内容的分割线,一般用于页面底部"没有更多"的提示。
 * @tutorial https://www.uviewui.com/components/divider.html
 * @property {String Number} half-width 文字左或右边线条宽度,数值或百分比,数值时单位为rpx
 * @property {String} border-color 线条颜色,优先级高于type(默认#dcdfe6)
 * @property {String} color 文字颜色(默认#909399)
 * @property {String Number} fontSize 字体大小,单位rpx(默认26)
 * @property {String} bg-color 整个divider的背景颜色(默认呢#ffffff)
 * @property {String Number} height 整个divider的高度,单位rpx(默认40)
 * @property {String} type 将线条设置主题色(默认primary)
 * @property {Boolean} useSlot 是否使用slot传入内容,如果不传入,中间不会有空隙(默认true)
 * @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0)
 * @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0)
 * @event {Function} click divider组件被点击时触发
 * @example <u-divider color="#fa3534">长河落日圆</u-divider>
 */
export default {
    name: 'u-divider',
    props: {
        // 单一边divider横线的宽度(数值),单位rpx。或者百分比
        halfWidth: {
            type: [Number, String],
            default: 150
        },
        // divider横线的颜色,如设置,
        borderColor: {
            type: String,
            default: '#dcdfe6'
        },
        // 主题色,可以是primary|info|success|warning|error之一值
        type: {
            type: String,
            default: 'primary'
        },
        // 文字颜色
        color: {
            type: String,
            default: '#909399'
        },
        // 文字大小,单位rpx
        fontSize: {
            type: [Number, String],
            default: 26
        },
        // 整个divider的背景颜色
        bgColor: {
            type: String,
            default: '#ffffff'
        },
        // 整个divider的高度单位rpx
        height: {
            type: [Number, String],
            default: 'auto'
        },
        // 上边距
        marginTop: {
            type: [String, Number],
            default: 0
        },
        // 下边距
        marginBottom: {
            type: [String, Number],
            default: 0
        },
        // 是否使用slot传入内容,如果不用slot传入内容,先的中间就不会有空隙
        useSlot: {
            type: Boolean,
            default: true
        }
    },
    computed: {
        lineStyle() {
            let style = {};
            if(String(this.halfWidth).indexOf('%') != -1) style.width = this.halfWidth;
            else style.width = this.halfWidth + 'rpx';
            // borderColor优先级高于type值
            if(this.borderColor) style.borderColor = this.borderColor;
            return style;
        }
    },
    methods: {
        click() {
            this.$emit('click');
        }
    }
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-divider {
    width: 100%;
    position: relative;
    text-align: center;
    @include vue-flex;
    justify-content: center;
    align-items: center;
    overflow: hidden;
    flex-direction: row;
}
.u-divider-line {
    border-bottom: 1px solid $u-border-color;
    transform: scale(1, 0.5);
    transform-origin: center;
    &--bordercolor--primary {
        border-color: $u-type-primary;
    }
    &--bordercolor--success {
        border-color: $u-type-success;
    }
    &--bordercolor--error {
        border-color: $u-type-primary;
    }
    &--bordercolor--info {
        border-color: $u-type-info;
    }
    &--bordercolor--warning {
        border-color: $u-type-warning;
    }
}
.u-divider-text {
    white-space: nowrap;
    padding: 0 16rpx;
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
}
</style>
uview-ui/components/u-dropdown-item/u-dropdown-item.vue
New file
@@ -0,0 +1,132 @@
<template>
    <view class="u-dropdown-item" v-if="active" @touchmove.stop.prevent="() => {}" @tap.stop.prevent="() => {}">
        <block v-if="!$slots.default && !$slots.$default">
            <scroll-view scroll-y="true" :style="{
                height: $u.addUnit(height)
            }">
                <view class="u-dropdown-item__options">
                    <u-cell-group>
                        <u-cell-item @click="cellClick(item.value)" :arrow="false" :title="item.label" v-for="(item, index) in options"
                         :key="index" :title-style="{
                            color: value == item.value ? activeColor : inactiveColor
                        }">
                            <u-icon v-if="value == item.value" name="checkbox-mark" :color="activeColor" size="32"></u-icon>
                        </u-cell-item>
                    </u-cell-group>
                </view>
            </scroll-view>
        </block>
        <slot v-else />
    </view>
</template>
<script>
    /**
     * dropdown-item 下拉菜单
     * @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
     * @tutorial http://uviewui.com/components/dropdown.html
     * @property {String | Number} v-model 双向绑定选项卡选择值
     * @property {String} title 菜单项标题
     * @property {Array[Object]} options 选项数据,如果传入了默认slot,此参数无效
     * @property {Boolean} disabled 是否禁用此选项卡(默认false)
     * @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300)
     * @property {String | Number} height 弹窗下拉内容的高度(内容超出将会滚动)(默认auto)
     * @example <u-dropdown-item title="标题"></u-dropdown-item>
     */
    export default {
        name: 'u-dropdown-item',
        props: {
            // 当前选中项的value值
            value: {
                type: [Number, String, Array],
                default: ''
            },
            // 菜单项标题
            title: {
                type: [String, Number],
                default: ''
            },
            // 选项数据,如果传入了默认slot,此参数无效
            options: {
                type: Array,
                default () {
                    return []
                }
            },
            // 是否禁用此菜单项
            disabled: {
                type: Boolean,
                default: false
            },
            // 下拉弹窗的高度
            height: {
                type: [Number, String],
                default: 'auto'
            },
        },
        data() {
            return {
                active: false, // 当前项是否处于展开状态
                activeColor: '#2979ff', // 激活时左边文字和右边对勾图标的颜色
                inactiveColor: '#606266', // 未激活时左边文字和右边对勾图标的颜色
            }
        },
        computed: {
            // 监听props是否发生了变化,有些值需要传递给父组件u-dropdown,无法双向绑定
            propsChange() {
                return `${this.title}-${this.disabled}`;
            }
        },
        watch: {
            propsChange(n) {
                // 当值变化时,通知父组件重新初始化,让父组件执行每个子组件的init()方法
                // 将所有子组件数据重新整理一遍
                if (this.parent) this.parent.init();
            }
        },
        created() {
            // 父组件的实例
            this.parent = false;
        },
        methods: {
            init() {
                // 获取父组件u-dropdown
                let parent = this.$u.$parent.call(this, 'u-dropdown');
                if (parent) {
                    this.parent = parent;
                    // 将子组件的激活颜色配置为父组件设置的激活和未激活时的颜色
                    this.activeColor = parent.activeColor;
                    this.inactiveColor = parent.inactiveColor;
                    // 将本组件的this,放入到父组件的children数组中,让父组件可以操作本(子)组件的方法和属性
                    // push进去前,显判断是否已经存在了本实例,因为在子组件内部数据变化时,会通过父组件重新初始化子组件
                    let exist = parent.children.find(val => {
                        return this === val;
                    })
                    if (!exist) parent.children.push(this);
                    if (parent.children.length == 1) this.active = true;
                    // 父组件无法监听children的变化,故将子组件的title,传入父组件的menuList数组中
                    parent.menuList.push({
                        title: this.title,
                        disabled: this.disabled
                    });
                }
            },
            // cell被点击
            cellClick(value) {
                // 修改通过v-model绑定的值
                this.$emit('input', value);
                // 通知父组件(u-dropdown)收起菜单
                this.parent.close();
                // 发出事件,抛出当前勾选项的value
                this.$emit('change', value);
            }
        },
        mounted() {
            this.init();
        }
    }
</script>
<style scoped lang="scss">
    @import "../../libs/css/style.components.scss";
</style>
uview-ui/components/u-dropdown/u-dropdown.vue
New file
@@ -0,0 +1,298 @@
<template>
    <view class="u-dropdown">
        <view class="u-dropdown__menu" :style="{
            height: $u.addUnit(height)
        }" :class="{
            'u-border-bottom': borderBottom
        }">
            <view class="u-dropdown__menu__item" v-for="(item, index) in menuList" :key="index" @tap.stop="menuClick(index)">
                <view class="u-flex">
                    <text class="u-dropdown__menu__item__text" :style="{
                        color: item.disabled ? '#c0c4cc' : (index === current || highlightIndex == index) ? activeColor : inactiveColor,
                        fontSize: $u.addUnit(titleSize)
                    }">{{item.title}}</text>
                    <view class="u-dropdown__menu__item__arrow" :class="{
                        'u-dropdown__menu__item__arrow--rotate': index === current
                    }">
                        <u-icon :custom-style="{display: 'flex'}" :name="menuIcon" :size="$u.addUnit(menuIconSize)" :color="index === current || highlightIndex == index ? activeColor : '#c0c4cc'"></u-icon>
                    </view>
                </view>
            </view>
        </view>
        <view class="u-dropdown__content" :style="[contentStyle, {
            transition: `opacity ${duration / 1000}s linear`,
            top: $u.addUnit(height),
            height: contentHeight + 'px'
        }]"
         @tap="maskClick" @touchmove.stop.prevent>
            <view @tap.stop.prevent class="u-dropdown__content__popup" :style="[popupStyle]">
                <slot></slot>
            </view>
            <view class="u-dropdown__content__mask"></view>
        </view>
    </view>
</template>
<script>
    /**
     * dropdown 下拉菜单
     * @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
     * @tutorial http://uviewui.com/components/dropdown.html
     * @property {String} active-color 标题和选项卡选中的颜色(默认#2979ff)
     * @property {String} inactive-color 标题和选项卡未选中的颜色(默认#606266)
     * @property {Boolean} close-on-click-mask 点击遮罩是否关闭菜单(默认true)
     * @property {Boolean} close-on-click-self 点击当前激活项标题是否关闭菜单(默认true)
     * @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300)
     * @property {String | Number} height 标题菜单的高度,单位任意(默认80)
     * @property {String | Number} border-radius 菜单展开内容下方的圆角值,单位任意(默认0)
     * @property {Boolean} border-bottom 标题菜单是否显示下边框(默认false)
     * @property {String | Number} title-size 标题的字体大小,单位任意,数值默认为rpx单位(默认28)
     * @event {Function} open 下拉菜单被打开时触发
     * @event {Function} close 下拉菜单被关闭时触发
     * @example <u-dropdown></u-dropdown>
     */
    export default {
        name: 'u-dropdown',
        props: {
            // 菜单标题和选项的激活态颜色
            activeColor: {
                type: String,
                default: '#2979ff'
            },
            // 菜单标题和选项的未激活态颜色
            inactiveColor: {
                type: String,
                default: '#606266'
            },
            // 点击遮罩是否关闭菜单
            closeOnClickMask: {
                type: Boolean,
                default: true
            },
            // 点击当前激活项标题是否关闭菜单
            closeOnClickSelf: {
                type: Boolean,
                default: true
            },
            // 过渡时间
            duration: {
                type: [Number, String],
                default: 300
            },
            // 标题菜单的高度,单位任意,数值默认为rpx单位
            height: {
                type: [Number, String],
                default: 80
            },
            // 是否显示下边框
            borderBottom: {
                type: Boolean,
                default: false
            },
            // 标题的字体大小
            titleSize: {
                type: [Number, String],
                default: 28
            },
            // 下拉出来的内容部分的圆角值
            borderRadius: {
                type: [Number, String],
                default: 0
            },
            // 菜单右侧的icon图标
            menuIcon: {
                type: String,
                default: 'arrow-down'
            },
            // 菜单右侧图标的大小
            menuIconSize: {
                type: [Number, String],
                default: 26
            }
        },
        data() {
            return {
                showDropdown: true, // 是否打开下来菜单,
                menuList: [], // 显示的菜单
                active: false, // 下拉菜单的状态
                // 当前是第几个菜单处于激活状态,小程序中此处不能写成false或者"",否则后续将current赋值为0,
                // 无能的TX没有使用===而是使用==判断,导致程序认为前后二者没有变化,从而不会触发视图更新
                current: 99999,
                // 外层内容的样式,初始时处于底层,且透明
                contentStyle: {
                    zIndex: -1,
                    opacity: 0
                },
                // 让某个菜单保持高亮的状态
                highlightIndex: 99999,
                contentHeight: 0
            }
        },
        computed: {
            // 下拉出来部分的样式
            popupStyle() {
                let style = {};
                // 进行Y轴位移,展开状态时,恢复原位。收齐状态时,往上位移100%,进行隐藏
                style.transform = `translateY(${this.active ? 0 : '-100%'})`
                style['transition-duration'] = this.duration / 1000 + 's';
                style.borderRadius = `0 0 ${this.$u.addUnit(this.borderRadius)} ${this.$u.addUnit(this.borderRadius)}`;
                return style;
            }
        },
        created() {
            // 引用所有子组件(u-dropdown-item)的this,不能在data中声明变量,否则在微信小程序会造成循环引用而报错
            this.children = [];
        },
        mounted() {
            this.getContentHeight();
        },
        methods: {
            init() {
                // 当某个子组件内容变化时,触发父组件的init,父组件再让每一个子组件重新初始化一遍
                // 以保证数据的正确性
                this.menuList = [];
                this.children.map(child => {
                    child.init();
                })
            },
            // 点击菜单
            menuClick(index) {
                // 判断是否被禁用
                if (this.menuList[index].disabled) return;
                // 如果点击时的索引和当前激活项索引相同,意味着点击了激活项,需要收起下拉菜单
                if (index === this.current && this.closeOnClickSelf) {
                    this.close();
                    // 等动画结束后,再移除下拉菜单中的内容,否则直接移除,也就没有下拉菜单收起的效果了
                    setTimeout(() => {
                        this.children[index].active = false;
                    }, this.duration)
                    return;
                }
                this.open(index);
            },
            // 打开下拉菜单
            open(index) {
                // 重置高亮索引,否则会造成多个菜单同时高亮
                // this.highlightIndex = 9999;
                // 展开时,设置下拉内容的样式
                this.contentStyle = {
                    zIndex: 11,
                }
                // 标记展开状态以及当前展开项的索引
                this.active = true;
                this.current = index;
                // 历遍所有的子元素,将索引匹配的项标记为激活状态,因为子元素是通过v-if控制切换的
                // 之所以不是因display: none,是因为nvue没有display这个属性
                this.children.map((val, idx) => {
                    val.active = index == idx ? true : false;
                })
                this.$emit('open', this.current);
            },
            // 设置下拉菜单处于收起状态
            close() {
                this.$emit('close', this.current);
                // 设置为收起状态,同时current归位,设置为空字符串
                this.active = false;
                this.current = 99999;
                // 下拉内容的样式进行调整,不透明度设置为0
                this.contentStyle = {
                    zIndex: -1,
                    opacity: 0
                }
            },
            // 点击遮罩
            maskClick() {
                // 如果不允许点击遮罩,直接返回
                if (!this.closeOnClickMask) return;
                this.close();
            },
            // 外部手动设置某个菜单高亮
            highlight(index = undefined) {
                this.highlightIndex = index !== undefined ? index : 99999;
            },
            // 获取下拉菜单内容的高度
            getContentHeight() {
                // 这里的原理为,因为dropdown组件是相对定位的,它的下拉出来的内容,必须给定一个高度
                // 才能让遮罩占满菜单一下,直到屏幕底部的高度
                // this.$u.sys()为uView封装的获取设备信息的方法
                let windowHeight = this.$u.sys().windowHeight;
                this.$uGetRect('.u-dropdown__menu').then(res => {
                    // 这里获取的是dropdown的尺寸,在H5上,uniapp获取尺寸是有bug的(以前提出修复过,后来又出现了此bug,目前hx2.8.11版本)
                    // H5端bug表现为元素尺寸的top值为导航栏底部到到元素的上边沿的距离,但是元素的bottom值确是导航栏顶部到元素底部的距离
                    // 二者是互相矛盾的,本质原因是H5端导航栏非原生,uni的开发者大意造成
                    // 这里取菜单栏的botton值合理的,不能用res.top,否则页面会造成滚动
                    this.contentHeight = windowHeight - res.bottom;
                })
            }
        }
    }
</script>
<style scoped lang="scss">
    @import "../../libs/css/style.components.scss";
    .u-dropdown {
        flex: 1;
        width: 100%;
        position: relative;
        &__menu {
            @include vue-flex;
            position: relative;
            z-index: 11;
            height: 80rpx;
            &__item {
                flex: 1;
                @include vue-flex;
                justify-content: center;
                align-items: center;
                &__text {
                    font-size: 28rpx;
                    color: $u-content-color;
                }
                &__arrow {
                    margin-left: 6rpx;
                    transition: transform .3s;
                    align-items: center;
                    @include vue-flex;
                    &--rotate {
                        transform: rotate(180deg);
                    }
                }
            }
        }
        &__content {
            position: absolute;
            z-index: 8;
            width: 100%;
            left: 0px;
            bottom: 0;
            overflow: hidden;
            &__mask {
                position: absolute;
                z-index: 9;
                background: rgba(0, 0, 0, .3);
                width: 100%;
                left: 0;
                top: 0;
                bottom: 0;
            }
            &__popup {
                position: relative;
                z-index: 10;
                transition: all 0.3s;
                transform: translate3D(0, -100%, 0);
                overflow: hidden;
            }
        }
    }
</style>
uview-ui/components/u-empty/u-empty.vue
New file
@@ -0,0 +1,193 @@
<template>
    <view class="u-empty" v-if="show" :style="{
        marginTop: marginTop + 'rpx'
    }">
        <u-icon
            :name="src ? src : 'empty-' + mode"
            :custom-style="iconStyle"
            :label="text ? text : icons[mode]"
            label-pos="bottom"
            :label-color="color"
            :label-size="fontSize"
            :size="iconSize"
            :color="iconColor"
            margin-top="14"
        ></u-icon>
        <view class="u-slot-wrap">
            <slot name="bottom"></slot>
        </view>
    </view>
</template>
<script>
    /**
     * empty 内容为空
     * @description 该组件用于需要加载内容,但是加载的第一页数据就为空,提示一个"没有内容"的场景, 我们精心挑选了十几个场景的图标,方便您使用。
     * @tutorial https://www.uviewui.com/components/empty.html
     * @property {String} color 文字颜色(默认#c0c4cc)
     * @property {String} text 文字提示(默认“无内容”)
     * @property {String} src 自定义图标路径,如定义,mode参数会失效
     * @property {String Number} font-size 提示文字的大小,单位rpx(默认28)
     * @property {String} mode 内置的图标,见官网说明(默认data)
     * @property {String Number} img-width 图标的宽度,单位rpx(默认240)
     * @property {String} img-height 图标的高度,单位rpx(默认auto)
     * @property {String Number} margin-top 组件距离上一个元素之间的距离(默认0)
     * @property {Boolean} show 是否显示组件(默认true)
     * @event {Function} click 点击组件时触发
     * @event {Function} close 点击关闭按钮时触发
     * @example <u-empty text="所谓伊人,在水一方" mode="list"></u-empty>
     */
    export default {
        name: "u-empty",
        props: {
            // 图标路径
            src: {
                type: String,
                default: ''
            },
            // 提示文字
            text: {
                type: String,
                default: ''
            },
            // 文字颜色
            color: {
                type: String,
                default: '#c0c4cc'
            },
            // 图标的颜色
            iconColor: {
                type: String,
                default: '#c0c4cc'
            },
            // 图标的大小
            iconSize: {
                type: [String, Number],
                default: 120
            },
            // 文字大小,单位rpx
            fontSize: {
                type: [String, Number],
                default: 26
            },
            // 选择预置的图标类型
            mode: {
                type: String,
                default: 'data'
            },
            //  图标宽度,单位rpx
            imgWidth: {
                type: [String, Number],
                default: 120
            },
            // 图标高度,单位rpx
            imgHeight: {
                type: [String, Number],
                default: 'auto'
            },
            // 是否显示组件
            show: {
                type: Boolean,
                default: true
            },
            // 组件距离上一个元素之间的距离
            marginTop: {
                type: [String, Number],
                default: 0
            },
            iconStyle: {
                type: Object,
                default() {
                    return {}
                }
            }
        },
        data() {
            return {
                icons: {
                    car: '购物车为空',
                    page: '页面不存在',
                    search: '没有搜索结果',
                    address: '没有收货地址',
                    wifi: '没有WiFi',
                    order: '订单为空',
                    coupon: '没有优惠券',
                    favor: '暂无收藏',
                    permission: '无权限',
                    history: '无历史记录',
                    news: '无新闻列表',
                    message: '消息列表为空',
                    list: '列表为空',
                    data: '数据为空'
                },
                // icons: [{
                //     icon: 'car',
                //     text: '购物车为空'
                // },{
                //     icon: 'page',
                //     text: '页面不存在'
                // },{
                //     icon: 'search',
                //     text: '没有搜索结果'
                // },{
                //     icon: 'address',
                //     text: '没有收货地址'
                // },{
                //     icon: 'wifi',
                //     text: '没有WiFi'
                // },{
                //     icon: 'order',
                //     text: '订单为空'
                // },{
                //     icon: 'coupon',
                //     text: '没有优惠券'
                // },{
                //     icon: 'favor',
                //     text: '暂无收藏'
                // },{
                //     icon: 'permission',
                //     text: '无权限'
                // },{
                //     icon: 'history',
                //     text: '无历史记录'
                // },{
                //     icon: 'news',
                //     text: '无新闻列表'
                // },{
                //     icon: 'message',
                //     text: '消息列表为空'
                // },{
                //     icon: 'list',
                //     text: '列表为空'
                // },{
                //     icon: 'data',
                //     text: '数据为空'
                // }],
            }
        }
    }
</script>
<style scoped lang="scss">
    @import "../../libs/css/style.components.scss";
    .u-empty {
        @include vue-flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        height: 100%;
    }
    .u-image {
        margin-bottom: 20rpx;
    }
    .u-slot-wrap {
        @include vue-flex;
        justify-content: center;
        align-items: center;
        margin-top: 20rpx;
    }
</style>
uview-ui/components/u-field/u-field.vue
New file
@@ -0,0 +1,384 @@
<template>
    <view class="u-field" :class="{'u-border-top': borderTop, 'u-border-bottom': borderBottom }">
        <view class="u-field-inner" :class="[type == 'textarea' ? 'u-textarea-inner' : '', 'u-label-postion-' + labelPosition]">
            <view class="u-label" :class="[required ? 'u-required' : '']" :style="{
                justifyContent: justifyContent,
                flex: labelPosition == 'left' ? `0 0 ${labelWidth}rpx` : '1'
            }">
                <view class="u-icon-wrap" v-if="icon">
                    <u-icon size="32" :custom-style="iconStyle" :name="icon" :color="iconColor" class="u-icon"></u-icon>
                </view>
                <slot name="icon"></slot>
                <text class="u-label-text" :class="[this.$slots.icon || icon ? 'u-label-left-gap' : '']">{{ label }}</text>
            </view>
            <view class="fild-body">
                <view class="u-flex-1 u-flex" :style="[inputWrapStyle]">
                    <textarea v-if="type == 'textarea'" class="u-flex-1 u-textarea-class" :style="[fieldStyle]" :value="value"
                     :placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" :maxlength="inputMaxlength"
                     :focus="focus" :autoHeight="autoHeight" :fixed="fixed" @input="onInput" @blur="onBlur" @focus="onFocus" @confirm="onConfirm"
                     @tap="fieldClick" />
                    <input
                        v-else
                        :style="[fieldStyle]"
                        :type="type"
                        class="u-flex-1 u-field__input-wrap"
                        :value="value"
                        :password="password || this.type === 'password'"
                        :placeholder="placeholder"
                        :placeholderStyle="placeholderStyle"
                        :disabled="disabled"
                        :maxlength="inputMaxlength"
                        :focus="focus"
                        :confirmType="confirmType"
                        @focus="onFocus"
                        @blur="onBlur"
                        @input="onInput"
                        @confirm="onConfirm"
                        @tap="fieldClick"
                    />
                </view>
                <u-icon :size="clearSize" v-if="clearable && value != '' && focused" name="close-circle-fill" color="#c0c4cc" class="u-clear-icon" @click="onClear"/>
                <view class="u-button-wrap"><slot name="right" /></view>
                <u-icon v-if="rightIcon" @click="rightIconClick" :name="rightIcon" color="#c0c4cc" :style="[rightIconStyle]" size="26" class="u-arror-right" />
            </view>
        </view>
        <view v-if="errorMessage !== false && errorMessage != ''" class="u-error-message" :style="{
            paddingLeft: labelWidth + 'rpx'
        }">{{ errorMessage }}</view>
    </view>
</template>
<script>
/**
 * field 输入框
 * @description 借助此组件,可以实现表单的输入, 有"text"和"textarea"类型的,此外,借助uView的picker和actionSheet组件可以快速实现上拉菜单,时间,地区选择等, 为表单解决方案的利器。
 * @tutorial https://www.uviewui.com/components/field.html
 * @property {String} type 输入框的类型(默认text)
 * @property {String} icon label左边的图标,限uView的图标名称
 * @property {Object} icon-style 左边图标的样式,对象形式
 * @property {Boolean} right-icon 输入框右边的图标名称,限uView的图标名称(默认false)
 * @property {Boolean} required 是否必填,左边您显示红色"*"号(默认false)
 * @property {String} label 输入框左边的文字提示
 * @property {Boolean} password 是否密码输入方式(用点替换文字),type为text时有效(默认false)
 * @property {Boolean} clearable 是否显示右侧清空内容的图标控件(输入框有内容,且获得焦点时才显示),点击可清空输入框内容(默认true)
 * @property {Number String} label-width label的宽度,单位rpx(默认130)
 * @property {String} label-align label的文字对齐方式(默认left)
 * @property {Object} field-style 自定义输入框的样式,对象形式
 * @property {Number | String} clear-size 清除图标的大小,单位rpx(默认30)
 * @property {String} input-align 输入框内容对齐方式(默认left)
 * @property {Boolean} border-bottom 是否显示field的下边框(默认true)
 * @property {Boolean} border-top 是否显示field的上边框(默认false)
 * @property {String} icon-color 左边通过icon配置的图标的颜色(默认#606266)
 * @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true)
 * @property {String Boolean} error-message 显示的错误提示内容,如果为空字符串或者false,则不显示错误信息
 * @property {String} placeholder 输入框的提示文字
 * @property {String} placeholder-style placeholder的样式(内联样式,字符串),如"color: #ddd"
 * @property {Boolean} focus 是否自动获得焦点(默认false)
 * @property {Boolean} fixed 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false)
 * @property {Boolean} disabled 是否不可输入(默认false)
 * @property {Number String} maxlength 最大输入长度,设置为 -1 的时候不限制最大长度(默认140)
 * @property {String} confirm-type 设置键盘右下角按钮的文字,仅在type="text"时生效(默认done)
 * @event {Function} input 输入框内容发生变化时触发
 * @event {Function} focus 输入框获得焦点时触发
 * @event {Function} blur 输入框失去焦点时触发
 * @event {Function} confirm 点击完成按钮时触发
 * @event {Function} right-icon-click 通过right-icon生成的图标被点击时触发
 * @event {Function} click 输入框被点击或者通过right-icon生成的图标被点击时触发,这样设计是考虑到传递右边的图标,一般都为需要弹出"picker"等操作时的场景,点击倒三角图标,理应发出此事件,见上方说明
 * @example <u-field v-model="mobile" label="手机号" required :error-message="errorMessage"></u-field>
 */
export default {
    name:"u-field",
    props: {
        icon: String,
        rightIcon: String,
        // arrowDirection: {
        //     type: String,
        //     default: 'right'
        // },
        required: Boolean,
        label: String,
        password: Boolean,
        clearable: {
            type: Boolean,
            default: true
        },
        // 左边标题的宽度单位rpx
        labelWidth: {
            type: [Number, String],
            default: 130
        },
        // 对齐方式,left|center|right
        labelAlign: {
            type: String,
            default: 'left'
        },
        inputAlign: {
            type: String,
            default: 'left'
        },
        iconColor: {
            type: String,
            default: '#606266'
        },
        autoHeight: {
            type: Boolean,
            default: true
        },
        errorMessage: {
            type: [String, Boolean],
            default: ''
        },
        placeholder: String,
        placeholderStyle: String,
        focus: Boolean,
        fixed: Boolean,
        value: [Number, String],
        type: {
            type: String,
            default: 'text'
        },
        disabled: {
            type: Boolean,
            default: false
        },
        maxlength: {
            type: [Number, String],
            default: 140
        },
        confirmType: {
            type: String,
            default: 'done'
        },
        // lable的位置,可选为 left-左边,top-上边
        labelPosition: {
            type: String,
            default: 'left'
        },
        // 输入框的自定义样式
        fieldStyle: {
            type: Object,
            default() {
                return {}
            }
        },
        // 清除按钮的大小
        clearSize: {
            type: [Number, String],
            default: 30
        },
        // lable左边的图标样式,对象形式
        iconStyle: {
            type: Object,
            default() {
                return {}
            }
        },
        // 是否显示上边框
        borderTop: {
            type: Boolean,
            default: false
        },
        // 是否显示下边框
        borderBottom: {
            type: Boolean,
            default: true
        },
        // 是否自动去除两端的空格
        trim: {
            type: Boolean,
            default: true
        }
    },
    data() {
        return {
            focused: false,
            itemIndex: 0,
        };
    },
    computed: {
        inputWrapStyle() {
            let style = {};
            style.textAlign = this.inputAlign;
            // 判断lable的位置,如果是left的话,让input左边两边有间隙
            if(this.labelPosition == 'left') {
                style.margin = `0 8rpx`;
            } else {
                // 如果lable是top的,input的左边就没必要有间隙了
                style.marginRight = `8rpx`;
            }
            return style;
        },
        rightIconStyle() {
            let style = {};
            if (this.arrowDirection == 'top') style.transform = 'roate(-90deg)';
            if (this.arrowDirection == 'bottom') style.transform = 'roate(90deg)';
            else style.transform = 'roate(0deg)';
            return style;
        },
        labelStyle() {
            let style = {};
            if(this.labelAlign == 'left') style.justifyContent = 'flext-start';
            if(this.labelAlign == 'center') style.justifyContent = 'center';
            if(this.labelAlign == 'right') style.justifyContent = 'flext-end';
            return style;
        },
        // uni不支持在computed中写style.justifyContent = 'center'的形式,故用此方法
        justifyContent() {
            if(this.labelAlign == 'left') return 'flex-start';
            if(this.labelAlign == 'center') return 'center';
            if(this.labelAlign == 'right') return 'flex-end';
        },
        // 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值
        inputMaxlength() {
            return Number(this.maxlength)
        },
        // label的位置
        fieldInnerStyle() {
            let style = {};
            if(this.labelPosition == 'left') {
                style.flexDirection = 'row';
            } else {
                style.flexDirection = 'column';
            }
            return style;
        }
    },
    methods: {
        onInput(event) {
            let value = event.detail.value;
            // 判断是否去除空格
            if(this.trim) value = this.$u.trim(value);
            this.$emit('input', value);
        },
        onFocus(event) {
            this.focused = true;
            this.$emit('focus', event);
        },
        onBlur(event) {
            // 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错
            // 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时
            setTimeout(() => {
                this.focused = false;
            }, 100)
            this.$emit('blur', event);
        },
        onConfirm(e) {
            this.$emit('confirm', e.detail.value);
        },
        onClear(event) {
            this.$emit('input', '');
        },
        rightIconClick() {
            this.$emit('right-icon-click');
            this.$emit('click');
        },
        fieldClick() {
            this.$emit('click');
        }
    }
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-field {
    font-size: 28rpx;
    padding: 20rpx 28rpx;
    text-align: left;
    position: relative;
    color: $u-main-color;
}
.u-field-inner {
    @include vue-flex;
    align-items: center;
}
.u-textarea-inner {
    align-items: flex-start;
}
.u-textarea-class {
    min-height: 96rpx;
    width: auto;
    font-size: 28rpx;
}
.fild-body {
    @include vue-flex;
    flex: 1;
    align-items: center;
}
.u-arror-right {
    margin-left: 8rpx;
}
.u-label-text {
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
}
.u-label-left-gap {
    margin-left: 6rpx;
}
.u-label-postion-top {
    flex-direction: column;
    align-items: flex-start;
}
.u-label {
    width: 130rpx;
    flex: 1 1 130rpx;
    text-align: left;
    position: relative;
    @include vue-flex;
    align-items: center;
}
.u-required::before {
    content: '*';
    position: absolute;
    left: -16rpx;
    font-size: 14px;
    color: $u-type-error;
    height: 9px;
    line-height: 1;
}
.u-field__input-wrap {
    position: relative;
    overflow: hidden;
    font-size: 28rpx;
    height: 48rpx;
    flex: 1;
    width: auto;
}
.u-clear-icon {
    @include vue-flex;
    align-items: center;
}
.u-error-message {
    color: $u-type-error;
    font-size: 26rpx;
    text-align: left;
}
.placeholder-style {
    color: rgb(150, 151, 153);
}
.u-input-class {
    font-size: 28rpx;
}
.u-button-wrap {
    margin-left: 8rpx;
}
</style>
uview-ui/components/u-form-item/u-form-item.vue
New file
@@ -0,0 +1,431 @@
<template>
    <view class="u-form-item" :class="{'u-border-bottom': elBorderBottom, 'u-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom')}">
        <view class="u-form-item__body" :style="{
            flexDirection: elLabelPosition == 'left' ? 'row' : 'column'
        }">
            <!-- 微信小程序中,将一个参数设置空字符串,结果会变成字符串"true" -->
            <view class="u-form-item--left" :style="{
                width: uLabelWidth,
                flex: `0 0 ${uLabelWidth}`,
                marginBottom: elLabelPosition == 'left' ? 0 : '10rpx',
            }">
                <!-- 为了块对齐 -->
                <view class="u-form-item--left__content" v-if="required || leftIcon || label">
                    <!-- nvue不支持伪元素before -->
                    <text v-if="required" class="u-form-item--left__content--required">*</text>
                    <view class="u-form-item--left__content__icon" v-if="leftIcon">
                        <u-icon :name="leftIcon" :custom-style="leftIconStyle"></u-icon>
                    </view>
                    <view class="u-form-item--left__content__label" :style="[elLabelStyle, {
                        'justify-content': elLabelAlign == 'left' ? 'flex-start' : elLabelAlign == 'center' ? 'center' : 'flex-end'
                    }]">
                        {{label}}
                    </view>
                </view>
            </view>
            <view class="u-form-item--right u-flex">
                <view class="u-form-item--right__content">
                    <view class="u-form-item--right__content__slot ">
                        <slot />
                    </view>
                    <view class="u-form-item--right__content__icon u-flex" v-if="$slots.right || rightIcon">
                        <u-icon :custom-style="rightIconStyle" v-if="rightIcon" :name="rightIcon"></u-icon>
                        <slot name="right" />
                    </view>
                </view>
            </view>
        </view>
        <view class="u-form-item__message" v-if="validateState === 'error' && showError('message')" :style="{
            paddingLeft: elLabelPosition == 'left' ? $u.addUnit(elLabelWidth) : '0',
        }">{{validateMessage}}</view>
    </view>
</template>
<script>
    import Emitter from '../../libs/util/emitter.js';
    import schema from '../../libs/util/async-validator';
    // 去除警告信息
    schema.warning = function() {};
    /**
     * form-item 表单item
     * @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。
     * @tutorial http://uviewui.com/components/form.html
     * @property {String} label 左侧提示文字
     * @property {Object} prop 表单域model对象的属性名,在使用 validate、resetFields 方法的情况下,该属性是必填的
     * @property {Boolean} border-bottom 是否显示表单域的下划线边框
     * @property {String} label-position 表单域提示文字的位置,left-左侧,top-上方
     * @property {String Number} label-width 提示文字的宽度,单位rpx(默认90)
     * @property {Object} label-style lable的样式,对象形式
     * @property {String} label-align lable的对齐方式
     * @property {String} right-icon 右侧自定义字体图标(限uView内置图标)或图片地址
     * @property {String} left-icon 左侧自定义字体图标(限uView内置图标)或图片地址
     * @property {Object} left-icon-style 左侧图标的样式,对象形式
     * @property {Object} right-icon-style 右侧图标的样式,对象形式
     * @property {Boolean} required 是否显示左边的"*"号,这里仅起展示作用,如需校验必填,请通过rules配置必填规则(默认false)
     * @example <u-form-item label="姓名"><u-input v-model="form.name" /></u-form-item>
     */
    export default {
        name: 'u-form-item',
        mixins: [Emitter],
        inject: {
            uForm: {
                default () {
                    return null
                }
            }
        },
        props: {
            // input的label提示语
            label: {
                type: String,
                default: ''
            },
            // 绑定的值
            prop: {
                type: String,
                default: ''
            },
            // 是否显示表单域的下划线边框
            borderBottom: {
                type: [String, Boolean],
                default: ''
            },
            // label的位置,left-左边,top-上边
            labelPosition: {
                type: String,
                default: ''
            },
            // label的宽度,单位rpx
            labelWidth: {
                type: [String, Number],
                default: ''
            },
            // lable的样式,对象形式
            labelStyle: {
                type: Object,
                default () {
                    return {}
                }
            },
            // lable字体的对齐方式
            labelAlign: {
                type: String,
                default: ''
            },
            // 右侧图标
            rightIcon: {
                type: String,
                default: ''
            },
            // 左侧图标
            leftIcon: {
                type: String,
                default: ''
            },
            // 左侧图标的样式
            leftIconStyle: {
                type: Object,
                default () {
                    return {}
                }
            },
            // 左侧图标的样式
            rightIconStyle: {
                type: Object,
                default () {
                    return {}
                }
            },
            // 是否显示左边的必填星号,只作显示用,具体校验必填的逻辑,请在rules中配置
            required: {
                type: Boolean,
                default: false
            }
        },
        data() {
            return {
                initialValue: '', // 存储的默认值
                // isRequired: false, // 是否必填,由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成
                validateState: '', // 是否校验成功
                validateMessage: '', // 校验失败的提示语
                // 有错误时的提示方式,message-提示信息,border-如果input设置了边框,变成呈红色,
                errorType: ['message'],
                fieldValue: '', // 获取当前子组件input的输入的值
                // 父组件的参数,在computed计算中,无法得知this.parent发生变化,故将父组件的参数值,放到data中
                parentData: {
                    borderBottom: true,
                    labelWidth: 90,
                    labelPosition: 'left',
                    labelStyle: {},
                    labelAlign: 'left',
                }
            };
        },
        watch: {
            validateState(val) {
                this.broadcastInputError();
            },
            // 监听u-form组件的errorType的变化
            "uForm.errorType"(val) {
                this.errorType = val;
                this.broadcastInputError();
            },
        },
        computed: {
            // 计算后的label宽度,由于需要多个判断,故放到computed中
            uLabelWidth() {
                // 如果用户设置label为空字符串(微信小程序空字符串最终会变成字符串的'true'),意味着要将label的位置宽度设置为auto
                return this.elLabelPosition == 'left' ? (this.label === 'true' || this.label === '' ? 'auto' : this.$u.addUnit(this
                    .elLabelWidth)) : '100%';
            },
            showError() {
                return type => {
                    // 如果errorType数组中含有none,或者toast提示类型
                    if (this.errorType.indexOf('none') >= 0) return false;
                    else if (this.errorType.indexOf(type) >= 0) return true;
                    else return false;
                }
            },
            // label的宽度
            elLabelWidth() {
                // label默认宽度为90,优先使用本组件的值,如果没有(如果设置为0,也算是配置了值,依然起效),则用u-form的值
                return (this.labelWidth != 0 || this.labelWidth != '') ? this.labelWidth : (this.parentData.labelWidth ? this.parentData
                    .labelWidth :
                    90);
            },
            // label的样式
            elLabelStyle() {
                return Object.keys(this.labelStyle).length ? this.labelStyle : (this.parentData.labelStyle ? this.parentData.labelStyle :
                    {});
            },
            // label的位置,左侧或者上方
            elLabelPosition() {
                return this.labelPosition ? this.labelPosition : (this.parentData.labelPosition ? this.parentData.labelPosition :
                    'left');
            },
            // label的对齐方式
            elLabelAlign() {
                return this.labelAlign ? this.labelAlign : (this.parentData.labelAlign ? this.parentData.labelAlign : 'left');
            },
            // label的下划线
            elBorderBottom() {
                // 子组件的borderBottom默认为空字符串,如果不等于空字符串,意味着子组件设置了值,优先使用子组件的值
                return this.borderBottom !== '' ? this.borderBottom : this.parentData.borderBottom ? this.parentData.borderBottom :
                    true;
            }
        },
        methods: {
            broadcastInputError() {
                // 子组件发出事件,第三个参数为true或者false,true代表有错误
                this.broadcast('u-input', 'on-form-item-error', this.validateState === 'error' && this.showError('border'));
            },
            // 判断是否需要required校验
            setRules() {
                let that = this;
                // 由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成
                // 从父组件u-form拿到当前u-form-item需要验证 的规则
                // let rules = this.getRules();
                // if (rules.length) {
                //     this.isRequired = rules.some(rule => {
                //         // 如果有必填项,就返回,没有的话,就是undefined
                //         return rule.required;
                //     });
                // }
                // blur事件
                this.$on('on-form-blur', that.onFieldBlur);
                // change事件
                this.$on('on-form-change', that.onFieldChange);
            },
            // 从u-form的rules属性中,取出当前u-form-item的校验规则
            getRules() {
                // 父组件的所有规则
                let rules = this.parent.rules;
                rules = rules ? rules[this.prop] : [];
                // 保证返回的是一个数组形式
                return [].concat(rules || []);
            },
            // blur事件时进行表单校验
            onFieldBlur() {
                this.validation('blur');
            },
            // change事件进行表单校验
            onFieldChange() {
                this.validation('change');
            },
            // 过滤出符合要求的rule规则
            getFilteredRule(triggerType = '') {
                let rules = this.getRules();
                // 整体验证表单时,triggerType为空字符串,此时返回所有规则进行验证
                if (!triggerType) return rules;
                // 历遍判断规则是否有对应的事件,比如blur,change触发等的事件
                // 使用indexOf判断,是因为某些时候设置的验证规则的trigger属性可能为多个,比如['blur','change']
                // 某些场景可能的判断规则,可能不存在trigger属性,故先判断是否存在此属性
                return rules.filter(res => res.trigger && res.trigger.indexOf(triggerType) !== -1);
            },
            // 校验数据
            validation(trigger, callback = () => {}) {
                // 检验之间,先获取需要校验的值
                this.fieldValue = this.parent.model[this.prop];
                // blur和change是否有当前方式的校验规则
                let rules = this.getFilteredRule(trigger);
                // 判断是否有验证规则,如果没有规则,也调用回调方法,否则父组件u-form会因为
                // 对count变量的统计错误而无法进入上一层的回调
                if (!rules || rules.length === 0) {
                    return callback('');
                }
                // 设置当前的装填,标识为校验中
                this.validateState = 'validating';
                // 调用async-validator的方法
                let validator = new schema({
                    [this.prop]: rules
                });
                validator.validate({
                    [this.prop]: this.fieldValue
                }, {
                    firstFields: true
                }, (errors, fields) => {
                    // 记录状态和报错信息
                    this.validateState = !errors ? 'success' : 'error';
                    this.validateMessage = errors ? errors[0].message : '';
                    // 调用回调方法
                    callback(this.validateMessage);
                });
            },
            // 清空当前的u-form-item
            resetField() {
                this.parent.model[this.prop] = this.initialValue;
                // 设置为`success`状态,只是为了清空错误标记
                this.validateState = 'success';
            }
        },
        // 组件创建完成时,将当前实例保存到u-form中
        mounted() {
            // 支付宝、头条小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
            this.parent = this.$u.$parent.call(this, 'u-form');
            if (this.parent) {
                // 历遍parentData中的属性,将parent中的同名属性赋值给parentData
                Object.keys(this.parentData).map(key => {
                    this.parentData[key] = this.parent[key];
                });
                // 如果没有传入prop,或者uForm为空(如果u-form-input单独使用,就不会有uForm注入),就不进行校验
                if (this.prop) {
                    // 将本实例添加到父组件中
                    this.parent.fields.push(this);
                    this.errorType = this.parent.errorType;
                    // 设置初始值
                    this.initialValue = this.fieldValue;
                    // 添加表单校验,这里必须要写在$nextTick中,因为u-form的rules是通过ref手动传入的
                    // 不在$nextTick中的话,可能会造成执行此处代码时,父组件还没通过ref把规则给u-form,导致规则为空
                    this.$nextTick(() => {
                        this.setRules();
                    })
                }
            }
        },
        // 组件销毁前,将实例从u-form的缓存中移除
        beforeDestroy() {
            // 如果当前没有prop的话表示当前不要进行删除(因为没有注入)
            if (this.parent && this.prop) {
                this.parent.fields.map((item, index) => {
                    if (item === this) this.parent.fields.splice(index, 1);
                })
            }
        },
    };
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-form-item {
        @include vue-flex;
        // align-items: flex-start;
        padding: 20rpx 0;
        font-size: 28rpx;
        color: $u-main-color;
        box-sizing: border-box;
        line-height: $u-form-item-height;
        flex-direction: column;
        &__border-bottom--error:after {
            border-color: $u-type-error;
        }
        &__body {
            @include vue-flex;
        }
        &--left {
            @include vue-flex;
            align-items: center;
            &__content {
                position: relative;
                @include vue-flex;
                align-items: center;
                padding-right: 10rpx;
                flex: 1;
                &__icon {
                    margin-right: 8rpx;
                }
                &--required {
                    position: absolute;
                    left: -16rpx;
                    vertical-align: middle;
                    color: $u-type-error;
                    padding-top: 6rpx;
                }
                &__label {
                    @include vue-flex;
                    align-items: center;
                    flex: 1;
                }
            }
        }
        &--right {
            flex: 1;
            &__content {
                @include vue-flex;
                align-items: center;
                flex: 1;
                &__slot {
                    flex: 1;
                    /* #ifndef MP */
                    @include vue-flex;
                    align-items: center;
                    /* #endif */
                }
                &__icon {
                    margin-left: 10rpx;
                    color: $u-light-color;
                    font-size: 30rpx;
                }
            }
        }
        &__message {
            font-size: 24rpx;
            line-height: 24rpx;
            color: $u-type-error;
            margin-top: 12rpx;
        }
    }
</style>
uview-ui/components/u-form/u-form.vue
New file
@@ -0,0 +1,134 @@
<template>
    <view class="u-form"><slot /></view>
</template>
<script>
    /**
     * form 表单
     * @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。
     * @tutorial http://uviewui.com/components/form.html
     * @property {Object} model 表单数据对象
     * @property {Boolean} border-bottom 是否显示表单域的下划线边框
     * @property {String} label-position 表单域提示文字的位置,left-左侧,top-上方
     * @property {String Number} label-width 提示文字的宽度,单位rpx(默认90)
     * @property {Object} label-style lable的样式,对象形式
     * @property {String} label-align lable的对齐方式
     * @property {Object} rules 通过ref设置,见官网说明
     * @property {Array} error-type 错误的提示方式,数组形式,见上方说明(默认['message'])
     * @example <u-form :model="form" ref="uForm"></u-form>
     */
export default {
    name: 'u-form',
    props: {
        // 当前form的需要验证字段的集合
        model: {
            type: Object,
            default() {
                return {};
            }
        },
        // 验证规则
        // rules: {
        //     type: [Object, Function, Array],
        //     default() {
        //         return {};
        //     }
        // },
        // 有错误时的提示方式,message-提示信息,border-如果input设置了边框,变成呈红色,
        // border-bottom-下边框呈现红色,none-无提示
        errorType: {
            type: Array,
            default() {
                return ['message', 'toast']
            }
        },
        // 是否显示表单域的下划线边框
        borderBottom: {
            type: Boolean,
            default: true
        },
        // label的位置,left-左边,top-上边
        labelPosition: {
            type: String,
            default: 'left'
        },
        // label的宽度,单位rpx
        labelWidth: {
            type: [String, Number],
            default: 90
        },
        // lable字体的对齐方式
        labelAlign: {
            type: String,
            default: 'left'
        },
        // lable的样式,对象形式
        labelStyle: {
            type: Object,
            default() {
                return {}
            }
        },
    },
    provide() {
        return {
            uForm: this
        };
    },
    data() {
        return {
            rules: {}
        };
    },
    created() {
        // 存储当前form下的所有u-form-item的实例
        // 不能定义在data中,否则微信小程序会造成循环引用而报错
        this.fields = [];
    },
    methods: {
        setRules(rules) {
            this.rules = rules;
        },
        // 清空所有u-form-item组件的内容,本质上是调用了u-form-item组件中的resetField()方法
        resetFields() {
            this.fields.map(field => {
                field.resetField();
            });
        },
        // 校验全部数据
        validate(callback) {
            return new Promise(resolve => {
                // 对所有的u-form-item进行校验
                let valid = true; // 默认通过
                let count = 0; // 用于标记是否检查完毕
                let errorArr = []; // 存放错误信息
                this.fields.map(field => {
                    // 调用每一个u-form-item实例的validation的校验方法
                    field.validation('', error => {
                        // 如果任意一个u-form-item校验不通过,就意味着整个表单不通过
                        if (error) {
                            valid = false;
                            errorArr.push(error);
                        }
                        // 当历遍了所有的u-form-item时,调用promise的then方法
                        if (++count === this.fields.length) {
                            resolve(valid); // 进入promise的then方法
                            // 判断是否设置了toast的提示方式,只提示最前面的表单域的第一个错误信息
                            if(this.errorType.indexOf('none') === -1 && this.errorType.indexOf('toast') >= 0 && errorArr.length) {
                                this.$u.toast(errorArr[0]);
                            }
                            // 调用回调方法
                            if (typeof callback == 'function') callback(valid);
                        }
                    });
                });
            });
        }
    }
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
</style>
uview-ui/components/u-full-screen/u-full-screen.vue
New file
@@ -0,0 +1,52 @@
<template>
    <u-modal v-model="show" :show-cancel-button="true" confirm-text="升级" title="发现新版本" @cancel="cancel" @confirm="confirm">
        <view class="u-update-content">
            <rich-text :nodes="content"></rich-text>
        </view>
    </u-modal>
</template>
<script>
    export default {
        data() {
            return {
                show: false,
                content: `
                    1. 修复badge组件的size参数无效问题<br>
                    2. 新增Modal模态框组件<br>
                    3. 新增压窗屏组件,可以在APP上以弹窗的形式遮盖导航栏和底部tabbar<br>
                    4. 修复键盘组件在微信小程序上遮罩无效的问题
                `,
            }
        },
        onReady() {
            this.show = true;
        },
        methods: {
            cancel() {
                this.closeModal();
            },
            confirm() {
                this.closeModal();
            },
            closeModal() {
                uni.navigateBack();
            }
        }
    }
</script>
<style scoped lang="scss">
    @import "../../libs/css/style.components.scss";
    .u-full-content {
        background-color: #00C777;
    }
    .u-update-content {
        font-size: 26rpx;
        color: $u-content-color;
        line-height: 1.7;
        padding: 30rpx;
    }
</style>
uview-ui/components/u-gap/u-gap.vue
New file
@@ -0,0 +1,54 @@
<template>
    <view class="u-gap" :style="[gapStyle]"></view>
</template>
<script>
/**
 * gap 间隔槽
 * @description 该组件一般用于内容块之间的用一个灰色块隔开的场景,方便用户风格统一,减少工作量
 * @tutorial https://www.uviewui.com/components/gap.html
 * @property {String} bg-color 背景颜色(默认#f3f4f6)
 * @property {String Number} height 分割槽高度,单位rpx(默认30)
 * @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0)
 * @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0)
 * @example <u-gap height="80" bg-color="#bbb"></u-gap>
 */
export default {
    name: "u-gap",
    props: {
        bgColor: {
            type: String,
            default: 'transparent ' // 背景透明
        },
        // 高度
        height: {
            type: [String, Number],
            default: 30
        },
        // 与上一个组件的距离
        marginTop: {
            type: [String, Number],
            default: 0
        },
        // 与下一个组件的距离
        marginBottom: {
            type: [String, Number],
            default: 0
        },
    },
    computed: {
        gapStyle() {
            return {
                backgroundColor: this.bgColor,
                height: this.height + 'rpx',
                marginTop: this.marginTop + 'rpx',
                marginBottom: this.marginBottom + 'rpx'
            };
        }
    }
};
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
</style>
uview-ui/components/u-grid-item/u-grid-item.vue
New file
@@ -0,0 +1,126 @@
<template>
    <view class="u-grid-item" :hover-class="parentData.hoverClass"
     :hover-stay-time="200" @tap="click" :style="{
            background: bgColor,
            width: width,
        }">
        <view class="u-grid-item-box" :style="[customStyle]" :class="[parentData.border ? 'u-border-right u-border-bottom' : '']">
            <slot />
        </view>
    </view>
</template>
<script>
    /**
     * gridItem 提示
     * @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。搭配u-grid使用
     * @tutorial https://www.uviewui.com/components/grid.html
     * @property {String} bg-color 宫格的背景颜色(默认#ffffff)
     * @property {String Number} index 点击宫格时,返回的值
     * @property {Object} custom-style 自定义样式,对象形式
     * @event {Function} click 点击宫格触发
     * @example <u-grid-item></u-grid-item>
     */
    export default {
        name: "u-grid-item",
        props: {
            // 背景颜色
            bgColor: {
                type: String,
                default: '#ffffff'
            },
            // 点击时返回的index
            index: {
                type: [Number, String],
                default: ''
            },
            // 自定义样式,对象形式
            customStyle: {
                type: Object,
                default() {
                    return {
                        padding: '30rpx 0'
                    }
                }
            }
        },
        data() {
            return {
                parentData: {
                    hoverClass: '', // 按下去的时候,是否显示背景灰色
                    col: 3, // 父组件划分的宫格数
                    border: true, // 是否显示边框,根据父组件决定
                }
            };
        },
        created() {
            // 父组件的实例
            this.updateParentData();
            // this.parent在updateParentData()中定义
            this.parent.children.push(this);
        },
        computed: {
            // 每个grid-item的宽度
            width() {
                return 100 / Number(this.parentData.col) + '%';
            },
        },
        methods: {
            // 获取父组件的参数
            updateParentData() {
                // 此方法写在mixin中
                this.getParentData('u-grid');
            },
            click() {
                this.$emit('click', this.index);
                this.parent && this.parent.click(this.index);
            }
        }
    };
</script>
<style scoped lang="scss">
    @import "../../libs/css/style.components.scss";
    .u-grid-item {
        box-sizing: border-box;
        background: #fff;
        @include vue-flex;
        align-items: center;
        justify-content: center;
        position: relative;
        flex-direction: column;
        /* #ifdef MP */
        position: relative;
        float: left;
        /* #endif */
    }
    .u-grid-item-hover {
        background: #f7f7f7 !important;
    }
    .u-grid-marker-box {
        position: absolute;
        /* #ifndef APP-NVUE */
        display: inline-flex;
        /* #endif */
        line-height: 0;
    }
    .u-grid-marker-wrap {
        position: absolute;
    }
    .u-grid-item-box {
        padding: 30rpx 0;
        @include vue-flex;
        align-items: center;
        justify-content: center;
        flex-direction: column;
        flex: 1;
        width: 100%;
        height: 100%;
    }
</style>
uview-ui/components/u-grid/u-grid.vue
New file
@@ -0,0 +1,108 @@
<template>
    <view class="u-grid" :class="{'u-border-top u-border-left': border}" :style="[gridStyle]"><slot /></view>
</template>
<script>
/**
 * grid 宫格布局
 * @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。
 * @tutorial https://www.uviewui.com/components/grid.html
 * @property {String Number} col 宫格的列数(默认3)
 * @property {Boolean} border 是否显示宫格的边框(默认true)
 * @property {Boolean} hover-class 点击宫格的时候,是否显示按下的灰色背景(默认false)
 * @event {Function} click 点击宫格触发
 * @example <u-grid :col="3" @click="click"></u-grid>
 */
export default {
    name: 'u-grid',
    props: {
        // 分成几列
        col: {
            type: [Number, String],
            default: 3
        },
        // 是否显示边框
        border: {
            type: Boolean,
            default: true
        },
        // 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右
        align: {
            type: String,
            default: 'left'
        },
        // 宫格按压时的样式类,"none"为无效果
        hoverClass: {
            type: String,
            default: 'u-hover-class'
        }
    },
    data() {
        return {
            index: 0,
        }
    },
    watch: {
        // 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件
        parentData() {
            if(this.children.length) {
                this.children.map(child => {
                    // 判断子组件(u-radio)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
                    typeof(child.updateParentData) == 'function' && child.updateParentData();
                })
            }
        },
    },
    created() {
        // 如果将children定义在data中,在微信小程序会造成循环引用而报错
        this.children = [];
    },
    computed: {
        // 计算父组件的值是否发生变化
        parentData() {
            return [this.hoverClass, this.col, this.size, this.border];
        },
        // 宫格对齐方式
        gridStyle() {
            let style = {};
            switch(this.align) {
                case 'left':
                    style.justifyContent = 'flex-start';
                    break;
                case 'center':
                    style.justifyContent = 'center';
                    break;
                case 'right':
                    style.justifyContent = 'flex-end';
                    break;
                default: style.justifyContent = 'flex-start';
            };
            return style;
        }
    },
    methods: {
        click(index) {
            this.$emit('click', index);
        }
    }
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-grid {
    width: 100%;
    /* #ifdef MP */
    position: relative;
    box-sizing: border-box;
    overflow: hidden;
    /* #endif */
    /* #ifndef MP */
    @include vue-flex;
    flex-wrap: wrap;
    align-items: center;
    /* #endif */
}
</style>
uview-ui/components/u-icon/u-icon.vue
New file
@@ -0,0 +1,336 @@
<template>
    <view :style="[customStyle]" class="u-icon" @tap="click" :class="['u-icon--' + labelPos]">
        <image class="u-icon__img" v-if="isImg" :src="name" :mode="imgMode" :style="[imgStyle]"></image>
        <text v-else class="u-icon__icon" :class="customClass" :style="[iconStyle]" :hover-class="hoverClass"
              @touchstart="touchstart">
            <text v-if="showDecimalIcon" :style="[decimalIconStyle]" :class="decimalIconClass" :hover-class="hoverClass"
                  class="u-icon__decimal">
            </text>
        </text>
        <!-- 这里进行空字符串判断,如果仅仅是v-if="label",可能会出现传递0的时候,结果也无法显示 -->
        <text v-if="label !== ''" class="u-icon__label" :style="{
            color: labelColor,
            fontSize: $u.addUnit(labelSize),
            marginLeft: labelPos == 'right' ? $u.addUnit(marginLeft) : 0,
            marginTop: labelPos == 'bottom' ? $u.addUnit(marginTop) : 0,
            marginRight: labelPos == 'left' ? $u.addUnit(marginRight) : 0,
            marginBottom: labelPos == 'top' ? $u.addUnit(marginBottom) : 0,
        }">{{ label }}
        </text>
    </view>
</template>
<script>
/**
 * icon 图标
 * @description 基于字体的图标集,包含了大多数常见场景的图标。
 * @tutorial https://www.uviewui.com/components/icon.html
 * @property {String} name 图标名称,见示例图标集
 * @property {String} color 图标颜色(默认inherit)
 * @property {String | Number} size 图标字体大小,单位rpx(默认32)
 * @property {String | Number} label-size label字体大小,单位rpx(默认28)
 * @property {String} label 图标右侧的label文字(默认28)
 * @property {String} label-pos label文字相对于图标的位置,只能right或bottom(默认right)
 * @property {String} label-color label字体颜色(默认#606266)
 * @property {Object} custom-style icon的样式,对象形式
 * @property {String} custom-prefix 自定义字体图标库时,需要写上此值
 * @property {String | Number} margin-left label在右侧时与图标的距离,单位rpx(默认6)
 * @property {String | Number} margin-top label在下方时与图标的距离,单位rpx(默认6)
 * @property {String | Number} margin-bottom label在上方时与图标的距离,单位rpx(默认6)
 * @property {String | Number} margin-right label在左侧时与图标的距离,单位rpx(默认6)
 * @property {String} label-pos label相对于图标的位置,只能right或bottom(默认right)
 * @property {String} index 一个用于区分多个图标的值,点击图标时通过click事件传出
 * @property {String} hover-class 图标按下去的样式类,用法同uni的view组件的hover-class参数,详情见官网
 * @property {String} width 显示图片小图标时的宽度
 * @property {String} height 显示图片小图标时的高度
 * @property {String} top 图标在垂直方向上的定位
 * @property {String} top 图标在垂直方向上的定位
 * @property {String} top 图标在垂直方向上的定位
 * @property {Boolean} show-decimal-icon 是否为DecimalIcon
 * @property {String} inactive-color 背景颜色,可接受主题色,仅Decimal时有效
 * @property {String | Number} percent 显示的百分比,仅Decimal时有效
 * @event {Function} click 点击图标时触发
 * @example <u-icon name="photo" color="#2979ff" size="28"></u-icon>
 */
export default {
    name: 'u-icon',
    props: {
        // 图标类名
        name: {
            type: String,
            default: ''
        },
        // 图标颜色,可接受主题色
        color: {
            type: String,
            default: ''
        },
        // 字体大小,单位rpx
        size: {
            type: [Number, String],
            default: 'inherit'
        },
        // 是否显示粗体
        bold: {
            type: Boolean,
            default: false
        },
        // 点击图标的时候传递事件出去的index(用于区分点击了哪一个)
        index: {
            type: [Number, String],
            default: ''
        },
        // 触摸图标时的类名
        hoverClass: {
            type: String,
            default: ''
        },
        // 自定义扩展前缀,方便用户扩展自己的图标库
        customPrefix: {
            type: String,
            default: 'uicon'
        },
        // 图标右边或者下面的文字
        label: {
            type: [String, Number],
            default: ''
        },
        // label的位置,只能右边或者下边
        labelPos: {
            type: String,
            default: 'right'
        },
        // label的大小
        labelSize: {
            type: [String, Number],
            default: '28'
        },
        // label的颜色
        labelColor: {
            type: String,
            default: '#606266'
        },
        // label与图标的距离(横向排列)
        marginLeft: {
            type: [String, Number],
            default: '6'
        },
        // label与图标的距离(竖向排列)
        marginTop: {
            type: [String, Number],
            default: '6'
        },
        // label与图标的距离(竖向排列)
        marginRight: {
            type: [String, Number],
            default: '6'
        },
        // label与图标的距离(竖向排列)
        marginBottom: {
            type: [String, Number],
            default: '6'
        },
        // 图片的mode
        imgMode: {
            type: String,
            default: 'widthFix'
        },
        // 自定义样式
        customStyle: {
            type: Object,
            default() {
                return {}
            }
        },
        // 用于显示图片小图标时,图片的宽度
        width: {
            type: [String, Number],
            default: ''
        },
        // 用于显示图片小图标时,图片的高度
        height: {
            type: [String, Number],
            default: ''
        },
        // 用于解决某些情况下,让图标垂直居中的用途
        top: {
            type: [String, Number],
            default: 0
        },
        // 是否为DecimalIcon
        showDecimalIcon: {
            type: Boolean,
            default: false
        },
        // 背景颜色,可接受主题色,仅Decimal时有效
        inactiveColor: {
            type: String,
            default: '#ececec'
        },
        // 显示的百分比,仅Decimal时有效
        percent: {
            type: [Number, String],
            default: '50'
        }
    },
    computed: {
        customClass() {
            let classes = []
            classes.push(this.customPrefix + '-' + this.name)
            // uView的自定义图标类名为u-iconfont
            if (this.customPrefix == 'uicon') {
                classes.push('u-iconfont')
            } else {
                classes.push(this.customPrefix)
            }
            // 主题色,通过类配置
            if (this.showDecimalIcon && this.inactiveColor && this.$u.config.type.includes(this.inactiveColor)) {
                classes.push('u-icon__icon--' + this.inactiveColor)
            } else if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color)
            // 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
            // 故需将其拆成一个字符串的形式,通过空格隔开各个类名
            //#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
            classes = classes.join(' ')
            //#endif
            return classes
        },
        iconStyle() {
            let style = {}
            style = {
                fontSize: this.size == 'inherit' ? 'inherit' : this.$u.addUnit(this.size),
                fontWeight: this.bold ? 'bold' : 'normal',
                // 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
                top: this.$u.addUnit(this.top)
            }
            // 非主题色值时,才当作颜色值
            if (this.showDecimalIcon && this.inactiveColor && !this.$u.config.type.includes(this.inactiveColor)) {
                style.color = this.inactiveColor
            } else if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color
            return style
        },
        // 判断传入的name属性,是否图片路径,只要带有"/"均认为是图片形式
        isImg() {
            return this.name.indexOf('/') !== -1
        },
        imgStyle() {
            let style = {}
            // 如果设置width和height属性,则优先使用,否则使用size属性
            style.width = this.width ? this.$u.addUnit(this.width) : this.$u.addUnit(this.size)
            style.height = this.height ? this.$u.addUnit(this.height) : this.$u.addUnit(this.size)
            return style
        },
        decimalIconStyle() {
            let style = {}
            style = {
                fontSize: this.size == 'inherit' ? 'inherit' : this.$u.addUnit(this.size),
                fontWeight: this.bold ? 'bold' : 'normal',
                // 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
                top: this.$u.addUnit(this.top),
                width: this.percent + '%'
            }
            // 非主题色值时,才当作颜色值
            if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color
            return style
        },
        decimalIconClass() {
            let classes = []
            classes.push(this.customPrefix + '-' + this.name)
            // uView的自定义图标类名为u-iconfont
            if (this.customPrefix == 'uicon') {
                classes.push('u-iconfont')
            } else {
                classes.push(this.customPrefix)
            }
            // 主题色,通过类配置
            if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color)
            else classes.push('u-icon__icon--primary')
            // 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
            // 故需将其拆成一个字符串的形式,通过空格隔开各个类名
            //#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
            classes = classes.join(' ')
            //#endif
            return classes
        }
    },
    methods: {
        click() {
            this.$emit('click', this.index)
        },
        touchstart() {
            this.$emit('touchstart', this.index)
        }
    }
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
@import '../../iconfont.css';
.u-icon {
    display: inline-flex;
    align-items: center;
    &--left {
        flex-direction: row-reverse;
        align-items: center;
    }
    &--right {
        flex-direction: row;
        align-items: center;
    }
    &--top {
        flex-direction: column-reverse;
        justify-content: center;
    }
    &--bottom {
        flex-direction: column;
        justify-content: center;
    }
    &__icon {
        position: relative;
        &--primary {
            color: $u-type-primary;
        }
        &--success {
            color: $u-type-success;
        }
        &--error {
            color: $u-type-error;
        }
        &--warning {
            color: $u-type-warning;
        }
        &--info {
            color: $u-type-info;
        }
    }
    &__decimal {
        position: absolute;
        top: 0;
        left: 0;
        display: inline-block;
        overflow: hidden;
    }
    &__img {
        height: auto;
        will-change: transform;
    }
    &__label {
        line-height: 1;
    }
}
</style>
uview-ui/components/u-image/u-image.vue
New file
@@ -0,0 +1,267 @@
<template>
    <view class="u-image" @tap="onClick" :style="[wrapStyle, backgroundStyle]">
        <image
            v-if="!isError"
            :src="src"
            :mode="mode"
            @error="onErrorHandler"
            @load="onLoadHandler"
            :lazy-load="lazyLoad"
            class="u-image__image"
            :show-menu-by-longpress="showMenuByLongpress"
            :style="{
                borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius)
            }"
        ></image>
        <view
            v-if="showLoading && loading"
            class="u-image__loading"
            :style="{
                borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius),
                backgroundColor: this.bgColor
            }"
        >
            <slot v-if="$slots.loading" name="loading" />
            <u-icon v-else :name="loadingIcon" :width="width" :height="height"></u-icon>
        </view>
        <view
            v-if="showError && isError && !loading"
            class="u-image__error"
            :style="{
                borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius)
            }"
        >
            <slot v-if="$slots.error" name="error" />
            <u-icon v-else :name="errorIcon" :width="width" :height="height"></u-icon>
        </view>
    </view>
</template>
<script>
/**
 * Image 图片
 * @description 此组件为uni-app的image组件的加强版,在继承了原有功能外,还支持淡入动画、加载中、加载失败提示、圆角值和形状等。
 * @tutorial https://uviewui.com/components/image.html
 * @property {String} src 图片地址
 * @property {String} mode 裁剪模式,见官网说明
 * @property {String | Number} width 宽度,单位任意,如果为数值,则为rpx单位(默认100%)
 * @property {String | Number} height 高度,单位任意,如果为数值,则为rpx单位(默认 auto)
 * @property {String} shape 图片形状,circle-圆形,square-方形(默认square)
 * @property {String | Number} border-radius 圆角值,单位任意,如果为数值,则为rpx单位(默认 0)
 * @property {Boolean} lazy-load 是否懒加载,仅微信小程序、App、百度小程序、字节跳动小程序有效(默认 true)
 * @property {Boolean} show-menu-by-longpress 是否开启长按图片显示识别小程序码菜单,仅微信小程序有效(默认 false)
 * @property {String} loading-icon 加载中的图标,或者小图片(默认 photo)
 * @property {String} error-icon 加载失败的图标,或者小图片(默认 error-circle)
 * @property {Boolean} show-loading 是否显示加载中的图标或者自定义的slot(默认 true)
 * @property {Boolean} show-error 是否显示加载错误的图标或者自定义的slot(默认 true)
 * @property {Boolean} fade 是否需要淡入效果(默认 true)
 * @property {String Number} width 传入图片路径时图片的宽度
 * @property {String Number} height 传入图片路径时图片的高度
 * @property {Boolean} webp 只支持网络资源,只对微信小程序有效(默认 false)
 * @property {String | Number} duration 搭配fade参数的过渡时间,单位ms(默认 500)
 * @event {Function} click 点击图片时触发
 * @event {Function} error 图片加载失败时触发
 * @event {Function} load 图片加载成功时触发
 * @example <u-image width="100%" height="300rpx" :src="src"></u-image>
 */
export default {
    name: 'u-image',
    props: {
        // 图片地址
        src: {
            type: String,
            default: ''
        },
        // 裁剪模式
        mode: {
            type: String,
            default: 'aspectFill'
        },
        // 宽度,单位任意
        width: {
            type: [String, Number],
            default: '100%'
        },
        // 高度,单位任意
        height: {
            type: [String, Number],
            default: 'auto'
        },
        // 图片形状,circle-圆形,square-方形
        shape: {
            type: String,
            default: 'square'
        },
        // 圆角,单位任意
        borderRadius: {
            type: [String, Number],
            default: 0
        },
        // 是否懒加载,微信小程序、App、百度小程序、字节跳动小程序
        lazyLoad: {
            type: Boolean,
            default: true
        },
        // 开启长按图片显示识别微信小程序码菜单
        showMenuByLongpress: {
            type: Boolean,
            default: true
        },
        // 加载中的图标,或者小图片
        loadingIcon: {
            type: String,
            default: 'photo'
        },
        // 加载失败的图标,或者小图片
        errorIcon: {
            type: String,
            default: 'error-circle'
        },
        // 是否显示加载中的图标或者自定义的slot
        showLoading: {
            type: Boolean,
            default: true
        },
        // 是否显示加载错误的图标或者自定义的slot
        showError: {
            type: Boolean,
            default: true
        },
        // 是否需要淡入效果
        fade: {
            type: Boolean,
            default: true
        },
        // 只支持网络资源,只对微信小程序有效
        webp: {
            type: Boolean,
            default: false
        },
        // 过渡时间,单位ms
        duration: {
            type: [String, Number],
            default: 500
        },
        // 背景颜色,用于深色页面加载图片时,为了和背景色融合
        bgColor: {
            type: String,
            default: '#f3f4f6'
        }
    },
    data() {
        return {
            // 图片是否加载错误,如果是,则显示错误占位图
            isError: false,
            // 初始化组件时,默认为加载中状态
            loading: true,
            // 不透明度,为了实现淡入淡出的效果
            opacity: 1,
            // 过渡时间,因为props的值无法修改,故需要一个中间值
            durationTime: this.duration,
            // 图片加载完成时,去掉背景颜色,因为如果是png图片,就会显示灰色的背景
            backgroundStyle: {}
        };
    },
    watch: {
        src: {
            immediate: true,
            handler (n) {
                if(!n) {
                    // 如果传入null或者'',或者false,或者undefined,标记为错误状态
                    this.isError = true;
                    this.loading = false;
                } else {
                    this.isError = false;
                }
            }
        }
    },
    computed: {
        wrapStyle() {
            let style = {};
            // 通过调用addUnit()方法,如果有单位,如百分比,px单位等,直接返回,如果是纯粹的数值,则加上rpx单位
            style.width = this.$u.addUnit(this.width);
            style.height = this.$u.addUnit(this.height);
            // 如果是配置了圆形,设置50%的圆角,否则按照默认的配置值
            style.borderRadius = this.shape == 'circle' ? '50%' : this.$u.addUnit(this.borderRadius);
            // 如果设置圆角,必须要有hidden,否则可能圆角无效
            style.overflow = this.borderRadius > 0 ? 'hidden' : 'visible';
            if (this.fade) {
                style.opacity = this.opacity;
                style.transition = `opacity ${Number(this.durationTime) / 1000}s ease-in-out`;
            }
            return style;
        }
    },
    methods: {
        // 点击图片
        onClick() {
            this.$emit('click');
        },
        // 图片加载失败
        onErrorHandler(err) {
            this.loading = false;
            this.isError = true;
            this.$emit('error', err);
        },
        // 图片加载完成,标记loading结束
        onLoadHandler() {
            this.loading = false;
            this.isError = false;
            this.$emit('load');
            // 如果不需要动画效果,就不执行下方代码,同时移除加载时的背景颜色
            // 否则无需fade效果时,png图片依然能看到下方的背景色
            if (!this.fade) return this.removeBgColor();
            // 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的灰色),再改成1,是为了获得过渡效果
            this.opacity = 0;
            // 这里设置为0,是为了图片展示到背景全透明这个过程时间为0,延时之后延时之后重新设置为duration,是为了获得背景透明(灰色)
            // 到图片展示的过程中的淡入效果
            this.durationTime = 0;
            // 延时50ms,否则在浏览器H5,过渡效果无效
            setTimeout(() => {
                this.durationTime = this.duration;
                this.opacity = 1;
                setTimeout(() => {
                    this.removeBgColor();
                }, this.durationTime);
            }, 50);
        },
        // 移除图片的背景色
        removeBgColor() {
            // 淡入动画过渡完成后,将背景设置为透明色,否则png图片会看到灰色的背景
            this.backgroundStyle = {
                backgroundColor: 'transparent'
            };
        }
    }
};
</script>
<style scoped lang="scss">
@import '../../libs/css/style.components.scss';
.u-image {
    position: relative;
    transition: opacity 0.5s ease-in-out;
    &__image {
        width: 100%;
        height: 100%;
    }
    &__loading,
    &__error {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        @include vue-flex;
        align-items: center;
        justify-content: center;
        background-color: $u-bg-color;
        color: $u-tips-color;
        font-size: 46rpx;
    }
}
</style>
uview-ui/components/u-index-anchor/u-index-anchor.vue
New file
@@ -0,0 +1,89 @@
<template>
    <!-- 支付宝小程序使用$u.getRect()获取组件的根元素尺寸,所以在外面套一个"壳" -->
    <view>
        <view class="u-index-anchor-wrapper" :id="$u.guid()" :style="[wrapperStyle]">
            <view class="u-index-anchor " :class="[active ? 'u-index-anchor--active' : '']" :style="[customAnchorStyle]">
                <slot v-if="useSlot" />
                <block v-else>
                    <text>{{ index }}</text>
                </block>
            </view>
        </view>
    </view>
</template>
<script>
    /**
     * indexAnchor 索引列表锚点
     * @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用
     * @tutorial https://www.uviewui.com/components/indexList.html#indexanchor-props
     * @property {Boolean} use-slot 是否使用自定义内容的插槽(默认false)
     * @property {String Number} index 索引字符,如果定义了use-slot,此参数自动失效
     * @property {Object} custStyle 自定义样式,对象形式,如"{color: 'red'}"
     * @event {Function} default 锚点位置显示内容,默认为索引字符
     * @example <u-index-anchor :index="item" />
     */
    export default {
        name: "u-index-anchor",
        props: {
            useSlot: {
                type: Boolean,
                default: false
            },
            index: {
                type: String,
                default: ''
            },
            customStyle: {
                type: Object,
                default () {
                    return {}
                }
            }
        },
        data() {
            return {
                active: false,
                wrapperStyle: {},
                anchorStyle: {}
            }
        },
        created() {
            this.parent = false;
        },
        mounted() {
            this.parent = this.$u.$parent.call(this, 'u-index-list');
            if(this.parent) {
                this.parent.children.push(this);
                this.parent.updateData();
            }
        },
        computed: {
            customAnchorStyle() {
                return Object.assign(this.anchorStyle, this.customStyle);
            }
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-index-anchor {
        box-sizing: border-box;
        padding: 14rpx 24rpx;
        color: #606266;
        width: 100%;
        font-weight: 500;
        font-size: 28rpx;
        line-height: 1.2;
        background-color: rgb(245, 245, 245);
    }
    .u-index-anchor--active {
        right: 0;
        left: 0;
        color: #2979ff;
        background-color: #fff;
    }
</style>
uview-ui/components/u-index-list/u-index-list.vue
New file
@@ -0,0 +1,315 @@
<template>
    <!-- 支付宝小程序使用$u.getRect()获取组件的根元素尺寸,所以在外面套一个"壳" -->
    <view>
        <view class="u-index-bar">
            <slot />
            <view v-if="showSidebar" class="u-index-bar__sidebar" @touchstart.stop.prevent="onTouchMove" @touchmove.stop.prevent="onTouchMove"
             @touchend.stop.prevent="onTouchStop" @touchcancel.stop.prevent="onTouchStop">
                <view v-for="(item, index) in indexList" :key="index" class="u-index-bar__index" :style="{zIndex: zIndex + 1, color: activeAnchorIndex === index ? activeColor : ''}"
                 :data-index="index">
                    {{ item }}
                </view>
            </view>
            <view class="u-indexed-list-alert" v-if="touchmove && indexList[touchmoveIndex]" :style="{
                zIndex: alertZIndex
            }">
                <text>{{indexList[touchmoveIndex]}}</text>
            </view>
        </view>
    </view>
</template>
<script>
    var indexList = function() {
        var indexList = [];
        var charCodeOfA = 'A'.charCodeAt(0);
        for (var i = 0; i < 26; i++) {
            indexList.push(String.fromCharCode(charCodeOfA + i));
        }
        return indexList;
    };
    /**
     * indexList 索引列表
     * @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用
     * @tutorial https://www.uviewui.com/components/indexList.html#indexanchor-props
     * @property {Number String} scroll-top 当前滚动高度,自定义组件无法获得滚动条事件,所以依赖接入方传入
     * @property {Array} index-list 索引字符列表,数组(默认A-Z)
     * @property {Number String} z-index 锚点吸顶时的层级(默认965)
     * @property {Boolean} sticky 是否开启锚点自动吸顶(默认true)
     * @property {Number String} offset-top 锚点自动吸顶时与顶部的距离(默认0)
     * @property {String} highlight-color 锚点和右边索引字符高亮颜色(默认#2979ff)
     * @event {Function} select 选中右边索引字符时触发
     * @example <u-index-list :scrollTop="scrollTop"></u-index-list>
     */
    export default {
        name: "u-index-list",
        props: {
            sticky: {
                type: Boolean,
                default: true
            },
            zIndex: {
                type: [Number, String],
                default: ''
            },
            scrollTop: {
                type: [Number, String],
                default: 0,
            },
            offsetTop: {
                type: [Number, String],
                default: 0
            },
            indexList: {
                type: Array,
                default () {
                    return indexList()
                }
            },
            activeColor: {
                type: String,
                default: '#2979ff'
            }
        },
        created() {
            // #ifdef H5
            this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 44;
            // #endif
            // #ifndef H5
            this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 0;
            // #endif
            // 只能在created生命周期定义children,如果在data定义,会因为循环引用而报错
            this.children = [];
        },
        data() {
            return {
                activeAnchorIndex: 0,
                showSidebar: true,
                // children: [],
                touchmove: false,
                touchmoveIndex: 0,
            }
        },
        watch: {
            scrollTop() {
                this.updateData()
            }
        },
        computed: {
            // 弹出toast的z-index值
            alertZIndex() {
                return this.$u.zIndex.toast;
            }
        },
        methods: {
            updateData() {
                this.timer && clearTimeout(this.timer);
                this.timer = setTimeout(() => {
                    this.showSidebar = !!this.children.length;
                    this.setRect().then(() => {
                        this.onScroll();
                    });
                }, 0);
            },
            setRect() {
                return Promise.all([
                    this.setAnchorsRect(),
                    this.setListRect(),
                    this.setSiderbarRect()
                ]);
            },
            setAnchorsRect() {
                return Promise.all(this.children.map((anchor, index) => anchor
                    .$uGetRect('.u-index-anchor-wrapper')
                    .then((rect) => {
                        Object.assign(anchor, {
                            height: rect.height,
                            top: rect.top
                        });
                    })));
            },
            setListRect() {
                return this.$uGetRect('.u-index-bar').then((rect) => {
                    Object.assign(this, {
                        height: rect.height,
                        top: rect.top + this.scrollTop
                    });
                });
            },
            setSiderbarRect() {
                return this.$uGetRect('.u-index-bar__sidebar').then(rect => {
                    this.sidebar = {
                        height: rect.height,
                        top: rect.top
                    };
                });
            },
            getActiveAnchorIndex() {
                const {
                    children
                } = this;
                const {
                    sticky
                } = this;
                for (let i = this.children.length - 1; i >= 0; i--) {
                    const preAnchorHeight = i > 0 ? children[i - 1].height : 0;
                    const reachTop = sticky ? preAnchorHeight : 0;
                    if (reachTop >= children[i].top) {
                        return i;
                    }
                }
                return -1;
            },
            onScroll() {
                const {
                    children = []
                } = this;
                if (!children.length) {
                    return;
                }
                const {
                    sticky,
                    stickyOffsetTop,
                    zIndex,
                    scrollTop,
                    activeColor
                } = this;
                const active = this.getActiveAnchorIndex();
                this.activeAnchorIndex = active;
                if (sticky) {
                    let isActiveAnchorSticky = false;
                    if (active !== -1) {
                        isActiveAnchorSticky =
                            children[active].top <= 0;
                    }
                    children.forEach((item, index) => {
                        if (index === active) {
                            let wrapperStyle = '';
                            let anchorStyle = {
                                color: `${activeColor}`
                            };
                            if (isActiveAnchorSticky) {
                                wrapperStyle = {
                                    height: `${children[index].height}px`
                                };
                                anchorStyle = {
                                    position: 'fixed',
                                    top: `${stickyOffsetTop}px`,
                                    zIndex: `${zIndex ? zIndex : this.$u.zIndex.indexListSticky}`,
                                    color: `${activeColor}`
                                };
                            }
                            item.active = active;
                            item.wrapperStyle = wrapperStyle;
                            item.anchorStyle = anchorStyle;
                        } else if (index === active - 1) {
                            const currentAnchor = children[index];
                            const currentOffsetTop = currentAnchor.top;
                            const targetOffsetTop = index === children.length - 1 ?
                                this.top :
                                children[index + 1].top;
                            const parentOffsetHeight = targetOffsetTop - currentOffsetTop;
                            const translateY = parentOffsetHeight - currentAnchor.height;
                            const anchorStyle = {
                                position: 'relative',
                                transform: `translate3d(0, ${translateY}px, 0)`,
                                zIndex: `${zIndex ? zIndex : this.$u.zIndex.indexListSticky}`,
                                color: `${activeColor}`
                            };
                            item.active = active;
                            item.anchorStyle = anchorStyle;
                        } else {
                            item.active = false;
                            item.anchorStyle = '';
                            item.wrapperStyle = '';
                        }
                    });
                }
            },
            onTouchMove(event) {
                this.touchmove = true;
                const sidebarLength = this.children.length;
                const touch = event.touches[0];
                const itemHeight = this.sidebar.height / sidebarLength;
                let clientY = 0;
                clientY = touch.clientY;
                let index = Math.floor((clientY - this.sidebar.top) / itemHeight);
                if (index < 0) {
                    index = 0;
                } else if (index > sidebarLength - 1) {
                    index = sidebarLength - 1;
                }
                this.touchmoveIndex = index;
                this.scrollToAnchor(index);
            },
            onTouchStop() {
                this.touchmove = false;
                this.scrollToAnchorIndex = null;
            },
            scrollToAnchor(index) {
                if (this.scrollToAnchorIndex === index) {
                    return;
                }
                this.scrollToAnchorIndex = index;
                const anchor = this.children.find((item) => item.index === this.indexList[index]);
                if (anchor) {
                    this.$emit('select', anchor.index);
                    uni.pageScrollTo({
                        duration: 0,
                        scrollTop: anchor.top + this.scrollTop
                    });
                }
            }
        }
    };
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-index-bar {
        position: relative
    }
    .u-index-bar__sidebar {
        position: fixed;
        top: 50%;
        right: 0;
        @include vue-flex;
        flex-direction: column;
        text-align: center;
        transform: translateY(-50%);
        user-select: none;
        z-index: 99;
    }
    .u-index-bar__index {
        font-weight: 500;
        padding: 8rpx 18rpx;
        font-size: 22rpx;
        line-height: 1
    }
    .u-indexed-list-alert {
        position: fixed;
        width: 120rpx;
        height: 120rpx;
        right: 90rpx;
        top: 50%;
        margin-top: -60rpx;
        border-radius: 24rpx;
        font-size: 50rpx;
        color: #fff;
        background-color: rgba(0, 0, 0, 0.65);
        @include vue-flex;
        justify-content: center;
        align-items: center;
        padding: 0;
        z-index: 9999999;
    }
    .u-indexed-list-alert text {
        line-height: 50rpx;
    }
</style>
uview-ui/components/u-input/u-input.vue
New file
@@ -0,0 +1,387 @@
<template>
    <view
        class="u-input"
        :class="{
            'u-input--border': border,
            'u-input--error': validateState
        }"
        :style="{
            padding: `0 ${border ? 20 : 0}rpx`,
            borderColor: borderColor,
            textAlign: inputAlign
        }"
        @tap.stop="inputClick"
    >
        <textarea
            v-if="type == 'textarea'"
            class="u-input__input u-input__textarea"
            :style="[getStyle]"
            :value="defaultValue"
            :placeholder="placeholder"
            :placeholderStyle="placeholderStyle"
            :disabled="disabled"
            :maxlength="inputMaxlength"
            :fixed="fixed"
            :focus="focus"
            :autoHeight="autoHeight"
            :selection-end="uSelectionEnd"
            :selection-start="uSelectionStart"
            :cursor-spacing="getCursorSpacing"
            :show-confirm-bar="showConfirmbar"
            @input="handleInput"
            @blur="handleBlur"
            @focus="onFocus"
            @confirm="onConfirm"
        />
        <input
            v-else
            class="u-input__input"
            :type="type == 'password' ? 'text' : type"
            :style="[getStyle]"
            :value="defaultValue"
            :password="type == 'password' && !showPassword"
            :placeholder="placeholder"
            :placeholderStyle="placeholderStyle"
            :disabled="disabled || type === 'select'"
            :maxlength="inputMaxlength"
            :focus="focus"
            :confirmType="confirmType"
            :cursor-spacing="getCursorSpacing"
            :selection-end="uSelectionEnd"
            :selection-start="uSelectionStart"
            :show-confirm-bar="showConfirmbar"
            @focus="onFocus"
            @blur="handleBlur"
            @input="handleInput"
            @confirm="onConfirm"
        />
        <view class="u-input__right-icon u-flex">
            <view class="u-input__right-icon__clear u-input__right-icon__item" @tap="onClear" v-if="clearable && value != '' && focused">
                <u-icon size="32" name="close-circle-fill" color="#c0c4cc"/>
            </view>
            <view class="u-input__right-icon__clear u-input__right-icon__item" v-if="passwordIcon && type == 'password'">
                <u-icon size="32" :name="!showPassword ? 'eye' : 'eye-fill'" color="#c0c4cc" @click="showPassword = !showPassword"/>
            </view>
            <view class="u-input__right-icon--select u-input__right-icon__item" v-if="type == 'select'" :class="{
                'u-input__right-icon--select--reverse': selectOpen
            }">
                <u-icon name="arrow-down-fill" size="26" color="#c0c4cc"></u-icon>
            </view>
        </view>
    </view>
</template>
<script>
import Emitter from '../../libs/util/emitter.js';
/**
 * input 输入框
 * @description 此组件为一个输入框,默认没有边框和样式,是专门为配合表单组件u-form而设计的,利用它可以快速实现表单验证,输入内容,下拉选择等功能。
 * @tutorial http://uviewui.com/components/input.html
 * @property {String} type 模式选择,见官网说明
 * @property {Boolean} clearable 是否显示右侧的清除图标(默认true)
 * @property {} v-model 用于双向绑定输入框的值
 * @property {String} input-align 输入框文字的对齐方式(默认left)
 * @property {String} placeholder placeholder显示值(默认 '请输入内容')
 * @property {Boolean} disabled 是否禁用输入框(默认false)
 * @property {String Number} maxlength 输入框的最大可输入长度(默认140)
 * @property {String Number} selection-start 光标起始位置,自动聚焦时有效,需与selection-end搭配使用(默认-1)
 * @property {String Number} maxlength 光标结束位置,自动聚焦时有效,需与selection-start搭配使用(默认-1)
 * @property {String Number} cursor-spacing 指定光标与键盘的距离,单位px(默认0)
 * @property {String} placeholderStyle placeholder的样式,字符串形式,如"color: red;"(默认 "color: #c0c4cc;")
 * @property {String} confirm-type 设置键盘右下角按钮的文字,仅在type为text时生效(默认done)
 * @property {Object} custom-style 自定义输入框的样式,对象形式
 * @property {Boolean} focus 是否自动获得焦点(默认false)
 * @property {Boolean} fixed 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false)
 * @property {Boolean} password-icon type为password时,是否显示右侧的密码查看图标(默认true)
 * @property {Boolean} border 是否显示边框(默认false)
 * @property {String} border-color 输入框的边框颜色(默认#dcdfe6)
 * @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true)
 * @property {String Number} height 高度,单位rpx(text类型时为70,textarea时为100)
 * @example <u-input v-model="value" :type="type" :border="border" />
 */
export default {
    name: 'u-input',
    mixins: [Emitter],
    props: {
        value: {
            type: [String, Number],
            default: ''
        },
        // 输入框的类型,textarea,text,number
        type: {
            type: String,
            default: 'text'
        },
        inputAlign: {
            type: String,
            default: 'left'
        },
        placeholder: {
            type: String,
            default: '请输入内容'
        },
        disabled: {
            type: Boolean,
            default: false
        },
        maxlength: {
            type: [Number, String],
            default: 140
        },
        placeholderStyle: {
            type: String,
            default: 'color: #c0c4cc;'
        },
        confirmType: {
            type: String,
            default: 'done'
        },
        // 输入框的自定义样式
        customStyle: {
            type: Object,
            default() {
                return {};
            }
        },
        // 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
        fixed: {
            type: Boolean,
            default: false
        },
        // 是否自动获得焦点
        focus: {
            type: Boolean,
            default: false
        },
        // 密码类型时,是否显示右侧的密码图标
        passwordIcon: {
            type: Boolean,
            default: true
        },
        // input|textarea是否显示边框
        border: {
            type: Boolean,
            default: false
        },
        // 输入框的边框颜色
        borderColor: {
            type: String,
            default: '#dcdfe6'
        },
        autoHeight: {
            type: Boolean,
            default: true
        },
        // type=select时,旋转右侧的图标,标识当前处于打开还是关闭select的状态
        // open-打开,close-关闭
        selectOpen: {
            type: Boolean,
            default: false
        },
        // 高度,单位rpx
        height: {
            type: [Number, String],
            default: ''
        },
        // 是否可清空
        clearable: {
            type: Boolean,
            default: true
        },
        // 指定光标与键盘的距离,单位 px
        cursorSpacing: {
            type: [Number, String],
            default: 0
        },
        // 光标起始位置,自动聚焦时有效,需与selection-end搭配使用
        selectionStart: {
            type: [Number, String],
            default: -1
        },
        // 光标结束位置,自动聚焦时有效,需与selection-start搭配使用
        selectionEnd: {
            type: [Number, String],
            default: -1
        },
        // 是否自动去除两端的空格
        trim: {
            type: Boolean,
            default: true
        },
        // 是否显示键盘上方带有”完成“按钮那一栏
        showConfirmbar:{
            type:Boolean,
            default:true
        }
    },
    data() {
        return {
            defaultValue: this.value,
            inputHeight: 70, // input的高度
            textareaHeight: 100, // textarea的高度
            validateState: false, // 当前input的验证状态,用于错误时,边框是否改为红色
            focused: false, // 当前是否处于获得焦点的状态
            showPassword: false, // 是否预览密码
            lastValue: '', // 用于头条小程序,判断@input中,前后的值是否发生了变化,因为头条中文下,按下键没有输入内容,也会触发@input时间
        };
    },
    watch: {
        value(nVal, oVal) {
            this.defaultValue = nVal;
            // 当值发生变化,且为select类型时(此时input被设置为disabled,不会触发@input事件),模拟触发@input事件
            if(nVal != oVal && this.type == 'select') this.handleInput({
                detail: {
                    value: nVal
                }
            })
        },
    },
    computed: {
        // 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值
        inputMaxlength() {
            return Number(this.maxlength);
        },
        getStyle() {
            let style = {};
            // 如果没有自定义高度,就根据type为input还是textare来分配一个默认的高度
            style.minHeight = this.height ? this.height + 'rpx' : this.type == 'textarea' ?
                this.textareaHeight + 'rpx' : this.inputHeight + 'rpx';
            style = Object.assign(style, this.customStyle);
            return style;
        },
        //
        getCursorSpacing() {
            return Number(this.cursorSpacing);
        },
        // 光标起始位置
        uSelectionStart() {
            return String(this.selectionStart);
        },
        // 光标结束位置
        uSelectionEnd() {
            return String(this.selectionEnd);
        }
    },
    created() {
        // 监听u-form-item发出的错误事件,将输入框边框变红色
        this.$on('on-form-item-error', this.onFormItemError);
    },
    methods: {
        /**
         * change 事件
         * @param event
         */
        handleInput(event) {
            let value = event.detail.value;
            // 判断是否去除空格
            if(this.trim) value = this.$u.trim(value);
            // vue 原生的方法 return 出去
            this.$emit('input', value);
            // 当前model 赋值
            this.defaultValue = value;
            // 过一个生命周期再发送事件给u-form-item,否则this.$emit('input')更新了父组件的值,但是微信小程序上
            // 尚未更新到u-form-item,导致获取的值为空,从而校验混论
            // 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱
            setTimeout(() => {
                // 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
                // #ifdef MP-TOUTIAO
                if(this.$u.trim(value) == this.lastValue) return ;
                this.lastValue = value;
                // #endif
                // 将当前的值发送到 u-form-item 进行校验
                this.dispatch('u-form-item', 'on-form-change', value);
            }, 40)
        },
        /**
         * blur 事件
         * @param event
         */
        handleBlur(event) {
            // 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错
            // 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时
            setTimeout(() => {
                this.focused = false;
            }, 100)
            // vue 原生的方法 return 出去
            this.$emit('blur', event.detail.value);
            setTimeout(() => {
                // 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
                // #ifdef MP-TOUTIAO
                if(this.$u.trim(value) == this.lastValue) return ;
                this.lastValue = value;
                // #endif
                // 将当前的值发送到 u-form-item 进行校验
                this.dispatch('u-form-item', 'on-form-blur', event.detail.value);
            }, 40)
        },
        onFormItemError(status) {
            this.validateState = status;
        },
        onFocus(event) {
            this.focused = true;
            this.$emit('focus');
        },
        onConfirm(e) {
            this.$emit('confirm', e.detail.value);
        },
        onClear(event) {
            this.$emit('input', '');
        },
        inputClick() {
            this.$emit('click');
        }
    }
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-input {
    position: relative;
    flex: 1;
    @include vue-flex;
    &__input {
        //height: $u-form-item-height;
        font-size: 28rpx;
        color: $u-main-color;
        flex: 1;
    }
    &__textarea {
        width: auto;
        font-size: 28rpx;
        color: $u-main-color;
        padding: 10rpx 0;
        line-height: normal;
        flex: 1;
    }
    &--border {
        border-radius: 6rpx;
        border-radius: 4px;
        border: 1px solid $u-form-item-border-color;
    }
    &--error {
        border-color: $u-type-error!important;
    }
    &__right-icon {
        &__item {
            margin-left: 10rpx;
        }
        &--select {
            transition: transform .4s;
            &--reverse {
                transform: rotate(-180deg);
            }
        }
    }
}
</style>
uview-ui/components/u-keyboard/u-keyboard.vue
New file
@@ -0,0 +1,217 @@
<template>
    <u-popup class="" :mask="mask" :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto"
     :safeAreaInsetBottom="safeAreaInsetBottom" @close="popupClose" :zIndex="uZIndex">
        <slot />
        <view class="u-tooltip" v-if="tooltip">
            <view class="u-tooltip-item u-tooltip-cancel" hover-class="u-tooltip-cancel-hover" @tap="onCancel">
                {{cancelBtn ? cancelText : ''}}
            </view>
            <view v-if="showTips" class="u-tooltip-item u-tooltip-tips">
                {{tips ? tips : mode == 'number' ? '数字键盘' : mode == 'card' ? '身份证键盘' : '车牌号键盘'}}
            </view>
            <view v-if="confirmBtn" @tap="onConfirm" class="u-tooltip-item u-tooltips-submit" hover-class="u-tooltips-submit-hover">
                {{confirmBtn ? confirmText : ''}}
            </view>
        </view>
        <block v-if="mode == 'number' || mode == 'card'">
            <u-number-keyboard :random="random" @backspace="backspace" @change="change" :mode="mode" :dotEnabled="dotEnabled"></u-number-keyboard>
        </block>
        <block v-else>
            <u-car-keyboard :random="random" @backspace="backspace" @change="change"></u-car-keyboard>
        </block>
    </u-popup>
</template>
<script>
    /**
     * keyboard 键盘
     * @description 此为uViw自定义的键盘面板,内含了数字键盘,车牌号键,身份证号键盘3中模式,都有可以打乱按键顺序的选项。
     * @tutorial https://www.uviewui.com/components/keyboard.html
     * @property {String} mode 键盘类型,见官网基本使用的说明(默认number)
     * @property {Boolean} dot-enabled 是否显示"."按键,只在mode=number时有效(默认true)
     * @property {Boolean} tooltip 是否显示键盘顶部工具条(默认true)
     * @property {String} tips 工具条中间的提示文字,见上方基本使用的说明,如不需要,请传""空字符
     * @property {Boolean} cancel-btn 是否显示工具条左边的"取消"按钮(默认true)
     * @property {Boolean} confirm-btn 是否显示工具条右边的"完成"按钮(默认true)
     * @property {Boolean} mask 是否显示遮罩(默认true)
     * @property {String} confirm-text 确认按钮的文字
     * @property {String} cancel-text 取消按钮的文字
     * @property {Number String} z-index 弹出键盘的z-index值(默认1075)
     * @property {Boolean} random 是否打乱键盘按键的顺序(默认false)
     * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
     * @property {Boolean} mask-close-able 是否允许点击遮罩收起键盘(默认true)
     * @event {Function} change 按键被点击(不包含退格键被点击)
     * @event {Function} cancel 键盘顶部工具条左边的"取消"按钮被点击
     * @event {Function} confirm 键盘顶部工具条右边的"完成"按钮被点击
     * @event {Function} backspace 键盘退格键被点击
     * @example <u-keyboard mode="number" v-model="show"></u-keyboard>
     */
    export default {
        name: "u-keyboard",
        props: {
            // 键盘的类型,number-数字键盘,card-身份证键盘,car-车牌号键盘
            mode: {
                type: String,
                default: 'number'
            },
            // 是否显示键盘的"."符号
            dotEnabled: {
                type: Boolean,
                default: true
            },
            // 是否显示顶部工具条
            tooltip: {
                type: Boolean,
                default: true
            },
            // 是否显示工具条中间的提示
            showTips: {
                type: Boolean,
                default: true
            },
            // 工具条中间的提示文字
            tips: {
                type: String,
                default: ''
            },
            // 是否显示工具条左边的"取消"按钮
            cancelBtn: {
                type: Boolean,
                default: true
            },
            // 是否显示工具条右边的"完成"按钮
            confirmBtn: {
                type: Boolean,
                default: true
            },
            // 是否打乱键盘按键的顺序
            random: {
                type: Boolean,
                default: false
            },
            // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
            safeAreaInsetBottom: {
                type: Boolean,
                default: false
            },
            // 是否允许通过点击遮罩关闭键盘
            maskCloseAble: {
                type: Boolean,
                default: true
            },
            // 通过双向绑定控制键盘的弹出与收起
            value: {
                type: Boolean,
                default: false
            },
            // 是否显示遮罩,某些时候数字键盘时,用户希望看到自己的数值,所以可能不想要遮罩
            mask: {
                type: Boolean,
                default: true
            },
            // z-index值
            zIndex: {
                type: [Number, String],
                default: ''
            },
            // 取消按钮的文字
            cancelText: {
                type: String,
                default: '取消'
            },
            // 确认按钮的文字
            confirmText: {
                type: String,
                default: '确认'
            }
        },
        data() {
            return {
                //show: false
            }
        },
        computed: {
            uZIndex() {
                return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
            }
        },
        methods: {
            change(e) {
                this.$emit('change', e);
            },
            // 键盘关闭
            popupClose() {
                // 通过发送input这个特殊的事件名,可以修改父组件传给props的value的变量,也即双向绑定
                this.$emit('input', false);
            },
            // 输入完成
            onConfirm() {
                this.popupClose();
                this.$emit('confirm');
            },
            // 取消输入
            onCancel() {
                this.popupClose();
                this.$emit('cancel');
            },
            // 退格键
            backspace() {
                this.$emit('backspace');
            },
            // 关闭键盘
            // close() {
            //     this.show = false;
            // },
            // // 打开键盘
            // open() {
            //     this.show = true;
            // }
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-keyboard {
        position: relative;
        z-index: 1003;
    }
    .u-tooltip {
        @include vue-flex;
        justify-content: space-between;
    }
    .u-tooltip-item {
        color: #333333;
        flex: 0 0 33.333333%;
        text-align: center;
        padding: 20rpx 10rpx;
        font-size: 28rpx;
    }
    .u-tooltips-submit {
        text-align: right;
        flex-grow: 1;
        flex-wrap: 0;
        padding-right: 40rpx;
        color: $u-type-primary;
    }
    .u-tooltip-cancel {
        text-align: left;
        flex-grow: 1;
        flex-wrap: 0;
        padding-left: 40rpx;
        color: #888888;
    }
    .u-tooltips-submit-hover {
        color: $u-type-success;
    }
    .u-tooltip-cancel-hover {
        color: #333333;
    }
</style>
uview-ui/components/u-lazy-load/u-lazy-load.vue
New file
@@ -0,0 +1,244 @@
<template>
    <view class="u-wrap" :style="{
              opacity: Number(opacity),
              borderRadius: borderRadius + 'rpx',
              // 因为time值需要改变,所以不直接用duration值(不能改变父组件prop传过来的值)
              transition: `opacity ${time / 1000}s ease-in-out`
           }"
     :class="'u-lazy-item-' + elIndex">
        <view :class="'u-lazy-item-' + elIndex">
            <image :style="{borderRadius: borderRadius + 'rpx', height: imgHeight}" v-if="!isError" class="u-lazy-item"
             :src="isShow ? image : loadingImg" :mode="imgMode" @load="imgLoaded" @error="loadError" @tap="clickImg"></image>
            <image :style="{borderRadius: borderRadius + 'rpx', height: imgHeight}" class="u-lazy-item error" v-else :src="errorImg"
             :mode="imgMode" @load="errorImgLoaded" @tap="clickImg"></image>
        </view>
    </view>
</template>
<script>
    /**
     * lazyLoad 懒加载
     * @description 懒加载使用的场景为:页面有很多图片时,APP会同时加载所有的图片,导致页面卡顿,各个位置的图片出现前后不一致等.
     * @tutorial https://www.uviewui.com/components/lazyLoad.html
     * @property {String Number} index 用户自定义值,在事件触发时回调,用以区分是哪个图片
     * @property {String} image 图片路径
     * @property {String} loading-img 预加载时的占位图
     * @property {String} error-img 图片加载出错时的占位图
     * @property {String} threshold 触发加载时的位置,见上方说明,单位 rpx(默认300)
     * @property {String Number} duration 图片加载成功时,淡入淡出时间,单位ms(默认)
     * @property {String} effect 图片加载成功时,淡入淡出的css动画效果(默认ease-in-out)
     * @property {Boolean} is-effect 图片加载成功时,是否启用淡入淡出效果(默认true)
     * @property {String Number} border-radius 图片圆角值,单位rpx(默认0)
     * @property {String Number} height 图片高度,注意:实际高度可能受img-mode参数影响(默认450)
     * @property {String Number} mg-mode 图片的裁剪模式,详见image组件裁剪模式(默认widthFix)
     * @event {Function} click 点击图片时触发
     * @event {Function} load 图片加载成功时触发
     * @event {Function} error 图片加载失败时触发
     * @example <u-lazy-load :image="image" :loading-img="loadingImg" :error-img="errorImg"></u-lazy-load>
     */
    export default {
        name: 'u-lazy-load',
        props: {
            index: {
                type: [Number, String]
            },
            // 要显示的图片
            image: {
                type: String,
                default: ''
            },
            // 图片裁剪模式
            imgMode: {
                type: String,
                default: 'widthFix'
            },
            // 占位图片路径
            loadingImg: {
                type: String,
                default: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAMAAAC3Ycb+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUM0QjNBQjkyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUM0QjNBQkEyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5QzRCM0FCNzJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QzRCM0FCODJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PtRHfPcAAAAzUExURZWVldfX18PDw62trZubm9zc3Li4uKGhoebm5tLS0uHh4c3Nzaenp729vcjIyLKysuvr6141L40AAAcXSURBVHja7NzZlqpGAEBR5lG0//9rIw7IJKJi4or7PGTdtN10wr5SVAEGf/qqArsAiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAg+nmQFMi5Jis+sIniED23jSzIgLTtg2D//iYme/8QBM/9lQ+CAEhbNLM3N9hEHAThX7GPCiBfAxK1b51kD+R7QMLjXg7iCsgWIPUh7pfVozG791oeBPngm48G583uW5GkBvI+SBaM2xXDn1oqum423bX/mgF5FySc2cv93Voug9TdZotsggnkBZB2NzbhrSY5HnoG07jei8dvzsJB/c3W60SALILE46+WCztsbhPR7R2VJq0ukEcT49nyy8QhaKcRa3fYHZD4+ufqOJAcgDz8/59vtw1I3QP5K6JsOG0vm3hce4I8LQp/BaRZGJC3AAn7IKOKXbC+7EdA5vdmmVwOLksgRThqOqiH4XEGsht+peoPUE8U/jJIO5OLH4GEwUslV5G0PTBG5Uiw/Y2jyigO3l9HAHKv9PYb82LloH74dZBoBUgar+l48NsNvtD0fkez9iwrAvIYZDRCl+Xs149Hm/KZmQ+QjUCiO1ei4ru7EsgnQYrkznlQb7thCuRfAzlOAPN72427P4VA/i2Q/DKT/Ls/VR8fvIBsDZIuz7TPF6TCbnk4GJkB2RokejTjuE7/unlgCuSTIO0Cy+Plp6vDfnQlBchy8QtjSHVd3EgmK1bHLm+H6+nXYbz2DuQRSPnqoL7vvq0u70on4zvxgCyWD3b9UyDVdW24PaWaiGTnFZJwPIQAebDpIKheBIm7n124ZthMJipAlkqHO+IZkP1tbfzOJark/A7MgKyvvl60fRqkvXfhuow+t9+q00+0/yyBrK8ZngOtBzldhw2X9tvpNGty0gvkmbPeJ0Cy/r09s/stbmfo0yMWkEdjevgKyOn2t2pxv7UXoibTdCDLje9/Ww1ymqzn87dbp92242ZmMRjI8hASvwKSLq4udqN6ksw8nxXN3tszD9L8Gkg+2mFrQYql5az4tvFj5xOx4VwnSdeBtGdyPwUytxK77pBVlNHdO7OK3rh/eTPUvdutT3fO52tuHMqD4N7llv8pyOQQ//w19YVDfX27+Sfuby9/6nau4pdA8vEdOZuChEH/quHt0Jg+IRJ/5+PrHwKZXfjbDiS73Zo7mu5UkzX7uTsXe0e/7nC3ePf1O69+BUg2XDfZCqSqOu7rGVf8cHBe8zhC2b61dtUHXv0OkGo6ZL4JkpbRYXdUaFevivx2M/1GIOctNh949TtAoumQ+TpIHMX54CJu+8BDd8FkE5BqcZh/59XvAClmTvKfB0nDqIlHo3T70SftyW1eX9dXtgQJqs1f/Q6QaOa/7wmQKtxH8eiGoCRuovODIO3VxOMmruZbHrLyD7z6DSDtGyT7ew1kf9hNn07c986JTovzzem0Id9wUG+Vk/IDr34DSNR7huZJkMFT6vEhqrPx/j5cnlZML8N6/PAzh9Y99Flm5Yde/c9BquDOkvkKkMP58dA4qi9vivE8JOvGz/j8FokfPpr288+pH2ZPOZrLmeGD+7KOh6dqYWJ48ki7yUg0tz0go/fv/LLddfV3sgOLJyaGPY/zrSlh1a36Arkzoue9CyG35ze6E6/dzO2Ga0EGHqdRJIkfn9/8OEjTW8Vq91ZWh39FeehWA7Nu9ft8CpUEk1WWOyDF0OPyEU2Pnzf/bZC0P6IPzmAvu7KauQBVrgKpJ0tG2arHzX8e5Pb3PezNs/PrX+3JMyCLn9XXf37tPFHvt09WfCDDjx+yyn1/p1V11j7GnB/q3leLuVva79S/tzed+db08YpF4uOZtmz/9oXWMq6BCAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiAALELvqt/BBgACqVeUBXxcCkAAAAASUVORK5CYII='
            },
            // 加载失败的错误占位图
            errorImg: {
                type: String,
                default: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAMAAAC3Ycb+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODdDMjhENDYyQUQ2MTFFQTlDQ0VBODgxQjFFOEEyMEMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODdDMjhENDcyQUQ2MTFFQTlDQ0VBODgxQjFFOEEyMEMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4N0MyOEQ0NDJBRDYxMUVBOUNDRUE4ODFCMUU4QTIwQyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4N0MyOEQ0NTJBRDYxMUVBOUNDRUE4ODFCMUU4QTIwQyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PhLwhikAAAAzUExURZWVldfX162trcPDw5ubm7i4uNzc3Obm5s3NzaGhoeHh4cjIyKenp9LS0r29vbKysuvr67sDMEkAAAlpSURBVHja7NzpYqMgAIVRUVHc8/5PO66R1WAbOzX97q+ZtDEpR0AWTR7kVyWhCAAhgABCAAGEAAIIAQQQAggBBBACCCAEEEAIIIAQQAgggBBAACGAAEIAAYQAQgABhAACCAEEEAIIIAQQAgggBBBACCCAEEAAIYAQQAAhgABCAAGEAAIIAYQAAggBBBACCCAEEEAIIAQQQAgggBBAACGAAEIAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAAIYAQQAAhgABCAAGEAAIIAYQAAggBBBACCCAEEEAIIAQQQAgggBBAACGAAEIAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAIIIAQQAAhgABCAAGEAEIAAYQAAggBBBACCCAEEAIIIAQQQAgggBBAACGAEEAAIYAAsqeX5QWHKIcs/Ptl03lfL4zDFPWfBGmSpPn+IZzSH5KkCL5B+n+oklwz6Iz//R2QzFOabzhEmiRirAmZt/bl0w/dpMbLqeeo4wEdpC7zR5WAPKziHKtO7ql+ReKvIa9BxgNaL5ZtEkpeAGIVp5jKJa09xVo9vgSSzQcszdYvmOqjQNSQ6pHK6rO1n1Xj32788miwHLaZz1Tl9i/yayDlYJ/60/+lp8GSY7OY1B8E4p55bWmfquFk22GLuUUxi78cX+m+BjL2GLkhMrV+/muS6Sfic0CEp5T1Yu2OQdTzsKV0MJV73KVjroyTffxfuv5Tf3fd6iLT9wz8YdVHgUzF2Is9/Xhi5sYJqP1w/GUpjOiHVbaI0w2L+pg3GZzvtokcgHxWDXHaiy78l3sPke01qphamT5c+dqyeAGSumdL/mkggauTam0e3L/mPEiqtzKDbl0Z1Wn8xOa4ySo8X/7TQIJnY/seEKWf12UmC72CKP9xYjr19RPT7NNA+oMO+R0gwmlotAry+C6I0f59ch8yXVQOr0BKYcXt1IUYRyCt+Ur9HGsrQKI79WY9sY9ARPKlzFOFdb41ioD8b5Bp+mqeeRKAxINkESBFGpOpKhgv9OuYpH8A8l4Qa3qp60Kl2/k+rG2sWafuuyCBafb2j4JkgZUob3nWcmicpkxEgmTLLGejTxnWSWCi8lPmsk6DlIHFJv24ojiYyYoGacwL8zXTLEAVaDI/Ybb3NIgKDSv2oXpmHkvNs+PTpMASEdlk7fOZeRk37fwJ6yGnQarQsGIfqqcvx43rTOXY6jf7uKXdRzdLDRPbjIrx1cIj3Kr4KyBFezzgUGuR5893qkOQ19fR2uVBaU+r16LphJNOiatK7PeBZK/Kb+tUn71rcQjSvARpghfH/yG/D2RetTuI3N5QrMWdP46brP7FmKZ//CGQ9At9SL01DLkzY/Vs8Z97fQZ7gelw7jHqCz+/Wile5J4g3Vc79eb5a6oLSue+Ve83gaSv2jp5PxCzjzwFUm9zw9MllSMil1kS4d2E9SaQ1xNo9wMxx0+nQNLnew/WDHvveMAHYm08mofl3TFI/8pD3Q6kMAv6DIi2jTCwRJUvNdDYrrJum9oHhusCbWALonwxBRk1vXMnEGWuT5wAmfYuVGUYpJ7fUZujCN92hvzwWlrFgxSfANKb10DxIMbShnfrynyZZV30imA7P43ArXXHbvBVkTCIuGy25AdBrHmNeBCpL214QdLp9LZarG3IMWrmW0ehtuO7F2PS09UcgqS3B7FKPhpknrStD0HGF/vQRne37LwLG8EbHT4WxN7/Fg0yD9Yr/3br4nnstA+0Il6QxzdBmg8A6a2/IRbkcK9h/uzV8zywF/oSkOyageCPglRWgcWClHnEzs9q/t/SENVXgFijlsq3VtXdCsRp4qObrLLLgjuzSq3fX89ZZW6AfxNIzF6X9FYgThN/fk093KkvHX/hbWd+DqS/FUhlf+G3gohEXzVs3g9iDluWoaW8fL73QhB34u+tIHIf19nLuF4Q98a09Eynnl56q+ePgEhnX+dbQOp6H5XnJ0ACd8dFgkwf12nTOTcEqd2pom+CFF02TIPw6dKmrLS5qOtBpo8b5quUtrwrSGbuqPkeSJqllTFHO02NPxdMrm+y5LKdWyWXjw4vA5nGEtnjuyCFyHqNYvEolzmASm3zK1Eg5zr13lhqV1tlksnVw8Pkwgri7O07AVKLJkutRYw87bPlRpBpNXE8xGb+fhBlvEGrGPLqViu5sILIx9dAmqF1705sxF4M8+R8P5dOdQwi12fMnATpjJ2JSt/POIvU9wPJEs/jduJAjLvU0yFT0i64Yb1bsVi79dA4pEy3TzoHMq2O7Re4vXm5O9+l290NpE4CU+YRIMNye2iaqbVS2AUnn2fsekthYKReVNutVedA5juttyIXrT38mOds+ps9DWhwL7GWc61/DVKPzVN9UHDarf1icU98IOU8tm6L031Iq63t1tKzj3fe/FCpO4F0/i0Z2+yvA1KeGBjqj1qYx8/zoxpKZ1Yl367I1k+sfcft/QPy9csXy/32qX1qLZsrryG5BGQaRj0vc/b7N54XXq293TCLB5HO42Fy517obW19b+qjl3CHp0fdLJcWvmdy1etESi/uAdJrs1hTaUklHuW8qSDdC3UfXVR5cnD3rAFSSqtFb7z7eapErx7rC739jCXfbK3aWiipjXo8UbmxXPa7QQq9R289j2Gr88N7Ag5AlHPRKc37pNZv0CZtX1tVMG6rm8qW1/KlCgQvcMss933ybwXZz3dReW5yce4ByZtHFIhwT9kmjxg8BzbKDUe1PB9edBJqSN7/KM1LmqyuMZ5BpeTUw1aD/uDI0relPfSHa/Wn8Pxq1BNfxy/h3IdwOJqIKumb9CHvTqMefyY82RoQAgggBBBACCCAEEAAIYAQQAAhgABCAAGEAAIIAYQAAggBBBACCCAEEEAIIAQQQAgggBBAACGAAEIAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAIIIAQQAAhgABCAAGEAEIAAYQAAggBBBACCCAEEAIIIAQQQAgggBBAACGAEEAAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAIIIAQQAAhgABCAAGEAEIAAYQAAggBBBACCCAEEAIIIAQQQAgggBBAACGAEEAAIYAAQgABhAACCAGEAAIIAQQQAgggBBBACCAEEEAIIIAQQAAhgABCACGAAEIAAYQAAggBBBACCAEEEAIIIAQQQAggfyL/BBgA8PgLdH0TBtkAAAAASUVORK5CYII='
            },
            // 图片进入可见区域前多少像素时,单位rpx,开始加载图片
            // 负数为图片超出屏幕底部多少距离后触发懒加载,正数为图片顶部距离屏幕底部多少距离时触发(图片还没出现在屏幕上)
            threshold: {
                type: [Number, String],
                default: 100
            },
            // 淡入淡出动画的过渡时间
            duration: {
                type: [Number, String],
                default: 500
            },
            // 渡效果的速度曲线,各个之间差别不大,因为这是淡入淡出,且时间很短,不是那些变形或者移动的情况,会明显
            // linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);
            effect: {
                type: String,
                default: 'ease-in-out'
            },
            // 是否使用过渡效果
            isEffect: {
                type: Boolean,
                default: true
            },
            // 圆角值
            borderRadius: {
                type: [Number, String],
                default: 0
            },
            // 图片高度,单位rpx
            height: {
                type: [Number, String],
                default: '450'
            }
        },
        data() {
            return {
                isShow: false,
                opacity: 1,
                time: this.duration,
                loadStatus: '', // 默认是懒加载中的状态
                isError: false, // 图片加载失败
                elIndex: this.$u.guid()
            }
        },
        computed: {
            // 将threshold从rpx转为px
            getThreshold() {
                // 先取绝对值,因为threshold可能是负数,最后根据this.threshold是正数或者负数,重新还原
                let thresholdPx = uni.upx2px(Math.abs(this.threshold));
                return this.threshold < 0 ? -thresholdPx : thresholdPx;
            },
            // 计算图片的高度,可能为auto,带%,或者直接数值
            imgHeight() {
                return this.$u.addUnit(this.height);
            }
        },
        created() {
            // 由于一些特殊原因,不能将此变量放到data中定义
            this.observer = {};
        },
        watch: {
            isShow(nVal) {
                // 如果是不开启过渡效果,直接返回
                if (!this.isEffect) return;
                this.time = 0;
                // 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的白色),再改成1,是为了获得过渡效果
                this.opacity = 0;
                // 延时30ms,否则在浏览器H5,过渡效果无效
                setTimeout(() => {
                    this.time = this.duration;
                    this.opacity = 1;
                }, 30)
            },
            // 图片路径发生变化时,需要重新标记一些变量,否则会一直卡在某一个状态,比如isError
            image(n) {
                if(!n) {
                    // 如果传入null或者'',或者undefined,标记为错误状态
                    this.isError = true;
                } else {
                    this.init();
                    this.isError = false;
                }
            }
        },
        methods: {
            // 用于重新初始化
            init() {
                this.isError = false;
                this.loadStatus = '';
            },
            // 点击图片触发的事件,loadlazy-还是懒加载中状态,loading-图片正在加载,loaded-图片加加载完成
            clickImg() {
                let whichImg = '';
                // 如果isShow为false,意味着图片还没开始加载,点击的只能是最开始的占位图
                if (this.isShow == false) whichImg = 'lazyImg';
                // 如果isError为true,意味着图片加载失败,这是只剩下错误的占位图,所以点击的只能是错误占位图
                // 当然,也可以给错误的占位图元素绑定点击事件,看你喜欢~
                else if (this.isError == true) whichImg = 'errorImg';
                // 总共三张图片,除了两个占位图,剩下的只能是正常的那张图片了
                else whichImg = 'realImg';
                // 只通知当前图片的index
                this.$emit('click', this.index);
            },
            // 图片加载完成事件,可能是加载占位图时触发,也可能是加载真正的图片完成时触发,通过isShow区分
            imgLoaded() {
                // 占位图加载完成
                if (this.loadStatus == '') {
                    this.loadStatus = 'lazyed';
                }
                // 真正的图片加载完成
                else if (this.loadStatus == 'lazyed') {
                    this.loadStatus = 'loaded';
                    this.$emit('load', this.index);
                }
            },
            // 错误的图片加载完成
            errorImgLoaded() {
                this.$emit('error', this.index);
            },
            // 图片加载失败
            loadError() {
                this.isError = true;
            },
            disconnectObserver(observerName) {
                const observer = this[observerName];
                observer && observer.disconnect();
            },
        },
        beforeDestroy() {
            // 销毁页面时,可能还没触发某张很底部的懒加载图片,所以把这个事件给去掉
            //observer.disconnect();
        },
        mounted() {
            // 此uOnReachBottom事件由mixin.js发出,目的是让页面到底时,保证所有图片都进行加载,做到绝对稳定且可靠
            this.$nextTick(() => {
                uni.$once('uOnReachBottom', () => {
                    if (!this.isShow) this.isShow = true;
                });
            })
            // mounted的时候,不一定挂载了这个元素,延时30ms,否则会报错或者不报错,但是也没有效果
            setTimeout(() => {
                // 这里是组件内获取布局状态,不能用uni.createIntersectionObserver,而必须用this.createIntersectionObserver
                this.disconnectObserver('contentObserver');
                const contentObserver = uni.createIntersectionObserver(this);
                // 要理解这里怎么计算的,请看这个:
                // https://blog.csdn.net/qq_25324335/article/details/83687695
                contentObserver.relativeToViewport({
                    bottom: this.getThreshold,
                }).observe('.u-lazy-item-' + this.elIndex, (res) => {
                    if (res.intersectionRatio > 0) {
                        // 懒加载状态改变
                        this.isShow = true;
                        // 如果图片已经加载,去掉监听,减少性能的消耗
                        this.disconnectObserver('contentObserver');
                    }
                })
                this.contentObserver = contentObserver;
            }, 30)
        }
    }
</script>
<style scoped lang="scss">
    @import "../../libs/css/style.components.scss";
    .u-wrap {
        background-color: #eee;
        overflow: hidden;
    }
    .u-lazy-item {
        width: 100%;
        // 骗系统开启硬件加速
        transform: transition3d(0, 0, 0);
        // 防止图片加载“闪一下”
        will-change: transform;
        /* #ifndef APP-NVUE */
        display: block;
        /* #endif */
    }
</style>
uview-ui/components/u-line-progress/u-line-progress.vue
New file
@@ -0,0 +1,147 @@
<template>
    <view class="u-progress" :style="{
        borderRadius: round ? '100rpx' : 0,
        height: height + 'rpx',
        backgroundColor: inactiveColor
    }">
        <view :class="[
            type ? `u-type-${type}-bg` : '',
            striped ? 'u-striped' : '',
            striped && stripedActive ? 'u-striped-active' : ''
        ]" class="u-active" :style="[progressStyle]">
            <slot v-if="$slots.default || $slots.$default" />
            <block v-else-if="showPercent">
                {{percent + '%'}}
            </block>
        </view>
    </view>
</template>
<script>
    /**
     * lineProgress 线型进度条
     * @description 展示操作或任务的当前进度,比如上传文件,是一个线形的进度条。
     * @tutorial https://www.uviewui.com/components/lineProgress.html
     * @property {String Number} percent 进度条百分比值,为数值类型,0-100
     * @property {Boolean} round 进度条两端是否为半圆(默认true)
     * @property {String} type 如设置,active-color值将会失效
     * @property {String} active-color 进度条激活部分的颜色(默认#19be6b)
     * @property {String} inactive-color 进度条的底色(默认#ececec)
     * @property {Boolean} show-percent 是否在进度条内部显示当前的百分比值数值(默认true)
     * @property {String Number} height 进度条的高度,单位rpx(默认28)
     * @property {Boolean} striped 是否显示进度条激活部分的条纹(默认false)
     * @property {Boolean} striped-active 条纹是否具有动态效果(默认false)
     * @example <u-line-progress :percent="70" :show-percent="true"></u-line-progress>
     */
    export default {
        name: "u-line-progress",
        props: {
            // 两端是否显示半圆形
            round: {
                type: Boolean,
                default: true
            },
            // 主题颜色
            type: {
                type: String,
                default: ''
            },
            // 激活部分的颜色
            activeColor: {
                type: String,
                default: '#19be6b'
            },
            inactiveColor: {
                type: String,
                default: '#ececec'
            },
            // 进度百分比,数值
            percent: {
                type: Number,
                default: 0
            },
            // 是否在进度条内部显示百分比的值
            showPercent: {
                type: Boolean,
                default: true
            },
            // 进度条的高度,单位rpx
            height: {
                type: [Number, String],
                default: 28
            },
            // 是否显示条纹
            striped: {
                type: Boolean,
                default: false
            },
            // 条纹是否显示活动状态
            stripedActive: {
                type: Boolean,
                default: false
            }
        },
        data() {
            return {
            }
        },
        computed: {
            progressStyle() {
                let style = {};
                style.width = this.percent + '%';
                if(this.activeColor) style.backgroundColor = this.activeColor;
                return style;
            }
        },
        methods: {
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-progress {
        overflow: hidden;
        height: 15px;
        /* #ifndef APP-NVUE */
        display: inline-flex;
        /* #endif */
        align-items: center;
        width: 100%;
        border-radius: 100rpx;
    }
    .u-active {
        width: 0;
        height: 100%;
        align-items: center;
        @include vue-flex;
        justify-items: flex-end;
        justify-content: space-around;
        font-size: 20rpx;
        color: #ffffff;
        transition: all 0.4s ease;
    }
    .u-striped {
        background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
        background-size: 39px 39px;
    }
    .u-striped-active {
        animation: progress-stripes 2s linear infinite;
    }
    @keyframes progress-stripes {
        0% {
            background-position: 0 0;
        }
        100% {
            background-position: 39px 0;
        }
    }
</style>
uview-ui/components/u-line/u-line.vue
New file
@@ -0,0 +1,84 @@
<template>
    <view class="u-line" :style="[lineStyle]">
    </view>
</template>
<script>
    /**
     * line 线条
     * @description 此组件一般用于显示一根线条,用于分隔内容块,有横向和竖向两种模式,且能设置0.5px线条,使用也很简单
     * @tutorial https://www.uviewui.com/components/line.html
     * @property {String} color 线条的颜色(默认#e4e7ed)
     * @property {String} length 长度,竖向时表现为高度,横向时表现为长度,可以为百分比,带rpx单位的值等
     * @property {String} direction 线条的方向,row-横向,col-竖向(默认row)
     * @property {String} border-style 线条的类型,solid-实线,dashed-方形虚线,dotted-圆点虚线(默认solid)
     * @property {Boolean} hair-line 是否显示细线条(默认true)
     * @property {String} margin 线条与上下左右元素的间距,字符串形式,如"30rpx"
     * @example <u-line color="red"></u-line>
     */
    export default {
        name: 'u-line',
        props: {
            color: {
                type: String,
                default: '#e4e7ed'
            },
            // 长度,竖向时表现为高度,横向时表现为长度,可以为百分比,带rpx单位的值等
            length: {
                type: String,
                default: '100%'
            },
            // 线条方向,col-竖向,row-横向
            direction: {
                type: String,
                default: 'row'
            },
            // 是否显示细边框
            hairLine: {
                type: Boolean,
                default: true
            },
            // 线条与上下左右元素的间距,字符串形式,如"30rpx"、"20rpx 30rpx"
            margin: {
                type: String,
                default: '0'
            },
            // 线条的类型,solid-实线,dashed-方形虚线,dotted-圆点虚线
            borderStyle: {
                type: String,
                default: 'solid'
            }
        },
        computed: {
            lineStyle() {
                let style = {};
                style.margin = this.margin;
                // 如果是水平线条,边框高度为1px,再通过transform缩小一半,就是0.5px了
                if(this.direction == 'row') {
                    // 此处采用兼容分开写,兼容nvue的写法
                    style.borderBottomWidth = '1px';
                    style.borderBottomStyle = this.borderStyle;
                    style.width = this.$u.addUnit(this.length);
                    if(this.hairLine) style.transform = 'scaleY(0.5)';
                } else {
                    // 如果是竖向线条,边框宽度为1px,再通过transform缩小一半,就是0.5px了
                    style.borderLeftWidth = '1px';
                    style.borderLeftStyle = this.borderStyle;
                    style.height = this.$u.addUnit(this.length);
                    if(this.hairLine) style.transform = 'scaleX(0.5)';
                }
                style.borderColor = this.color;
                return style;
            }
        }
    }
</script>
<style scoped lang="scss">
    @import "../../libs/css/style.components.scss";
    .u-line {
        vertical-align: middle;
    }
</style>
uview-ui/components/u-link/u-link.vue
New file
@@ -0,0 +1,89 @@
<template>
    <text class="u-link" @tap.stop="openLink" :style="{
        color: color,
        fontSize: fontSize + 'rpx',
        borderBottom: underLine ? `1px solid ${lineColor ? lineColor : color}` : 'none',
        paddingBottom: underLine ? '0rpx' : '0'
    }">
        <slot></slot>
    </text>
</template>
<script>
    /**
     * link 超链接
     * @description 该组件为超链接组件,在不同平台有不同表现形式:在APP平台会通过plus环境打开内置浏览器,在小程序中把链接复制到粘贴板,同时提示信息,在H5中通过window.open打开链接。
     * @tutorial https://www.uviewui.com/components/link.html
     * @property {String} color 文字颜色(默认#606266)
     * @property {String Number} font-size 字体大小,单位rpx(默认28)
     * @property {Boolean} under-line 是否显示下划线(默认false)
     * @property {String} href 跳转的链接,要带上http(s)
     * @property {String} line-color 下划线颜色,默认同color参数颜色
     * @property {String} mp-tips 各个小程序平台把链接复制到粘贴板后的提示语(默认“链接已复制,请在浏览器打开”)
     * @example <u-link href="http://www.uviewui.com">蜀道难,难于上青天</u-link>
     */
    export default {
        name: "u-link",
        props: {
            // 文字颜色
            color: {
                type: String,
                default: '#2979ff'
            },
            // 字体大小,单位rpx
            fontSize: {
                type: [String, Number],
                default: 28
            },
            // 是否显示下划线
            underLine: {
                type: Boolean,
                default: false
            },
            // 要跳转的链接
            href: {
                type: String,
                default: ''
            },
            // 小程序中复制到粘贴板的提示语
            mpTips: {
                type: String,
                default: '链接已复制,请在浏览器打开'
            },
            // 下划线颜色
            lineColor: {
                type: String,
                default: ''
            }
        },
        methods: {
            openLink() {
                // #ifdef APP-PLUS
                plus.runtime.openURL(this.href)
                // #endif
                // #ifdef H5
                window.open(this.href)
                // #endif
                // #ifdef MP
                uni.setClipboardData({
                    data: this.href,
                    success: () => {
                        uni.hideToast();
                        this.$nextTick(() => {
                            this.$u.toast(this.mpTips);
                        })
                    }
                });
                // #endif
            }
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-link {
        line-height: 1;
    }
</style>
uview-ui/components/u-loading-page/u-loading-page.vue
New file
@@ -0,0 +1,25 @@
<template>
    <view class="u-loading-page">
    </view>
</template>
<script>
    export default {
        props: {
        },
        data() {
            return {
            }
        },
        methods: {
        }
    }
</script>
<style lang="scss" scoped>
</style>
uview-ui/components/u-loading/u-loading.vue
New file
@@ -0,0 +1,103 @@
<template>
    <view v-if="show" class="u-loading" :class="mode == 'circle' ? 'u-loading-circle' : 'u-loading-flower'" :style="[cricleStyle]">
    </view>
</template>
<script>
    /**
     * loading 加载动画
     * @description 警此组件为一个小动画,目前用在uView的loadmore加载更多和switch开关等组件的正在加载状态场景。
     * @tutorial https://www.uviewui.com/components/loading.html
     * @property {String} mode 模式选择,见官网说明(默认circle)
     * @property {String} color 动画活动区域的颜色,只对 mode = flower 模式有效(默认#c7c7c7)
     * @property {String Number} size 加载图标的大小,单位rpx(默认34)
     * @property {Boolean} show 是否显示动画(默认true)
     * @example <u-loading mode="circle"></u-loading>
     */
    export default {
        name: "u-loading",
        props: {
            // 动画的类型
            mode: {
                type: String,
                default: 'circle'
            },
            // 动画的颜色
            color: {
                type: String,
                default: '#c7c7c7'
            },
            // 加载图标的大小,单位rpx
            size: {
                type: [String, Number],
                default: '34'
            },
            // 是否显示动画
            show: {
                type: Boolean,
                default: true
            }
        },
        computed: {
            // 加载中圆圈动画的样式
            cricleStyle() {
                let style = {};
                style.width = this.size + 'rpx';
                style.height = this.size + 'rpx';
                if (this.mode == 'circle') style.borderColor = `#e4e4e4 #e4e4e4 #e4e4e4 ${this.color ? this.color : '#c7c7c7'}`;
                return style;
            },
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-loading-circle {
        /* #ifndef APP-NVUE */
        display: inline-flex;
        /* #endif */
        vertical-align: middle;
        width: 28rpx;
        height: 28rpx;
        background: 0 0;
        border-radius: 50%;
        border: 2px solid;
        border-color: #e5e5e5 #e5e5e5 #e5e5e5 #8f8d8e;
        animation: u-circle 1s linear infinite;
    }
    .u-loading-flower {
        width: 20px;
        height: 20px;
        display: inline-block;
        vertical-align: middle;
        -webkit-animation: a 1s steps(12) infinite;
        animation: u-flower 1s steps(12) infinite;
        background: transparent url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iMTIwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgxMDB2MTAwSDB6Ii8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTlFOUU5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTMwKSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iIzk4OTY5NyIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgzMCAxMDUuOTggNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjOUI5OTlBIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDYwIDc1Ljk4IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0EzQTFBMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NSA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNBQkE5QUEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDU4LjY2IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0IyQjJCMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgxNTAgNTQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjQkFCOEI5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA1MCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDMkMwQzEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTE1MCA0NS45OCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDQkNCQ0IiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTEyMCA0MS4zNCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNEMkQyRDIiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDM1IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0RBREFEQSIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgtNjAgMjQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTJFMkUyIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKC0zMCAtNS45OCA2NSkiLz48L3N2Zz4=) no-repeat;
        background-size: 100%;
    }
    @keyframes u-flower {
        0% {
            -webkit-transform: rotate(0deg);
            transform: rotate(0deg);
        }
        to {
            -webkit-transform: rotate(1turn);
            transform: rotate(1turn);
        }
    }
    @-webkit-keyframes u-circle {
        0% {
            transform: rotate(0);
        }
        100% {
            transform: rotate(360deg);
        }
    }
</style>
uview-ui/components/u-loadmore/u-loadmore.vue
New file
@@ -0,0 +1,203 @@
<template>
    <view class="u-load-more-wrap" :style="{
        backgroundColor: bgColor,
        marginBottom: marginBottom + 'rpx',
        marginTop: marginTop + 'rpx',
        height: $u.addUnit(height)
    }">
        <u-line color="#d4d4d4" length="50"></u-line>
        <!-- 加载中和没有更多的状态才显示两边的横线 -->
        <view :class="status == 'loadmore' || status == 'nomore' ? 'u-more' : ''" class="u-load-more-inner">
            <view class="u-loadmore-icon-wrap">
                <u-loading class="u-loadmore-icon" :color="iconColor" :mode="iconType == 'circle' ? 'circle' : 'flower'" :show="status == 'loading' && icon"></u-loading>
            </view>
            <!-- 如果没有更多的状态下,显示内容为dot(粗点),加载特定样式 -->
            <view class="u-line-1" :style="[loadTextStyle]" :class="[(status == 'nomore' && isDot == true) ? 'u-dot-text' : 'u-more-text']" @tap="loadMore">
                {{ showText }}
            </view>
        </view>
        <u-line color="#d4d4d4" length="50"></u-line>
    </view>
</template>
<script>
    /**
     * loadmore 加载更多
     * @description 此组件一般用于标识页面底部加载数据时的状态。
     * @tutorial https://www.uviewui.com/components/loadMore.html
     * @property {String} status 组件状态(默认loadmore)
     * @property {String} bg-color 组件背景颜色,在页面是非白色时会用到(默认#ffffff)
     * @property {Boolean} icon 加载中时是否显示图标(默认true)
     * @property {String} icon-type 加载中时的图标类型(默认circle)
     * @property {String} icon-color icon-type为circle时有效,加载中的动画图标的颜色(默认#b7b7b7)
     * @property {Boolean} is-dot status为nomore时,内容显示为一个"●"(默认false)
     * @property {String} color 字体颜色(默认#606266)
     * @property {String Number} margin-top 到上一个相邻元素的距离
     * @property {String Number} margin-bottom 到下一个相邻元素的距离
     * @property {Object} load-text 自定义显示的文字,见上方说明示例
     * @event {Function} loadmore status为loadmore时,点击组件会发出此事件
     * @example <u-loadmore :status="status" icon-type="iconType" load-text="loadText" />
     */
    export default {
        name: "u-loadmore",
        props: {
            // 组件背景色
            bgColor: {
                type: String,
                default: 'transparent'
            },
            // 是否显示加载中的图标
            icon: {
                type: Boolean,
                default: true
            },
            // 字体大小
            fontSize: {
                type: String,
                default: '28'
            },
            // 字体颜色
            color: {
                type: String,
                default: '#606266'
            },
            // 组件状态,loadmore-加载前的状态,loading-加载中的状态,nomore-没有更多的状态
            status: {
                type: String,
                default: 'loadmore'
            },
            // 加载中状态的图标,flower-花朵状图标,circle-圆圈状图标
            iconType: {
                type: String,
                default: 'circle'
            },
            // 显示的文字
            loadText: {
                type: Object,
                default () {
                    return {
                        loadmore: '加载更多',
                        loading: '正在加载...',
                        nomore: '没有更多了'
                    }
                }
            },
            // 在“没有更多”状态下,是否显示粗点
            isDot: {
                type: Boolean,
                default: false
            },
            // 加载中显示圆圈动画时,动画的颜色
            iconColor: {
                type: String,
                default: '#b7b7b7'
            },
            // 上边距
            marginTop: {
                type: [String, Number],
                default: 0
            },
            // 下边距
            marginBottom: {
                type: [String, Number],
                default: 0
            },
            // 高度,单位rpx
            height: {
                type: [String, Number],
                default: 'auto'
            }
        },
        data() {
            return {
                // 粗点
                dotText: "●"
            }
        },
        computed: {
            // 加载的文字显示的样式
            loadTextStyle() {
                return {
                    color: this.color,
                    fontSize: this.fontSize + 'rpx',
                    position: 'relative',
                    zIndex: 1,
                    backgroundColor: this.bgColor,
                    // 如果是加载中状态,动画和文字需要距离近一点
                }
            },
            // 加载中圆圈动画的样式
            cricleStyle() {
                return {
                    borderColor: `#e5e5e5 #e5e5e5 #e5e5e5 ${this.circleColor}`
                }
            },
            // 加载中花朵动画形式
            // 动画由base64图片生成,暂不支持修改
            flowerStyle() {
                return {
                }
            },
            // 显示的提示文字
            showText() {
                let text = '';
                if(this.status == 'loadmore') text = this.loadText.loadmore;
                else if(this.status == 'loading') text = this.loadText.loading;
                else if(this.status == 'nomore' && this.isDot) text = this.dotText;
                else text = this.loadText.nomore;
                return text;
            }
        },
        methods: {
            loadMore() {
                // 只有在“加载更多”的状态下才发送点击事件,内容不满一屏时无法触发底部上拉事件,所以需要点击来触发
                if(this.status == 'loadmore') this.$emit('loadmore');
            }
        }
    }
</script>
<style scoped lang="scss">
    @import "../../libs/css/style.components.scss";
    /* #ifdef MP */
    // 在mp.scss中,赋予了u-line为flex: 1,这里需要一个明确的长度,所以重置掉它
    // 在组件内部,把组件名(u-line)当做选择器,在微信开发工具会提示不合法,但不影响使用
    u-line {
        flex: none;
    }
    /* #endif */
    .u-load-more-wrap {
        @include vue-flex;
        justify-content: center;
        align-items: center;
    }
    .u-load-more-inner {
        @include vue-flex;
        justify-content: center;
        align-items: center;
        padding: 0 12rpx;
    }
    .u-more {
        position: relative;
        @include vue-flex;
        justify-content: center;
    }
    .u-dot-text {
        font-size: 28rpx;
    }
    .u-loadmore-icon-wrap {
        margin-right: 8rpx;
    }
    .u-loadmore-icon {
        @include vue-flex;
        align-items: center;
        justify-content: center;
    }
</style>
uview-ui/components/u-mask/u-mask.vue
New file
@@ -0,0 +1,123 @@
<template>
    <view class="u-mask" hover-stop-propagation :style="[maskStyle, zoomStyle]" @tap="click" @touchmove.stop.prevent="() => {}" :class="{
        'u-mask-zoom': zoom,
        'u-mask-show': show
    }">
        <slot />
    </view>
</template>
<script>
    /**
     * mask 遮罩
     * @description 创建一个遮罩层,用于强调特定的页面元素,并阻止用户对遮罩下层的内容进行操作,一般用于弹窗场景
     * @tutorial https://www.uviewui.com/components/mask.html
     * @property {Boolean} show 是否显示遮罩(默认false)
     * @property {String Number} z-index z-index 层级(默认1070)
     * @property {Object} custom-style 自定义样式对象,见上方说明
     * @property {String Number} duration 动画时长,单位毫秒(默认300)
     * @property {Boolean} zoom 是否使用scale对遮罩进行缩放(默认true)
     * @property {Boolean} mask-click-able 遮罩是否可点击,为false时点击不会发送click事件(默认true)
     * @event {Function} click mask-click-able为true时,点击遮罩发送此事件
     * @example <u-mask :show="show" @click="show = false"></u-mask>
     */
    export default {
        name: "u-mask",
        props: {
            // 是否显示遮罩
            show: {
                type: Boolean,
                default: false
            },
            // 层级z-index
            zIndex: {
                type: [Number, String],
                default: ''
            },
            // 用户自定义样式
            customStyle: {
                type: Object,
                default () {
                    return {}
                }
            },
            // 遮罩的动画样式, 是否使用使用zoom进行scale进行缩放
            zoom: {
                type: Boolean,
                default: true
            },
            // 遮罩的过渡时间,单位为ms
            duration: {
                type: [Number, String],
                default: 300
            },
            // 是否可以通过点击遮罩进行关闭
            maskClickAble: {
                type: Boolean,
                default: true
            }
        },
        data() {
            return {
                zoomStyle: {
                    transform: ''
                },
                scale: 'scale(1.2, 1.2)'
            }
        },
        watch: {
            show(n) {
                if(n && this.zoom) {
                    // 当展示遮罩的时候,设置scale为1,达到缩小(原来为1.2)的效果
                    this.zoomStyle.transform = 'scale(1, 1)';
                } else if(!n && this.zoom) {
                    // 当隐藏遮罩的时候,设置scale为1.2,达到放大(因为显示遮罩时已重置为1)的效果
                    this.zoomStyle.transform = this.scale;
                }
            }
        },
        computed: {
            maskStyle() {
                let style = {};
                style.backgroundColor = "rgba(0, 0, 0, 0.6)";
                if(this.show) style.zIndex = this.zIndex ? this.zIndex : this.$u.zIndex.mask;
                else style.zIndex = -1;
                style.transition = `all ${this.duration / 1000}s ease-in-out`;
                // 判断用户传递的对象是否为空,不为空就进行合并
                if (Object.keys(this.customStyle).length) style = {
                    ...style,
                    ...this.customStyle
                };
                return style;
            }
        },
        methods: {
            click() {
                if (!this.maskClickAble) return;
                this.$emit('click');
            }
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-mask {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        opacity: 0;
        transition: transform 0.3s;
    }
    .u-mask-show {
        opacity: 1;
    }
    .u-mask-zoom {
        transform: scale(1.2, 1.2);
    }
</style>
uview-ui/components/u-message-input/u-message-input.vue
New file
@@ -0,0 +1,311 @@
<template>
    <view class="u-char-box">
        <view class="u-char-flex">
            <input :disabled="disabledKeyboard" :value="valueModel" type="number" :focus="focus" :maxlength="maxlength" class="u-input" @input="getVal"/>
            <view v-for="(item, index) in loopCharArr" :key="index">
                <view :class="[breathe && charArrLength == index ? 'u-breathe' : '', 'u-char-item',
                charArrLength === index && mode == 'box' ? 'u-box-active' : '',
                mode === 'box' ? 'u-box' : '']" :style="{
                    fontWeight: bold ? 'bold' : 'normal',
                    fontSize: fontSize + 'rpx',
                    width: width + 'rpx',
                    height: width + 'rpx',
                    color: inactiveColor,
                    borderColor: charArrLength === index && mode == 'box' ? activeColor : inactiveColor
                }">
                    <view class="u-placeholder-line" :style="{
                            display: charArrLength === index ? 'block' : 'none',
                            height: width * 0.5 +'rpx'
                        }"
                        v-if="mode !== 'middleLine'"
                    ></view>
                    <view v-if="mode === 'middleLine' && charArrLength <= index" :class="[breathe && charArrLength == index ? 'u-breathe' : '', charArrLength === index ? 'u-middle-line-active' : '']"
                     class="u-middle-line" :style="{height: bold ? '4px' : '2px', background: charArrLength === index ? activeColor : inactiveColor}"></view>
                    <view v-if="mode === 'bottomLine'" :class="[breathe && charArrLength == index ? 'u-breathe' : '', charArrLength === index ? 'u-buttom-line-active' : '']"
                     class="u-bottom-line" :style="{height: bold ? '4px' : '2px', background: charArrLength === index ? activeColor : inactiveColor}"></view>
                    <block v-if="!dotFill"> {{ charArr[index] ? charArr[index] : ''}}</block>
                    <block v-else>
                        <text class="u-dot">{{ charArr[index] ? '●' : ''}}</text>
                    </block>
                </view>
            </view>
        </view>
    </view>
</template>
<script>
    /**
     * messageInput 验证码输入框
     * @description 该组件一般用于验证用户短信验证码的场景,也可以结合uView的键盘组件使用
     * @tutorial https://www.uviewui.com/components/messageInput.html
     * @property {String Number} maxlength 输入字符个数(默认4)
     * @property {Boolean} dot-fill 是否用圆点填充(默认false)
     * @property {String} mode 模式选择,见上方"基本使用"说明(默认box)
     * @property {String Number} value 预置值
     * @property {Boolean} breathe 是否开启呼吸效果,见上方说明(默认true)
     * @property {Boolean} focus 是否自动获取焦点(默认false)
     * @property {Boolean} bold 字体和输入横线是否加粗(默认true)
     * @property {String Number} font-size 字体大小,单位rpx(默认60)
     * @property {String} active-color 当前激活输入框的样式(默认#2979ff)
     * @property {String} inactive-color 非激活输入框的样式,文字颜色同此值(默认#606266)
     * @property {String | Number} width 输入框宽度,单位rpx,高等于宽(默认80)
     * @property {Boolean} disabled-keyboard 禁止点击输入框唤起系统键盘(默认false)
     * @event {Function} change 输入内容发生改变时触发,具体见官网说明
     * @event {Function} finish 输入字符个数达maxlength值时触发,见官网说明
     * @example <u-message-input mode="bottomLine"></u-message-input>
     */
    export default {
        name: "u-message-input",
        props: {
            // 最大输入长度
            maxlength: {
                type: [Number, String],
                default: 4
            },
            // 是否用圆点填充
            dotFill: {
                type: Boolean,
                default: false
            },
            // 显示模式,box-盒子模式,bottomLine-横线在底部模式,middleLine-横线在中部模式
            mode: {
                type: String,
                default: "box"
            },
            // 预置值
            value: {
                type: [String, Number],
                default: ''
            },
            // 当前激活输入item,是否带有呼吸效果
            breathe: {
                type: Boolean,
                default: true
            },
            // 是否自动获取焦点
            focus: {
                type: Boolean,
                default: false
            },
            // 字体是否加粗
            bold: {
                type: Boolean,
                default: false
            },
            // 字体大小
            fontSize: {
                type: [String, Number],
                default: 60
            },
            // 激活样式
            activeColor: {
                type: String,
                default: '#2979ff'
            },
            // 未激活的样式
            inactiveColor: {
                type: String,
                default: '#606266'
            },
            // 输入框的大小,单位rpx,宽等于高
            width: {
                type: [Number, String],
                default: '80'
            },
            // 是否隐藏原生键盘,如果想用自定义键盘的话,需设置此参数为true
            disabledKeyboard: {
                type: Boolean,
                default: false
            }
        },
        watch: {
            // maxlength: {
            //     // 此值设置为true,会在组件加载后无需maxlength变化就会执行一次本监听函数,无需再created生命周期中处理
            //     immediate: true,
            //     handler(val) {
            //         this.maxlength = Number(val);
            //     }
            // },
            value: {
                immediate: true,
                handler(val) {
                    // 转为字符串
                    val = String(val);
                    // 超出部分截掉
                    this.valueModel = val.substring(0, this.maxlength);
                }
            },
        },
        data() {
            return {
                valueModel: ""
            }
        },
        computed: {
            // 是否显示呼吸灯效果
            animationClass() {
                return (index) => {
                    if (this.breathe && this.charArr.length == index) return 'u-breathe';
                    else return '';
                }
            },
            // 用于显示字符
            charArr() {
                return this.valueModel.split('');
            },
            charArrLength() {
                return this.charArr.length;
            },
            // 根据长度,循环输入框的个数,因为头条小程序数值不能用于v-for
            loopCharArr() {
                return new Array(this.maxlength);
            }
        },
        methods: {
            getVal(e) {
                let {
                    value
                } = e.detail
                this.valueModel = value;
                // 判断长度是否超出了maxlength值,理论上不会发生,因为input组件设置了maxlength属性值
                if (String(value).length > this.maxlength) return;
                // 未达到maxlength之前,发送change事件,达到后发送finish事件
                this.$emit('change', value);
                if (String(value).length == this.maxlength) {
                    this.$emit('finish', value);
                }
            }
        }
    }
</script>
<style scoped lang="scss">
    @import "../../libs/css/style.components.scss";
    @keyframes breathe {
        0% {
            opacity: 0.3;
        }
        50% {
            opacity: 1;
        }
        100% {
            opacity: 0.3;
        }
    }
    .u-char-box {
        text-align: center;
    }
    .u-char-flex {
        @include vue-flex;
        justify-content: center;
        flex-wrap: wrap;
        position: relative;
    }
    .u-input {
        position: absolute;
        top: 0;
        left: -100%;
        width: 200%;
        height: 100%;
        text-align: left;
        z-index: 9;
        opacity: 0;
        background: none;
    }
    .u-char-item {
        position: relative;
        width: 90rpx;
        height: 90rpx;
        margin: 10rpx 10rpx;
        font-size: 60rpx;
        font-weight: bold;
        color: $u-main-color;
        line-height: 90rpx;
        @include vue-flex;
        justify-content: center;
        align-items: center;
    }
    .u-middle-line {
        border: none;
    }
    .u-box {
        box-sizing: border-box;
        border: 2rpx solid #cccccc;
        border-radius: 6rpx;
    }
    .u-box-active {
        overflow: hidden;
        animation-timing-function: ease-in-out;
        animation-duration: 1500ms;
        animation-iteration-count: infinite;
        animation-direction: alternate;
        border: 2rpx solid $u-type-primary;
    }
    .u-middle-line-active {
        background: $u-type-primary;
    }
    .u-breathe {
        animation: breathe 2s infinite ease;
    }
    .u-placeholder-line {
        /* #ifndef APP-NVUE */
        display: none;
        /* #endif */
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        width: 2rpx;
        height: 40rpx;
        background: #333333;
        animation: twinkling 1.5s infinite ease;
    }
    .u-animation-breathe {
        animation-name: breathe;
    }
    .u-dot {
        font-size: 34rpx;
        line-height: 34rpx;
    }
    .u-middle-line {
        height: 4px;
        background: #000000;
        width: 80%;
        position: absolute;
        border-radius: 2px;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
    }
    .u-buttom-line-active {
        background: $u-type-primary;
    }
    .u-bottom-line {
        height: 4px;
        background: #000000;
        width: 80%;
        position: absolute;
        border-radius: 2px;
        bottom: 0;
        left: 50%;
        transform: translate(-50%);
    }
</style>
uview-ui/components/u-modal/u-modal.vue
New file
@@ -0,0 +1,283 @@
<template>
    <view>
        <u-popup :zoom="zoom" mode="center" :popup="false" :z-index="uZIndex" v-model="value" :length="width"
         :mask-close-able="maskCloseAble" :border-radius="borderRadius" @close="popupClose" :negative-top="negativeTop">
            <view class="u-model">
                <view v-if="showTitle" class="u-model__title u-line-1" :style="[titleStyle]">{{ title }}</view>
                <view class="u-model__content">
                    <view :style="[contentStyle]" v-if="$slots.default  || $slots.$default">
                        <slot />
                    </view>
                    <view v-else class="u-model__content__message" :style="[contentStyle]">{{ content }}</view>
                </view>
                <view class="u-model__footer u-border-top" v-if="showCancelButton || showConfirmButton">
                    <view v-if="showCancelButton" :hover-stay-time="100" hover-class="u-model__btn--hover" class="u-model__footer__button"
                     :style="[cancelBtnStyle]" @tap="cancel">
                        {{cancelText}}
                    </view>
                    <view v-if="showConfirmButton || $slots['confirm-button']" :hover-stay-time="100" :hover-class="asyncClose ? 'none' : 'u-model__btn--hover'"
                     class="u-model__footer__button hairline-left" :style="[confirmBtnStyle]" @tap="confirm">
                        <slot v-if="$slots['confirm-button']" name="confirm-button"></slot>
                        <block v-else>
                            <u-loading mode="circle" :color="confirmColor" v-if="loading"></u-loading>
                            <block v-else>
                                {{confirmText}}
                            </block>
                        </block>
                    </view>
                </view>
            </view>
        </u-popup>
    </view>
</template>
<script>
    /**
     * modal 模态框
     * @description 弹出模态框,常用于消息提示、消息确认、在当前页面内完成特定的交互操作
     * @tutorial https://www.uviewui.com/components/modal.html
     * @property {Boolean} value 是否显示模态框
     * @property {String | Number} z-index 层级
     * @property {String} title 模态框标题(默认"提示")
     * @property {String | Number} width 模态框宽度(默认600)
     * @property {String} content 模态框内容(默认"内容")
     * @property {Boolean} show-title 是否显示标题(默认true)
     * @property {Boolean} async-close 是否异步关闭,只对确定按钮有效(默认false)
     * @property {Boolean} show-confirm-button 是否显示确认按钮(默认true)
     * @property {Stringr | Number} negative-top modal往上偏移的值
     * @property {Boolean} show-cancel-button 是否显示取消按钮(默认false)
     * @property {Boolean} mask-close-able 是否允许点击遮罩关闭modal(默认false)
     * @property {String} confirm-text 确认按钮的文字内容(默认"确认")
     * @property {String} cancel-text 取消按钮的文字内容(默认"取消")
     * @property {String} cancel-color 取消按钮的颜色(默认"#606266")
     * @property {String} confirm-color 确认按钮的文字内容(默认"#2979ff")
     * @property {String | Number} border-radius 模态框圆角值,单位rpx(默认16)
     * @property {Object} title-style 自定义标题样式,对象形式
     * @property {Object} content-style 自定义内容样式,对象形式
     * @property {Object} cancel-style 自定义取消按钮样式,对象形式
     * @property {Object} confirm-style 自定义确认按钮样式,对象形式
     * @property {Boolean} zoom 是否开启缩放模式(默认true)
     * @event {Function} confirm 确认按钮被点击
     * @event {Function} cancel 取消按钮被点击
     * @example <u-modal :src="title" :content="content"></u-modal>
     */
    export default {
        name: 'u-modal',
        props: {
            // 是否显示Modal
            value: {
                type: Boolean,
                default: false
            },
            // 层级z-index
            zIndex: {
                type: [Number, String],
                default: ''
            },
            // 标题
            title: {
                type: [String],
                default: '提示'
            },
            // 弹窗宽度,可以是数值(rpx),百分比,auto等
            width: {
                type: [Number, String],
                default: 600
            },
            // 弹窗内容
            content: {
                type: String,
                default: '内容'
            },
            // 是否显示标题
            showTitle: {
                type: Boolean,
                default: true
            },
            // 是否显示确认按钮
            showConfirmButton: {
                type: Boolean,
                default: true
            },
            // 是否显示取消按钮
            showCancelButton: {
                type: Boolean,
                default: false
            },
            // 确认文案
            confirmText: {
                type: String,
                default: '确认'
            },
            // 取消文案
            cancelText: {
                type: String,
                default: '取消'
            },
            // 确认按钮颜色
            confirmColor: {
                type: String,
                default: '#2979ff'
            },
            // 取消文字颜色
            cancelColor: {
                type: String,
                default: '#606266'
            },
            // 圆角值
            borderRadius: {
                type: [Number, String],
                default: 16
            },
            // 标题的样式
            titleStyle: {
                type: Object,
                default () {
                    return {}
                }
            },
            // 内容的样式
            contentStyle: {
                type: Object,
                default () {
                    return {}
                }
            },
            // 取消按钮的样式
            cancelStyle: {
                type: Object,
                default () {
                    return {}
                }
            },
            // 确定按钮的样式
            confirmStyle: {
                type: Object,
                default () {
                    return {}
                }
            },
            // 是否开启缩放效果
            zoom: {
                type: Boolean,
                default: true
            },
            // 是否异步关闭,只对确定按钮有效
            asyncClose: {
                type: Boolean,
                default: false
            },
            // 是否允许点击遮罩关闭modal
            maskCloseAble: {
                type: Boolean,
                default: false
            },
            // 给一个负的margin-top,往上偏移,避免和键盘重合的情况
            negativeTop: {
                type: [String, Number],
                default: 0
            }
        },
        data() {
            return {
                loading: false, // 确认按钮是否正在加载中
            }
        },
        computed: {
            cancelBtnStyle() {
                return Object.assign({
                    color: this.cancelColor
                }, this.cancelStyle);
            },
            confirmBtnStyle() {
                return Object.assign({
                    color: this.confirmColor
                }, this.confirmStyle);
            },
            uZIndex() {
                return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
            }
        },
        watch: {
            // 如果是异步关闭时,外部修改v-model的值为false时,重置内部的loading状态
            // 避免下次打开的时候,状态混乱
            value(n) {
                if (n === true) this.loading = false;
            }
        },
        methods: {
            confirm() {
                // 异步关闭
                if (this.asyncClose) {
                    this.loading = true;
                } else {
                    this.$emit('input', false);
                }
                this.$emit('confirm');
            },
            cancel() {
                this.$emit('cancel');
                this.$emit('input', false);
                // 目前popup弹窗关闭有一个延时操作,此处做一个延时
                // 避免确认按钮文字变成了"确定"字样,modal还没消失,造成视觉不好的效果
                setTimeout(() => {
                    this.loading = false;
                }, 300);
            },
            // 点击遮罩关闭modal,设置v-model的值为false,否则无法第二次弹起modal
            popupClose() {
                this.$emit('input', false);
            },
            // 清除加载中的状态
            clearLoading() {
                this.loading = false;
            }
        }
    };
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-model {
        height: auto;
        overflow: hidden;
        font-size: 32rpx;
        background-color: #fff;
        &__btn--hover {
            background-color: rgb(230, 230, 230);
        }
        &__title {
            padding-top: 48rpx;
            font-weight: 500;
            text-align: center;
            color: $u-main-color;
        }
        &__content {
            &__message {
                padding: 48rpx;
                font-size: 30rpx;
                text-align: center;
                color: $u-content-color;
            }
        }
        &__footer {
            @include vue-flex;
            &__button {
                flex: 1;
                height: 100rpx;
                line-height: 100rpx;
                font-size: 32rpx;
                box-sizing: border-box;
                cursor: pointer;
                text-align: center;
                border-radius: 4rpx;
            }
        }
    }
</style>
uview-ui/components/u-navbar/u-navbar.vue
New file
@@ -0,0 +1,315 @@
<template>
    <view class="">
        <view class="u-navbar" :style="[navbarStyle]" :class="{ 'u-navbar-fixed': isFixed, 'u-border-bottom': borderBottom }">
            <view class="u-status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
            <view class="u-navbar-inner" :style="[navbarInnerStyle]">
                <view class="u-back-wrap" v-if="isBack" @tap="goBack">
                    <view class="u-icon-wrap">
                        <u-icon :name="backIconName" :color="backIconColor" :size="backIconSize"></u-icon>
                    </view>
                    <view class="u-icon-wrap u-back-text u-line-1" v-if="backText" :style="[backTextStyle]">{{ backText }}</view>
                </view>
                <view class="u-navbar-content-title" v-if="title" :style="[titleStyle]">
                    <view
                        class="u-title u-line-1"
                        :style="{
                            color: titleColor,
                            fontSize: titleSize + 'rpx',
                            fontWeight: titleBold ? 'bold' : 'normal'
                        }">
                        {{ title }}
                    </view>
                </view>
                <view class="u-slot-content">
                    <slot></slot>
                </view>
                <view class="u-slot-right">
                    <slot name="right"></slot>
                </view>
            </view>
        </view>
        <!-- 解决fixed定位后导航栏塌陷的问题 -->
        <view class="u-navbar-placeholder" v-if="isFixed && !immersive" :style="{ width: '100%', height: Number(navbarHeight) + statusBarHeight + 'px' }"></view>
    </view>
</template>
<script>
    // 获取系统状态栏的高度
    let systemInfo = uni.getSystemInfoSync();
    let menuButtonInfo = {};
    // 如果是小程序,获取右上角胶囊的尺寸信息,避免导航栏右侧内容与胶囊重叠(支付宝小程序非本API,尚未兼容)
    // #ifdef MP-WEIXIN || MP-BAIDU || MP-TOUTIAO || MP-QQ
    menuButtonInfo = uni.getMenuButtonBoundingClientRect();
    // #endif
    /**
     * navbar 自定义导航栏
     * @description 此组件一般用于在特殊情况下,需要自定义导航栏的时候用到,一般建议使用uniapp自带的导航栏。
     * @tutorial https://www.uviewui.com/components/navbar.html
     * @property {String Number} height 导航栏高度(不包括状态栏高度在内,内部自动加上),注意这里的单位是px(默认44)
     * @property {String} back-icon-color 左边返回图标的颜色(默认#606266)
     * @property {String} back-icon-name 左边返回图标的名称,只能为uView自带的图标(默认arrow-left)
     * @property {String Number} back-icon-size 左边返回图标的大小,单位rpx(默认30)
     * @property {String} back-text 返回图标右边的辅助提示文字
     * @property {Object} back-text-style 返回图标右边的辅助提示文字的样式,对象形式(默认{ color: '#606266' })
     * @property {String} title 导航栏标题,如设置为空字符,将会隐藏标题占位区域
     * @property {String Number} title-width 导航栏标题的最大宽度,内容超出会以省略号隐藏,单位rpx(默认250)
     * @property {String} title-color 标题的颜色(默认#606266)
     * @property {String Number} title-size 导航栏标题字体大小,单位rpx(默认32)
     * @property {Function} custom-back 自定义返回逻辑方法
     * @property {String Number} z-index 固定在顶部时的z-index值(默认980)
     * @property {Boolean} is-back 是否显示导航栏左边返回图标和辅助文字(默认true)
     * @property {Object} background 导航栏背景设置,见官网说明(默认{ background: '#ffffff' })
     * @property {Boolean} is-fixed 导航栏是否固定在顶部(默认true)
     * @property {Boolean} immersive 沉浸式,允许fixed定位后导航栏塌陷,仅fixed定位下生效(默认false)
     * @property {Boolean} border-bottom 导航栏底部是否显示下边框,如定义了较深的背景颜色,可取消此值(默认true)
     * @example <u-navbar back-text="返回" title="剑未配妥,出门已是江湖"></u-navbar>
     */
    export default {
        name: "u-navbar",
        props: {
            // 导航栏高度,单位px,非rpx
            height: {
                type: [String, Number],
                default: ''
            },
            // 返回箭头的颜色
            backIconColor: {
                type: String,
                default: '#606266'
            },
            // 左边返回的图标
            backIconName: {
                type: String,
                default: 'nav-back'
            },
            // 左边返回图标的大小,rpx
            backIconSize: {
                type: [String, Number],
                default: '44'
            },
            // 返回的文字提示
            backText: {
                type: String,
                default: ''
            },
            // 返回的文字的 样式
            backTextStyle: {
                type: Object,
                default () {
                    return {
                        color: '#606266'
                    }
                }
            },
            // 导航栏标题
            title: {
                type: String,
                default: ''
            },
            // 标题的宽度,如果需要自定义右侧内容,且右侧内容很多时,可能需要减少这个宽度,单位rpx
            titleWidth: {
                type: [String, Number],
                default: '250'
            },
            // 标题的颜色
            titleColor: {
                type: String,
                default: '#606266'
            },
            // 标题字体是否加粗
            titleBold: {
                type: Boolean,
                default: false
            },
            // 标题的字体大小
            titleSize: {
                type: [String, Number],
                default: 32
            },
            isBack: {
                type: [Boolean, String],
                default: true
            },
            // 对象形式,因为用户可能定义一个纯色,或者线性渐变的颜色
            background: {
                type: Object,
                default () {
                    return {
                        background: '#ffffff'
                    }
                }
            },
            // 导航栏是否固定在顶部
            isFixed: {
                type: Boolean,
                default: true
            },
            // 是否沉浸式,允许fixed定位后导航栏塌陷,仅fixed定位下生效
            immersive: {
                type: Boolean,
                default: false
            },
            // 是否显示导航栏的下边框
            borderBottom: {
                type: Boolean,
                default: true
            },
            zIndex: {
                type: [String, Number],
                default: ''
            },
            // 自定义返回逻辑
            customBack: {
                type: Function,
                default: null
            }
        },
        data() {
            return {
                menuButtonInfo: menuButtonInfo,
                statusBarHeight: systemInfo.statusBarHeight
            };
        },
        computed: {
            // 导航栏内部盒子的样式
            navbarInnerStyle() {
                let style = {};
                // 导航栏宽度,如果在小程序下,导航栏宽度为胶囊的左边到屏幕左边的距离
                style.height = this.navbarHeight + 'px';
                // // 如果是各家小程序,导航栏内部的宽度需要减少右边胶囊的宽度
                // #ifdef MP
                let rightButtonWidth = systemInfo.windowWidth - menuButtonInfo.left;
                style.marginRight = rightButtonWidth + 'px';
                // #endif
                return style;
            },
            // 整个导航栏的样式
            navbarStyle() {
                let style = {};
                style.zIndex = this.zIndex ? this.zIndex : this.$u.zIndex.navbar;
                // 合并用户传递的背景色对象
                Object.assign(style, this.background);
                return style;
            },
            // 导航中间的标题的样式
            titleStyle() {
                let style = {};
                // #ifndef MP
                style.left = (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + 'px';
                style.right = (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + 'px';
                // #endif
                // #ifdef MP
                // 此处是为了让标题显示区域即使在小程序有右侧胶囊的情况下也能处于屏幕的中间,是通过绝对定位实现的
                let rightButtonWidth = systemInfo.windowWidth - menuButtonInfo.left;
                style.left = (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + 'px';
                style.right = rightButtonWidth - (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + rightButtonWidth +
                    'px';
                // #endif
                style.width = uni.upx2px(this.titleWidth) + 'px';
                return style;
            },
            // 转换字符数值为真正的数值
            navbarHeight() {
                // #ifdef APP-PLUS || H5
                return this.height ? this.height : 44;
                // #endif
                // #ifdef MP
                // 小程序特别处理,让导航栏高度 = 胶囊高度 + 两倍胶囊顶部与状态栏底部的距离之差(相当于同时获得了导航栏底部与胶囊底部的距离)
                // 此方法有缺陷,暂不用(会导致少了几个px),采用直接固定值的方式
                // return menuButtonInfo.height + (menuButtonInfo.top - this.statusBarHeight) * 2;//导航高度
                let height = systemInfo.platform == 'ios' ? 44 : 48;
                return this.height ? this.height : height;
                // #endif
            }
        },
        created() {},
        methods: {
            goBack() {
                // 如果自定义了点击返回按钮的函数,则执行,否则执行返回逻辑
                if (typeof this.customBack === 'function') {
                    // 在微信,支付宝等环境(H5正常),会导致父组件定义的customBack()函数体中的this变成子组件的this
                    // 通过bind()方法,绑定父组件的this,让this.customBack()的this为父组件的上下文
                    this.customBack.bind(this.$u.$parent.call(this))();
                } else {
                    uni.navigateBack();
                }
            }
        }
    };
</script>
<style scoped lang="scss">
    @import "../../libs/css/style.components.scss";
    .u-navbar {
        width: 100%;
    }
    .u-navbar-fixed {
        position: fixed;
        left: 0;
        right: 0;
        top: 0;
        z-index: 991;
    }
    .u-status-bar {
        width: 100%;
    }
    .u-navbar-inner {
        @include vue-flex;
        justify-content: space-between;
        position: relative;
        align-items: center;
    }
    .u-back-wrap {
        @include vue-flex;
        align-items: center;
        flex: 1;
        flex-grow: 0;
        padding: 14rpx 14rpx 14rpx 24rpx;
    }
    .u-back-text {
        padding-left: 4rpx;
        font-size: 30rpx;
    }
    .u-navbar-content-title {
        @include vue-flex;
        align-items: center;
        justify-content: center;
        flex: 1;
        position: absolute;
        left: 0;
        right: 0;
        height: 60rpx;
        text-align: center;
        flex-shrink: 0;
    }
    .u-navbar-centent-slot {
        flex: 1;
    }
    .u-title {
        line-height: 60rpx;
        font-size: 32rpx;
        flex: 1;
    }
    .u-navbar-right {
        flex: 1;
        @include vue-flex;
        align-items: center;
        justify-content: flex-end;
    }
    .u-slot-content {
        flex: 1;
        @include vue-flex;
        align-items: center;
    }
</style>
uview-ui/components/u-no-network/u-no-network.vue
New file
@@ -0,0 +1,233 @@
<template>
    <view class="u-no-network" v-if="!isConnected" :style="{'z-index': uZIndex}" @touchmove.stop.prevent="() => {}">
        <view class="u-inner">
            <image class="u-error-icon" :src="image" mode="widthFix"></image>
            <view class="u-tips">
                {{tips}}
            </view>
            <!-- 只有APP平台,才能跳转设置页,因为需要调用plus环境 -->
            <!-- #ifdef APP-PLUS -->
            <view class="u-to-setting">
                请检查网络,或前往<text class="u-setting-btn" @tap="openSettings">设置</text>
            </view>
            <!-- #endif -->
            <view class="u-retry" :hover-stay-time="150" @tap="retry" hover-class="u-retry-hover">
                重试
            </view>
        </view>
    </view>
</template>
<script>
    /**
     * noNetwork 无网络提示
     * @description 该组件无需任何配置,引入即可,内部自动处理所有功能和事件。
     * @tutorial https://www.uviewui.com/components/noNetwork.html
     * @property {String} tips 没有网络时的提示语(默认哎呀,网络信号丢失)
     * @property {String Number} zIndex 组件的z-index值(默认1080)
     * @property {String} image 无网络的图片提示,可用的src地址或base64图片
     * @event {Function} retry 用户点击页面的"重试"按钮时触发
     * @example <u-no-network></u-no-network>
     */
    export default {
        name: "u-no-network",
        props: {
            // 页面文字提示
            tips: {
                type: String,
                default: '哎呀,网络信号丢失'
            },
            // 一个z-index值,用于设置没有网络这个组件的层次,因为页面可能会有其他定位的元素层级过高,导致此组件被覆盖
            zIndex: {
                type: [Number, String],
                default: ''
            },
            // image 没有网络的图片提示
            image: {
                type: String,
                default: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAEYCAMAAABFglBLAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6M0U3MjVFMzQwNEY1MTFFQUE4MTNDOUEzMTVBREMxQjIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6M0U3MjVFMzUwNEY1MTFFQUE4MTNDOUEzMTVBREMxQjIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDozRTcyNUUzMjA0RjUxMUVBQTgxM0M5QTMxNUFEQzFCMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDozRTcyNUUzMzA0RjUxMUVBQTgxM0M5QTMxNUFEQzFCMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkHIU9QAAAMAUExURdHW2OWiou7u7tve4dnc3/vw8N3g4sPCwvjn5+jo6M7Q0u6+vtyEhPXY2Li+wuikpPXW1uXo6dba3Pbg4Na5u+qurqqyt/HJydjb3fjo6LrAxO7Bwey1td6MjOrs7fbc3OTn6Maytf7+/vz19eqqqrzCxvzz87a8wLO6vuqxsf78/PDFxenr7L/FyNTY26+2u/z09MjMzqy0udnZ2dvb27G4vMjN0O7v8P339+Ll5u3v8NDS1ODj5frt7bC3vP76+u24uOKamtTW2MTIy9zc3N7e3vPQ0OCTk8DGyfDDw9LR0c7S1LussNbY2fPOztLU1cLHyrK6vvji4vrv78bKzPX29/np6crP0vjk5Ozu78bLzuepqczR1MHFyMDEyOq1tfTS0vP09cLIy9DU173Ex8bGxvHy88/U18vO0LK4vM7OzsTKzcPJzPLMzM/T1sbMz8HJy+7FxbW8wNTW18XKzvb3+MjLzczP0d3e3+Dh4r3Dxtna29zd3rK5vdPU1cfM0Prq6uOenuPm58HGyfHz8/ro6Lq/w8nO0bzCxcrKytjZ2uXm5vTU1LvBxbW7v+rp6eefn/39/eLi4u3t7Ozs7Pj4+Pr6+vX19fHx8erq6vPz8+Xl5ff39+fn5/v7+/z8/PLx8fX09Pb29ri4uOTk5PHw8OPj4/Py8ubm5vn5+e3s7Pb19fTz8/f29tfX1+7t7cvQ0+zr6/Ly8u3t7fr5+efm5quzuOvq6vT19uvr6/j5+a61uuy6uvb39/T09Ojn5+jq6621uvn6+t/i5KyzuPn4+Le+wvv6+s3S1fj4+fDw8OHk5vv7/Pr7++Xk5LS7v7y8vObl5fz9/ff4+OTj48DEx8XJy+Hk5euysu/x8re9wfn5+v38/Pv8/Pr6++vt7uLi4+Pi4ubl5uXk5dPV1tbV1fHMzNPX2e7u7ejn6PT19fX19PDw7/b29f39/vnr6+Hh4a+3u+7t7q61uePi48PHyf35+enp6fLz9PPz8qmxtuDg4N/f3/Dv7////////1cfN/UAAAEAdFJOU////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wBT9wclAAAlqElEQVR42uydCXwUVZrAk5CEJBAQshACCGyjIRwBJR0It6JBGZZBkRtm5PBgFXAXHBghqAMz6LjGTrd9n7lvwn0JQhARxYNxVJTMwcwOM+7uzK6Z3VXcrnTXVr2q6q7urrvfq26c+n4egXTSVfXv993veym4JkklKdoj0IBoogHRgGiiAdGAaKIB0YBoogHRgGiiAdFEA6IBUVuwx+vqet/v1YAkhxwmcNT9gvjnsAYkGcT7eN1TtThe+1TdL2o1IEkg+rrvUV90MF9oQBIqv6ijF4a3d51XA5J4jVVXx3z587ojGpDEm/QwkO9pQJJB2CvksAYk8fJUXT31xZG63ppRTwI5QgcgR35Rd1MDkgxSX1f31P33P1V3i3u936HUyc3edWSsjuEakKRRW3r9kVv+JrRsrwbklpKTnXs+z0wfONOtAUm8rJu55oqfkZ0TOzUgCZS3Zh7s54+S5RqQhNGYMsnPISkakCSiQcjkNzUgasvUDaVsBF1fb9GXd//B0ziliPzjEA2IujLqGRaMST+cawgLNpn4q30aEDV1Va+dYRqzNy00RMoYUmdpQNTzcb8sCtuKH8w9Z4iWLuIbVzQgapmOBedDOO6c0WjgENK2vKwBUUUmvBSiUbrqxwZuOU18d5MGhEe/dI6a2WvIxA0HX1hfvH7V8gUpA9Mz3x/VuVTRLxvwdAhH5aJjBj4hv39QAxIl743ITHm6azBPnOCfVLwlc6pR3upYFbLjvIuDlEUqRYZo38JYS4gVt5rNZhMQO46byksoMeG4w8QIjtvMpJA/wP1EdZ0bKvkitkh555mBA3zSLnBlSFnNnlJvEJJ95ItW3pJAamvtZhy3m/T0g7fhthJGiG+EviYgmEJ/wHFP6GsbnlZSoicoucxmGwHVguPTBz543C9LHky/Jnql1w5KxWGo8KsThsAFYneYMOb5mlnP11ZCrwOPjfgGI8RqsZuZtUOsFmbllFhZP+zEbc8vGcqzMibv7qos3rd7Mg+TrQv2vCVkiLYcl4rDYLhKvi79FgFirDU7TPpyHHexINg9ZnOazWazyPxlVhsBzcKAKrl84cHo5zx758H0ASufWGrVBWkxup8YMepCespDV6Jfe3z9kOnc7/OjLydJx2FoJF983norAEkLqSbiw4+ZHGY7+UQhyYCDEZml8+ufzLz+RlBAPpieGYPloczYdaIb8rAMHAbD1yr5WHEBqTW7XIwZwEykBoIcrH3JrkgcL554PShRPpie/hDbE9v97oTIXz2zUhYOw0Lw4glJDMRiNtGrIo1AYUFwYZlsVbVzwyhfUJ4YO1MqWb/hsfRw6rzzPnk4DNlk2sQ/BU9aIGZaS2EOI6LLunBn+Fk+fWFpUJmcvPBSWH2dXz4A/OqTB2XiMBimgJf/NCmBGO0OykM1mW3ILur9cNq1cuATwbjk801htC8QcUQmk0NcIhWHoWKwPBdr9KwVuWvz//nVxYXIgTg9wCu1OZxGdJ+RacWhxNKazmD8snJL2DN44Z/oL/btMkgWsFr3Sr+BvmF5oP+fMpABMaaBSKHcgnTJ7tnLPLz1mW8E4ch76S9Hec47pOOgPCz/COm38EDfCEkdn4UCiNVDGg69Ay2OqSEFU7wnCFNGvcB2hHtk8Lhbdn/D6r7RMigLARDSbjgRW7QhTNBdeSEIW6ZPYXgUzZXBg3IB9n0o4y6+iAHS9/Zhc2ACMToIS27yoA5URzCe7ju9gigk5O767/sXqTz04PUPT5VzH31uv3f1o30eTb2djeQnr8IDkqYnLTlqMX7JfIAHeoOI5P3HmDSXRJv+8WC5BoQtZSv++SdhJMMgAbGSISDy1YEPYFzdJ91BhJL5Dv020sw6VdgdEMd9zfrTallERIE4ieWB2VDj+GALE1F3BtHKB0N209E/Js6Dyt28H+fNzbtXBhExIGaqiIHaetDLY/LAIHo5STtcs2+I8ZgNXtdLtqYadiY1NfXMsGEFob+QTkQEiIPwdJEvD7zX29Qj2jQ9qIqk087c1X8XzGBRL3pX9u3khZ2rsfOovyqgF8m8eIFYSjAjahxvLKdzsr2CaslK2p/rJ1BFP0ZXHvE4gJCxOsXgK4rIA7PiAkJYcidyHp1DqTufsi6ooqTQXREL+Xj8gY5PFdxRQWQM8kABi8igeIDYS+zItRX+Pm3NBwbVlVF0sSWHm8f/Ut8d40pTkJqIDgvzWUT+pByIhXCvkPPIpG78yij2w7K6/7PFiJrIe3QafjsXj266+Z1M3uld1jiB9B1L/m0uZVaWKQaClegtyM05nUf8ll1asr9Oihv5IhlCvfneWB4Y7WQ4HRhVhrN77DJ0dx75n4y8M/dGEukDvjyjFEhaSUka8moMdeMb2I9J92+vU/Jn5EToj8NQzp4fv/8FkEBwknkjFyjHyU7l5aWyg8Jx4E+3FyoEokevsOgezkjvyv46I1bkRKZR/m9RJI/7qct6ie3+0z1KLrmf0TwmF58bcr/GKwNiJ3t50ApdiBoVWQsP8Xjdjt60X6PC9vNsHrQmWxPlcaa5yPpDOeF4ii6Ugrxh8/LoqLCMzpw8UMYk5u9VBgQj3xup0LHAyshH1BIGElDB2dL1iybyCnVZWziu2GbGPDheTi4UQTt/hkJwhioWjg0rrXngq1eVADEityB0AuPNqCdkCQN5XRX/l0oAb43s+BEoSBmpDg/MzO/xMJkSyoCX0badwFMGMvKrlQCxlSPOuL9L3XjM87GqqrJIWUPlGikec6nLmiiYvqB8rxKXKJC+95KLhMIA6DwKlo4yo46WB138+DD2+QRCQP5TpRhxA9WFEtp0QESpojUJuwuokHIPl0GZF45A7iVNR35fhsNI8NVoGCVcJPEHV8NVaIn8RbWonSrubjQY6CKApI4fo9OKO8m4MZYJO5kFTDjlahHhyV3MF7KBODwofaxOqhD3U87HY6ECkb8YVQMSpPy9nDV+mfl2C+j8iGFSlpeXN+wBVt4kP6Sz7hVLn/ABQVoFWbeVy78Ke75/tgf+zx1UU7pYPSmZsu7FSTHhyPrl0yl4QmllUP1AjMv1qAIgGEIgur0c8UdiZWl4B8pM2fdDMNEbcTuWFs6ugCgkL1wDoWJ04ov55P9TFQAx8fsQccuahKR3RSokDI9pim7JQn6ECb+LUl15hFuVWsB4W6tDkQmxVnIfzV02DlcExISKBxUJrwkml9BVmT2Kb8sOfGG9w4pnADf39gzGmFPq6/bUM1K6SlN4a+moMlkDqERRkvHo56f8jAVx3JnVDDJerjOhDO8ZJiLMyJXa4pvCn+tFUys8CeznC8mF463TCk16jDkhwhMHneJdTZZAHlg9LLdMVgKcvzqFpm8UNO8O/W1S8fiW5WOVXo7v/qxp1tUhY15WJvvnU/iz7w50FZBRScVjBIXi9ELgaj0d9z0W3B6K0XF4QFxIgMykMndqPm4sm+5Mbf8om/MFM+kOFAzL8cPZ/FxAKK0HMnC4QKwoTMhvgQG587/UBPK93r3/mO1tr/h5794Y1/cH0lXkYCOGgW7sSZfjv9GMAsUqRChbg0hhdQZVBsIIFxC61YHwwrMxrGcMHKUVzyPij3VMethELoMOxYnBJALyULgJyYxh2CK/WhMbFKTfOdJZRmNcywdkVe/UJQ+QT+he+JmgmE8AwfbCUloIVJapJGaJuDFqu6fX6VAQN+6hevuDSQPkGtXjMJiaSGAjgVQUJVhpCQCxxS4RN3nNjsvN5P/kvxdocd+iMo/2x8NA/hi1o4reIERXZVrIu4LmaaEw6rFLBAChRVlRqus9lYE81ZslH7G/c4Hezhjqd6Du6zPQ+P1eMgKxxJSN4wHyVldIW6soFWwevR9vj+lbXB/+GxO4r12zJRVxEwGE3B1igwZkInmfz6rMw9w7Up7yRhbS/ctZL66nbmwjaDb+VTICMeqjmrPYQGS6WSfBB++6ujy8j0cB6V1BfcNH75GOyBlk03d2RaWp4vKB4M4SV8Rzb2YBkZlZeRIkt9VOU3mz/8jWWCVm6q9P0k166REvdtB3tgMYO28yAsEjNVYAY4usbPBUstxQejIBqUNvNm3XK8yh8iBVQj8/LfKVR5g7G5NAR0u0DcgaXiPuCB6YSc5naE0Cq7ZuoLhKwnt1qPCjK1p/hj5vwPXtZ0xKIFZ9yNMymiKBYM3S3+YJct7k0P9KDBBg2sPmnN4j9FhMU5gzdGf9FO2+VWeFOELhodsRBSS7RfLbbInV2GpKSe/eIYd3IsWDw9+zYRFLZGhyAiG7KRhLYnSGmcgambWOnIjw1x8lDIj35x9FdvJyuhcslbwPxsAAREAI35e1tc1IOVoyZ/uBtPu7CSwKMm13S5+lm6mDwkDAEtmUnEDInFZ5+Pl7SUtSL4/Hm2Ck24jEF2sn0NndzKAIEOw18mXTkxMIuZmKFQaSpq9F3pukgx34iefRixpl3cVTIWMDAWPLnkxSICzDDv6EBWS+yYP8n0o1ZQE9LOs/guJA9GRj0O5fJSkQ3ONhGxW5Xb/XQHLog0T3+tDFwSf5DQ3bhSxOkOcreX+IUXnz9RDVO004ZBQ1wfe4wEKNAHIDxCrJC8SIKZ+zAXbxTU8sj3S612dCUCIQDKRXOpMZiFIiU8Eg0ITi8NEDh559MygZyEbVjr9VqLIUE5mYcJM+9TG/lAa9SCA/JvsYd+uSFohyIo8l2qS/T022nvRcUA4QqpbbmbxAABEFe9fXJXo3CJ28qhQtjp3FYnXWl0kMBBDxyH4H0NvwfsJwvEcPKHhBfG754Ugg+oT4WbK2RRNE5E/8W8U1r0E1uV4pkLziq4ew61RvJTMQ3GiSzeM3pALfmSgemdTg3dLPpbw4LQrI18r3HKoFBETt8qCMApo4QTzoeVyPTZVWx4oCsohjKFDyAYnMNIoLaLeZkBAcJ+mjFpZ/KO31N6OAgCPUt6J9/uMgrBCCiIzRf8WcI2bUkE461z5E6g9kRwMBO3OXIgWiix8IOexa+jDGX05md2uqmmunttUWST+HpDkayEbkRkQHwYYArSU5RLwODgBUH4eOniPzoIwjrBqjgSxEfiDxXVCAgAH9EkPEXonJmzxBH5y0Rs5mlO5oIGA46XqUQArhAMEt+hK9tHlaz5D3NFVtHnSuXV6fizGGB0ZOZNwNmcE3rK9HwwJChIgSpzc9nAibTje2b5W3N6glFgiIKt9A51e9WAYLCDAlke6v2+l0x7zmLb/6Nv0kfWT9s/8h7+dssUDug3/iKpvH6Kw5EIFYIt3fNHJCXkP0i8CQnSmq8pi2VeEweXsskBz4Tb6PsL7+KksHc4VEuL9HqRmS0e2+M9XedvvLJ+nSoPytjGmxQBZGD1SO389lASnLehGHCYR0f/WM++umgBzlKk5NUzGXSB/V85KCLklHLBDgZu2D6laxBjzMyvoKLhDg/tojVkg0kCnqOlnp1FE9gxV1ETdj3G6WHyaQrHDkMScr6xHIQEj3lx6JojtB8jgRrRPBMbJela15pbIeSRMHkH2Q3azRWWG/6pGsrELYQHBLqBXb0mowtMbsGAF1aZWt+cG3FP04RxiCYeAM3W/hAXkx7FeNy8qCbUOogEQoZPeBU4NUteanlZ5k1cIFBAT816HxKMti+VWzCCKjoQMhR2xYeCskb6q285ax5sUrlf4GGxeQq2C6FzQgs7JY5+voCCDL4AMBm6wwnjzKNbVSi7Q19z+pfPqynQvIxvin/rGEMONsv+ougshdCICAww658/ET1CkXMta8NJ4zps1cQHKgDhEgzHjWuIgFk1WgQwAEBCScRPaospONseYPXYvnt7i4gNyAuiuBMCFZ7IMMSbteiAIIb4UkU4VpGow1j7Od28fFA3sFbqieQRD4JkppjUMBBAQkHESGoJ/HxFjzojjzAZw2HauAXBFZFmlF8K+ysjKQAAFEYjPyE5F3ODDWfFO854SmcQLBlJ72KaC0yqKU1hwkQHBLOUdTI2ogjDWHkL/08AOBWTPMiIoGC6OXCLyKsdEYu6kHMRDGmr8Tv1bkNiEUEKi7caOiwWVRVgXqCTtGLLr1FymQkDWfAqFRtVYAyAswgRBKq8AYocIilwhUIK7oJjqUQEbQ1vw8FLc6IABkCtwKFZsAGYkU4qiAUE10RnWA0JsM/A+thPLrsgWAHIRbVGfZ8W+yspaNxhECiSaCDAhzQj2saqSxWwAI3PZeMvZ4JGTiZ83BkQIBRKyRcQiCyfsDqQkA/p2wYPOZEAz+/ADSjH9Fub6/zspAkVyMlDR2GiUTSZvc1PVMbP5LWL/yshCQFMhGPWsW7VcVjsbRA8HtLKW1R1azs9RY8DzdxwBx6TULAZkI8/G8SNh0OqGo4zqMCkHnqt0aOiZ0AvyRAdfoM7/9774B75da+Hjsgj3PoSzGiqMHQloSWmtdg77hs9dfKRxXPof5W/mcXirbuwfik5n11RxcfSA2xo6AiuEqeA/u22eYWHAp1GXHq7HAgNKVEBeI6DEvaFaITV+it8GvqWeW0oUoyH6Cm48Htc0Q4pad0XhigITS8WTXyRVYmcSX6OXx9BOQ3YQ0XiCg2xreNIdxeKKAMETIvqzjcJ7ahSIKx2T4FUgXLxDyPUtxNQXZ/iCKCOhchOEOvUmPj/E/uBI6D36NBbZ9xjuddHOf/NvyZpUlGgjY1GOcCKmV9POtcFMlEjUWlIJhnyogdwxPMBCCiJPqfo87gHuDnu3q34nkPDF+jbVNaW7xxe/nz+9zT5/NuSsK8UEUkKrViQZC9tDNhBGqT6P3N/s3+FDwqOXXWBsVBeovzmMY0NJncd7aqqqErxAyaAfDPTfF9bys9H5afyWi80GP8ANRNKFpflW05BMBCIElCYBQg/Vmx7U8+jGZRB8aHsw5LlwCLNeH8pK5d8TwqFpLROgAS6KBEDwGx+dmrWOcq2JklXknPw/sbdlO1m1VHDISx/Oqql5NOBCmqKd8Wlavh+nYA+FBFzf5eSyUbdOXcfGoysLxV6uqFsMAUhgHD6D7qWMBFdY96EHt/k0IR5oKmHT5rdZzOHlUZeD4eMKGjP9+bt5XZfEBmaecB1A2Y7D/IWNdZWVBuu5R2iuIUMwCQEAma538kCNK+o/D8TPhPw26Z9jm3MVlioBk9f9GKQ/w6X6t2wB6QxS0FU4oZhK736LkYRXgATa0SRrPNKdwVsHikbfN414gY4lXEI5wlC88TwGQWf0l+mqxAh5n0e8MBqC4ZG8T+JAePebvQtyqnSYEZLbEpqzRVYJCPsPUqjuIwCxj8W35Zxgwt8kFMgs41HfcVqh0fexuNBgMPUoikWlDmbLgJ2h5+BoFeGwTGRsw7pGfzXqksOybccuEgYwnXkrEhbmLmVp62bKR8+4gCclcIYWkSpyvhAc4pLy0HuyUniQ7Ell6EHEoKKG5ITRYg7s6tTi/zxf9IxTQq7l5+TxACOWUQX2VOmj4/M0jV5CP9J6qKgU2pA/pQitIIYNw8A/U1vWdcvOLmVdQh4Lhdqx6ISCkh1jJSaN/zEMn9+GM5QFCPMOsyL9JHTSIL3IXBvLNWMXx+WkXxcOQI28X1ROrkIeCYbEL8bjBPUh57SCuhz6aUCh8KmsFjo8kgpGR88afGcRC+aIStzdLAQ+QUDzfTPMwgBT2UMk9PpPQh4Ih0bkwMad3RPTqGA4e5j3z543MKsTLCr9akZe7lvg7XcwqiAhDCP+LVv3Gwp+NXDu+zz19limKQxR4veCU8sHnDCEplX4I7oi9KoSCYTmMiWmsqDknZePJBzxoc5RdvYPwffILeIGMIxOOqRILwdBTJ0ujeRhWST2i7Q36BGf/FZVmAjYL8QBTezdE3NsK0krMXxFzz6nguX/Bw4P0poZXDUpQxfA34Ika2AIc39m/EU9cMVXBd1UaTC68QH4Qk3pfS64OLi/nrs2DBLze4WAN3ZMgICBZ3h0BxCDpZPsJDzHGvFMdHEGfoIuFkZq2KyKNRFY2+DIeGd8fzgdkGI7rQDCiHEhq3zyFPECnzsJIHoZF4t1ZS5ki7Wz1zmc9IsgDtMhtieKRK3TvGf25gXwfOGDzEgLkS5B8iuJh+HdwOrPQrv703TSP5T9VjYdbkAd1PvGASB4iPudm3jCkQIQlKiAgAFliiJEfCPeLDHiMicxVnD0nVAchZAZ5PXdG8hB7KDwprSwQhqxIAJALYItZLA8qFCni69dlioLHv/SpyMMpvEBeZuexRhf0qZKidIbxhSGEO7D6izP580b+7C4VgVwnNdNOA5dc4e0G0g2k29n9z65UEYdI0gRbSJaeS98bnZW7edjw1ULp8oiokS8MYWW5+o89k782L0MFIB+SbaNdn3ICyeGbmzXtThrHVpXHkacJLxAQpf8920rPl/IMcrmAgJxg6qD+MSl5xEBIzXO6gpOH4V/e5jwueuoUpuq+5RN1ediEeVSQlRB/6k8ifFdJbaOxPMiI8AsQHZbNyruNWHBUW8pmSUCG9Y0UuQZ9roFHXuY4XPKTlLeZft1OdXEEjc3CQLaTV/UPgMegM+PX5r0otTMXj40QiYgw2L9q+G15BYVGJp21bBBP8l0ciKkEc5md4oP3J5D7Ynfw8TD0DI6xIr2YLHtRr6DaIqKwekBrxt/l5y5+ZJxcRXEHR3mqMFROH94n/9W8grtI869QZZlKgOC4RfAMl/fIEt99Bn75LGoI/LTHQtpK/RPcRBQWtgQUjxVuC5kf2yXHVVEcpBCI0+whmJhIMnqX2SIUoe8T4GFofJu9Q3olU/Twr5da9bCeaq4/ZVVFYeknxbX3dmTkIskDXXLLRv9s5GZ2lTEvHqNuIVQWtVL0rjQu9UUe2zK5QgiIAXzqroAZlUuZqTH+rZK11ZGSDlJOqaCwqLRivw8U5/PmrJg3PKKW+GqoGoIbM1bk5vNXQ2R4WUanmVJf5QSgqLGwb5DmYIYgD8O5SXQjvG7IwwyPJ92SeXSUHLG5D3d3ZMfP46wID9Db4L8QX5J1DvHcCYcqtaqqjAxD+ku2RfLcXqcDI0eLl5eYIrQXWUPfaxARkK3764/er2RwbLou+RlaS0qoKL67wxZ3J1ajCJBKiEPLxlZV5S6b00dyNURBHGK04lZKe3ns9EKZSnhYRY1iQAwggcjUPPxdciLBU8zKsMW/RG6K8ABBLKxTXFaHfV+UuSxLGu18UYP4p3Ck3Dnkhj8sk76U1RHf2HGY/qqjG7EBoU7AhTUASFacH1eByuj0lIOj2vSeCt4UVpSMYXAc3yKztzSsqTrK0Xq8GNhiNHsdJCB3FYxcm39mbP985ECohWLFnSUlr5HGWgKPuUwacbnsXaCnGPfK3dEcFw9vvZhFf5t/lrWvZX9bTXtN2/4WI45M4i3hWhxkz36xOI5tTGB+XsHESlvH/ZRRbw7pLmXiEFsg4MCQSo4H7mtrCISlyf3rJAVCtY1WiOFY1BW2H0qG0WR39DiJ9dHc0YjWgIBoieM4BF9bIFpsviQFUuT3fyZM419n0NZjE7V/c4/8J+nLBnFhR2NcNazDYjxuHOceWHaxIRArDZakBDJCLCbsuXs2jWMAvYunn5JRPrbm8vLm+PRVbbcYEFBI3xmjsNoC3FKTfECMblJjYfw4Fv7QH8aB4yeBJbkvmAixihl06viW2BCkJhBQj0hcQIy1DQHiQ1XKr6v6ReDA6SmMkvoYoTfy3hRVWIM5k4r8PFAQiQdISzVxSZOIKJ0bh/4qrav8q1hWcoEfxRxGCWIW41HxGqiiRd+lOyAkliQCYgNXtIm4iX/lshyv0TRKNzxHxPSYyWQ2m624zUQtmglq87gsxoNqNDk/PeouvYI8Ag2+pAFCr+QhHJXbj3P6hVJWMz7BXSWM2PC0kudB2vevKvOwi/L42s+5g61GGEjAnSxAqukL+u/TZLOiJwTjd6/k3BkKOR6kktg2p9lsMplIIBaTB0z6lL5jRCUeVL/Flujb1DWIADmcJEDCVwQ+WVt/uGrGK4vu/uzK8XAA2O/gcwHOYvxAv1qHt0ktgdCdiv5VsZmIgJhcSgog1eELat7n55DXCBqEVHNmfabEN3ADdhsvc9aUv99vcakhCDKzrgxIxGW2boqmMXvKc8J+IVWkUqv5xyvKo4e6bo4zVp2iQNqSAEhL1DXt6sdoquNjijfO7WF/7yLnb6B2Eg5QB4gojwrq4kdwXGiDKJD22qPGRANpirmqtN/p5y6Y23Mz9nqrObtpPvErzWrJ7zER5UF5GdxTZsSBgOTvfl8igbQE5IibNwWG4uQEBfpqG12l4bzOdqm32WTRJQxIkywg3EsEX+lXJWRvkehf8Z2NVyvjRlsSBMQbCEBYIgwRtCfl1oryyBHkIZI4iTYn1oQAccsEwpeAu049igUJjT9oHg/x3ayuGsaHDy2QdplAmiJ/PGj0tlzcX1vjbHqOHojlS1x8vsPPExDKiAzhpn9TkC+QQMBIczhqqa1pYrstP6bVxTU0PMTziVQBRPD04aA8ixk4qzIQd4NsHoH9be127u/QAcBuJO7vETEc99NNlNsD1U02t+WSl9P9sMq824tqArFWB+BKuh+ZsyVaj3qePm5hIyuZXt3edtEbFeftl3lPLeoBsQagyy66iLUcsiER23KAYRup/VvHb3AUOZra3Faj5Ax8tPjUAoKARyDg2onCkLSYpJU//F2ZAt5IzX4qKyIvEo4vu5WSYB6E7IBvSEQbRrfRKeq9N8Xjvfb9Vp88z9KqChBjNSIggSG7IRsS0f6rq3Qy9CCaGzqrChB3AJl4imHuxtWJuVeL6L690iGobsioAhBvAKGkMXNID8Y/NdniEXF2mal1O/cgux+3CkBqYDNoPXTCcO5Ys6vR1N3T05Pzj3RtK1695RRpT8yh9ePg7R97bjpOHTqQhgBIkwpAYFiQtLTWU4bsY/UAQZTod4Y+uAjbqRcy77Lv+fBbY6ZPXceyDYda4bHRIQeii+PqDhxyZHtc9SasR1BymA09Uy4r9nazhSvnewfTM0+381xDN4HGk+2IG40POZBLirTSiXMff2rqkSxLJtPPa+I6ZepKcD+n/ip93IJ/aIfopXSbXMcMJw4oBWJFDkSej3Uo+9hHjViPbAlplMkHR8gPzgXVVcVVeiKw//RGGVfU3ejKdhySDcSCHIjkYLU122XqUS4hveXfJLNNyCY4FfluZoigf+cr8q8Kazwmb7W0IAcixes9YDhWj/XEK4ze8vsr038lvXQu1Et9Y28p8ztfvqH4wro/zj4k1bZ4UQMRT/Meajb1wJGFn4XaH3dvkJjhcvLv/dh2X5EfAg56qXya3Yo2MpQE5NJZkfc/kv37HojySmgjg99fPFF84IP1FK+bu2RMuH9v6AwoV1cvnv8KII1DdCLtlAeyG3tgS8VG1oPsWiPcUXeYx7natqSfHzoOsE6aDyEr5IoD0YnE6AdMPUgkZyjraZ5+9sKHfKkSzq3O5TP27mb//M4cuFeXjapGJQrEKFKzTUPEg5Btn01mdwwXb8mcaozJJHL5uq/cvW8w+yf3bbwf+sUZhHbx6NAB8Ylpy/oehLLr7n3HI/u433lm4ADWJt6zUb7urhnbPxtzOuInti5ZiOTaHGgqVGJARHsabvagFf3GyvPR3fWTitevWr4gZeCQRTk5i2bMyMnZsfHu7UvuG7p7cPQr//EHixBdVz2/B9xgRAdEQo63uQe55Oyc5Jcvx4du34bskrAjiLrlUuJTWKAq3qOC3Pi66G05NIq+noHycrpPCGxyM6IDclRatsRjUoNJzyszrr5cJM5idtdnO/QorwNzOQQidrsXRwdEcu/3oWPdPSrJwh1Xh75WenpwLInzr+28mtNRjvj9G88JxuqX42y4hgSEEEezqUdN+fGujudzNm6/umT7jpxFNzp26cvRvyfWKBYSBuLdkwBDZYVjRIenvrvnOyom102zeG4RcbO1LyBfWh3HTN8xFli9R2r23YcWiIRdwTxr5US2x2XCbnkSja7sm6dk1HPR7w+Jr7chrdVwrrnx1tNimMnluXmiVf4No99BZYTU83PK4DnmakzuNUN1n5xoVVxJDxxVYUubDnIH6QGyEaj5o0ZTd7JQaPyo+ZzhBIweoP3qbPrcH0AkaQdaTzhuZje7Pvp9dzemJoJ6V3O24QTMRiyyKxvKXnUpFcOWhoAKQuA5dOqEw3DznOeY51jzx/WfNv7eZDJhCklh3SZTIyHEsz/mOZd97qbBceIUZATQl4fUEq6xLZBgSUsj/jlwgPz3QOuR1gOtrYcOmQ8R/xL/aW09YCb+Jb+bRvwLXqy6tMMaryGxycFbE9BEQFu14Li6QIigvUl77rzaKoirDwTHL9VWa8+eQ1lZoE6Cl7Xp03ixXQMQue8A+gx4ufvUfW5NdYVo7Pfi0EXBaA2f+6wGI1CNggaudMSfz1LT8LdMo8F2VIfjSQSETKlY3c6/TRg1Fm8QRyZxzX7XWff/bVkUe02LD0crcR9XQayUs5f/JoxG21Ejjl5SoPwWn8V2+LsM43AbMpuBBggIUryWtvbvnqmvrnFb1YIBFwitwbwXbd8Nu1Ld3ub2GnG1JQXJbzVesrQ1NdzCi6IlAShQArlFlViCFoVqQCgBwxb31ziTNzNZ3VTT5j7q9enwJJAUFd/L6LNe3G9rr06SRWNvt+23WL3GpOCQECBhy++71OJuO9vU1KA2m4aGJnI5XEyS5ZAsQNgKTWf0ei+1XHTXtp1tPwx98Vyurm5vt7W53S0tl7y/1iUrhSQCErt6dD6f9WiLez8ptbU2Ww0p7U5nEyHVhNiZZdVgJ/9YTfy10+lsBy+z2WpryZ9zW45avT6jLojfcpKCa6IB0UQDogHRRAOiAdFEA6IB0UQDogHRRAOiiQZEA6KJBuTWl/8XYADnNmjWHFGctAAAAABJRU5ErkJggg=="
            }
        },
        data() {
            return {
                isConnected: true, // 是否有网络连接
                networkType: "none", // 网络类型
            }
        },
        computed: {
            uZIndex() {
                return this.zIndex ? this.zIndex : this.$u.zIndex.noNetwork;
            }
        },
        mounted() {
            this.isIOS = (uni.getSystemInfoSync().platform === 'ios');
            uni.onNetworkStatusChange((res) => {
                this.isConnected = res.isConnected;
                this.networkType = res.networkType;
            });
            uni.getNetworkType({
                success: (res) => {
                    this.networkType = res.networkType;
                    if (res.networkType == 'none') {
                        this.isConnected = false;
                    } else {
                        this.isConnected = true;
                    }
                }
            });
        },
        methods: {
            retry() {
                // 重新检查网络
                uni.getNetworkType({
                    success: (res) => {
                        this.networkType = res.networkType;
                        if (res.networkType == 'none') {
                            uni.showToast({
                                title: '无网络连接',
                                icon: 'none',
                                position: 'top'
                            })
                            this.isConnected = false;
                        } else {
                            uni.showToast({
                                title: '网络已连接',
                                icon: 'none',
                                position: 'top'
                            })
                            this.isConnected = true;
                        }
                    }
                });
                this.$emit('retry');
            },
            async openSettings() {
                if (this.networkType == "none") {
                    this.openSystemSettings();
                    return;
                }
            },
            openAppSettings() {
                this.gotoAppSetting();
            },
            openSystemSettings() {
                // 以下方法来自5+范畴,如需深究,请自行查阅相关文档
                // https://ask.dcloud.net.cn/docs/
                if (this.isIOS) {
                    this.gotoiOSSetting();
                } else {
                    this.gotoAndroidSetting();
                }
            },
            network() {
                var result = null;
                var cellularData = plus.ios.newObject("CTCellularData");
                var state = cellularData.plusGetAttribute("restrictedState");
                if (state == 0) {
                    result = null;
                } else if (state == 2) {
                    result = 1;
                } else if (state == 1) {
                    result = 2;
                }
                plus.ios.deleteObject(cellularData);
                return result;
            },
            gotoAppSetting() {
                if (this.isIOS) {
                    var UIApplication = plus.ios.import("UIApplication");
                    var application2 = UIApplication.sharedApplication();
                    var NSURL2 = plus.ios.import("NSURL");
                    var setting2 = NSURL2.URLWithString("app-settings:");
                    application2.openURL(setting2);
                    plus.ios.deleteObject(setting2);
                    plus.ios.deleteObject(NSURL2);
                    plus.ios.deleteObject(application2);
                } else {
                    var Intent = plus.android.importClass("android.content.Intent");
                    var Settings = plus.android.importClass("android.provider.Settings");
                    var Uri = plus.android.importClass("android.net.Uri");
                    var mainActivity = plus.android.runtimeMainActivity();
                    var intent = new Intent();
                    intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                    var uri = Uri.fromParts("package", mainActivity.getPackageName(), null);
                    intent.setData(uri);
                    mainActivity.startActivity(intent);
                }
            },
            gotoiOSSetting() {
                var UIApplication = plus.ios.import("UIApplication");
                var application2 = UIApplication.sharedApplication();
                var NSURL2 = plus.ios.import("NSURL");
                var setting2 = NSURL2.URLWithString("App-prefs:root=General");
                application2.openURL(setting2);
                plus.ios.deleteObject(setting2);
                plus.ios.deleteObject(NSURL2);
                plus.ios.deleteObject(application2);
            },
            gotoAndroidSetting() {
                var Intent = plus.android.importClass("android.content.Intent");
                var Settings = plus.android.importClass("android.provider.Settings");
                var mainActivity = plus.android.runtimeMainActivity();
                var intent = new Intent(Settings.ACTION_SETTINGS);
                mainActivity.startActivity(intent);
            }
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-no-network {
        background-color: #fff;
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
    }
    .u-inner {
        height: 100vh;
        @include vue-flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        margin-top: -15%;
    }
    .u-tips {
        color: $u-tips-color;
        font-size: 28rpx;
        padding: 30rpx 0;
    }
    .u-error-icon {
        width: 300rpx;
    }
    .u-to-setting {
        color: $u-light-color;
        font-size: 26rpx;
    }
    .u-setting-btn {
        font-size: 26rpx;
        color: $u-type-primary;
    }
    .u-retry {
        margin-top: 30rpx;
        border: 1px solid $u-tips-color;
        color: $u-tips-color;
        font-size: 28rpx;
        padding: 6rpx 30rpx;
        border-radius: 3px;
    }
    .u-retry-hover {
        color: #fff;
        background-color: $u-tips-color;
    }
</style>
uview-ui/components/u-notice-bar/u-notice-bar.vue
New file
@@ -0,0 +1,272 @@
<template>
    <view class="u-notice-bar-wrap" v-if="isShow" :style="{
        borderRadius: borderRadius + 'rpx',
    }">
        <block v-if="mode == 'horizontal' && isCircular">
            <u-row-notice
                :type="type"
                :color="color"
                :bgColor="bgColor"
                :list="list"
                :volumeIcon="volumeIcon"
                :moreIcon="moreIcon"
                :volumeSize="volumeSize"
                :closeIcon="closeIcon"
                :mode="mode"
                :fontSize="fontSize"
                :speed="speed"
                :playState="playState"
                :padding="padding"
                @getMore="getMore"
                @close="close"
                @click="click"
            ></u-row-notice>
        </block>
        <block v-if="mode == 'vertical' || (mode == 'horizontal' && !isCircular)">
            <u-column-notice
                :type="type"
                :color="color"
                :bgColor="bgColor"
                :list="list"
                :volumeIcon="volumeIcon"
                :moreIcon="moreIcon"
                :closeIcon="closeIcon"
                :mode="mode"
                :volumeSize="volumeSize"
                :disable-touch="disableTouch"
                :fontSize="fontSize"
                :duration="duration"
                :playState="playState"
                :padding="padding"
                @getMore="getMore"
                @close="close"
                @click="click"
                @end="end"
            ></u-column-notice>
        </block>
    </view>
</template>
<script>
/**
 * noticeBar 滚动通知
 * @description 该组件用于滚动通告场景,有多种模式可供选择
 * @tutorial https://www.uviewui.com/components/noticeBar.html
 * @property {Array} list 滚动内容,数组形式,见上方说明
 * @property {String} type 显示的主题(默认warning)
 * @property {Boolean} volume-icon 是否显示小喇叭图标(默认true)
 * @property {Boolean} more-icon 是否显示右边的向右箭头(默认false)
 * @property {Boolean} close-icon 是否显示关闭图标(默认false)
 * @property {Boolean} autoplay 是否自动播放(默认true)
 * @property {String} color 文字颜色
 * @property {String Number} bg-color 背景颜色
 * @property {String} mode 滚动模式(默认horizontal)
 * @property {Boolean} show 是否显示(默认true)
 * @property {String Number} font-size 字体大小,单位rpx(默认28)
 * @property {String Number} volume-size 左边喇叭的大小(默认34)
 * @property {String Number} duration 滚动周期时长,只对步进模式有效,横向衔接模式无效,单位ms(默认2000)
 * @property {String Number} speed 水平滚动时的滚动速度,即每秒移动多少距离,只对水平衔接方式有效,单位rpx(默认160)
 * @property {String Number} font-size 字体大小,单位rpx(默认28)
 * @property {Boolean} is-circular mode为horizontal时,指明是否水平衔接滚动(默认true)
 * @property {String} play-state 播放状态,play - 播放,paused - 暂停(默认play)
 * @property {String Nubmer} border-radius 通知栏圆角(默认为0)
 * @property {String Nubmer} padding 内边距,字符串,与普通的内边距css写法一直(默认"18rpx 24rpx")
 * @property {Boolean} no-list-hidden 列表为空时,是否显示组件(默认false)
 * @property {Boolean} disable-touch 是否禁止通过手动滑动切换通知,只有mode = vertical,或者mode = horizontal且is-circular = false时有效(默认true)
 * @event {Function} click 点击通告文字触发,只有mode = vertical,或者mode = horizontal且is-circular = false时有效
 * @event {Function} close 点击右侧关闭图标触发
 * @event {Function} getMore 点击右侧向右图标触发
 * @event {Function} end 列表的消息每次被播放一个周期时触发,只有mode = vertical,或者mode = horizontal且is-circular = false时有效
 * @example <u-notice-bar :more-icon="true" :list="list"></u-notice-bar>
 */
export default {
    name: "u-notice-bar",
    props: {
        // 显示的内容,数组
        list: {
            type: Array,
            default() {
                return [];
            }
        },
        // 显示的主题,success|error|primary|info|warning
        type: {
            type: String,
            default: 'warning'
        },
        // 是否显示左侧的音量图标
        volumeIcon: {
            type: Boolean,
            default: true
        },
        // 音量喇叭的大小
        volumeSize: {
            type: [Number, String],
            default: 34
        },
        // 是否显示右侧的右箭头图标
        moreIcon: {
            type: Boolean,
            default: false
        },
        // 是否显示右侧的关闭图标
        closeIcon: {
            type: Boolean,
            default: false
        },
        // 是否自动播放
        autoplay: {
            type: Boolean,
            default: true
        },
        // 文字颜色,各图标也会使用文字颜色
        color: {
            type: String,
            default: ''
        },
        // 背景颜色
        bgColor: {
            type: String,
            default: ''
        },
        // 滚动方向,horizontal-水平滚动,vertical-垂直滚动
        mode: {
            type: String,
            default: 'horizontal'
        },
        // 是否显示
        show: {
            type: Boolean,
            default: true
        },
        // 字体大小,单位rpx
        fontSize: {
            type: [Number, String],
            default: 28
        },
        // 滚动一个周期的时间长,单位ms
        duration: {
            type: [Number, String],
            default: 2000
        },
        // 水平滚动时的滚动速度,即每秒滚动多少rpx,这有利于控制文字无论多少时,都能有一个恒定的速度
        speed: {
            type: [Number, String],
            default: 160
        },
        // 水平滚动时,是否采用衔接形式滚动
        // 水平衔接模式,采用的是swiper组件,水平滚动
        isCircular: {
            type: Boolean,
            default: true
        },
        // 播放状态,play-播放,paused-暂停
        playState: {
            type: String,
            default: 'play'
        },
        // 是否禁止用手滑动切换
        // 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序
        disableTouch: {
            type: Boolean,
            default: true
        },
        // 滚动通知设置圆角
        borderRadius: {
            type: [Number, String],
            default: 0
        },
        // 通知的边距
        padding: {
            type: [Number, String],
            default: '18rpx 24rpx'
        },
        // list列表为空时,是否显示组件
        noListHidden: {
            type: Boolean,
            default: true
        }
    },
    computed: {
        // 如果设置show为false,或者设置了noListHidden为true,且list长度又为零的话,隐藏组件
        isShow() {
            if(this.show == false || (this.noListHidden == true && this.list.length == 0)) return false;
            else return true;
        }
    },
    methods: {
        // 点击通告栏
        click(index) {
            this.$emit('click', index);
        },
        // 点击关闭按钮
        close() {
            this.$emit('close');
        },
        // 点击更多箭头按钮
        getMore() {
            this.$emit('getMore');
        },
        // 滚动一个周期结束,只对垂直,或者水平步进形式有效
        end() {
            this.$emit('end');
        }
    }
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-notice-bar-wrap {
    overflow: hidden;
}
.u-notice-bar {
    padding: 18rpx 24rpx;
    overflow: hidden;
}
.u-direction-row {
    @include vue-flex;
    align-items: center;
    justify-content: space-between;
}
.u-left-icon {
    @include vue-flex;
    align-items: center;
}
.u-notice-box {
    flex: 1;
    @include vue-flex;
    overflow: hidden;
    margin-left: 12rpx;
}
.u-right-icon {
    margin-left: 12rpx;
    @include vue-flex;
    align-items: center;
}
.u-notice-content {
    line-height: 1;
    white-space: nowrap;
    font-size: 26rpx;
    animation: u-loop-animation 10s linear infinite both;
    text-align: right;
    // 这一句很重要,为了能让滚动左右连接起来
    padding-left: 100%;
}
@keyframes u-loop-animation {
    0% {
        transform: translate3d(0, 0, 0);
    }
    100% {
        transform: translate3d(-100%, 0, 0);
    }
}
</style>
uview-ui/components/u-number-box/u-number-box.vue
New file
@@ -0,0 +1,363 @@
<template>
    <view class="u-numberbox">
        <view class="u-icon-minus" @touchstart.stop.prevent="btnTouchStart('minus')" @touchend.stop.prevent="clearTimer" :class="{ 'u-icon-disabled': disabled || inputVal <= min }"
            :style="{
                background: bgColor,
                height: inputHeight + 'rpx',
                color: color
            }">
            <u-icon name="minus" :size="size"></u-icon>
        </view>
        <input :disabled="disabledInput || disabled" :cursor-spacing="getCursorSpacing" :class="{ 'u-input-disabled': disabled }"
            v-model="inputVal" class="u-number-input" @blur="onBlur" @focus="onFocus"
            type="number" :style="{
                color: color,
                fontSize: size + 'rpx',
                background: bgColor,
                height: inputHeight + 'rpx',
                width: inputWidth + 'rpx'
            }" />
        <view class="u-icon-plus" @touchstart.stop.prevent="btnTouchStart('plus')" @touchend.stop.prevent="clearTimer" :class="{ 'u-icon-disabled': disabled || inputVal >= max }"
            :style="{
                background: bgColor,
                height: inputHeight + 'rpx',
                color: color
            }">
            <u-icon name="plus" :size="size"></u-icon>
        </view>
    </view>
</template>
<script>
    /**
     * numberBox 步进器
     * @description 该组件一般用于商城购物选择物品数量的场景。注意:该输入框只能输入大于或等于0的整数,不支持小数输入
     * @tutorial https://www.uviewui.com/components/numberBox.html
     * @property {Number} value 输入框初始值(默认1)
     * @property {String} bg-color 输入框和按钮的背景颜色(默认#F2F3F5)
     * @property {Number} min 用户可输入的最小值(默认0)
     * @property {Number} max 用户可输入的最大值(默认99999)
     * @property {Number} step 步长,每次加或减的值(默认1)
     * @property {Boolean} disabled 是否禁用操作,禁用后无法加减或手动修改输入框的值(默认false)
     * @property {Boolean} disabled-input 是否禁止输入框手动输入值(默认false)
     * @property {Boolean} positive-integer 是否只能输入正整数(默认true)
     * @property {String | Number} size 输入框文字和按钮字体大小,单位rpx(默认26)
     * @property {String} color 输入框文字和加减按钮图标的颜色(默认#323233)
     * @property {String | Number} input-width 输入框宽度,单位rpx(默认80)
     * @property {String | Number} input-height 输入框和按钮的高度,单位rpx(默认50)
     * @property {String | Number} index 事件回调时用以区分当前发生变化的是哪个输入框
     * @property {Boolean} long-press 是否开启长按连续递增或递减(默认true)
     * @property {String | Number} press-time 开启长按触发后,每触发一次需要多久,单位ms(默认250)
     * @property {String | Number} cursor-spacing 指定光标于键盘的距离,避免键盘遮挡输入框,单位rpx(默认200)
     * @event {Function} change 输入框内容发生变化时触发,对象形式
     * @event {Function} blur 输入框失去焦点时触发,对象形式
     * @event {Function} minus 点击减少按钮时触发(按钮可点击情况下),对象形式
     * @event {Function} plus 点击增加按钮时触发(按钮可点击情况下),对象形式
     * @example <u-number-box :min="1" :max="100"></u-number-box>
     */
    export default {
        name: "u-number-box",
        props: {
            // 预显示的数字
            value: {
                type: Number,
                default: 1
            },
            // 背景颜色
            bgColor: {
                type: String,
                default: '#F2F3F5'
            },
            // 最小值
            min: {
                type: Number,
                default: 0
            },
            // 最大值
            max: {
                type: Number,
                default: 99999
            },
            // 步进值,每次加或减的值
            step: {
                type: Number,
                default: 1
            },
            // 是否禁用加减操作
            disabled: {
                type: Boolean,
                default: false
            },
            // input的字体大小,单位rpx
            size: {
                type: [Number, String],
                default: 26
            },
            // 加减图标的颜色
            color: {
                type: String,
                default: '#323233'
            },
            // input宽度,单位rpx
            inputWidth: {
                type: [Number, String],
                default: 80
            },
            // input高度,单位rpx
            inputHeight: {
                type: [Number, String],
                default: 50
            },
            // index索引,用于列表中使用,让用户知道是哪个numberbox发生了变化,一般使用for循环出来的index值即可
            index: {
                type: [Number, String],
                default: ''
            },
            // 是否禁用输入框,与disabled作用于输入框时,为OR的关系,即想要禁用输入框,又可以加减的话
            // 设置disabled为false,disabledInput为true即可
            disabledInput: {
                type: Boolean,
                default: false
            },
            // 输入框于键盘之间的距离
            cursorSpacing: {
                type: [Number, String],
                default: 100
            },
            // 是否开启长按连续递增或递减
            longPress: {
                type: Boolean,
                default: true
            },
            // 开启长按触发后,每触发一次需要多久
            pressTime: {
                type: [Number, String],
                default: 250
            },
            // 是否只能输入大于或等于0的整数(正整数)
            positiveInteger: {
                type: Boolean,
                default: true
            }
        },
        watch: {
            value(v1, v2) {
                // 只有value的改变是来自外部的时候,才去同步inputVal的值,否则会造成循环错误
                if(!this.changeFromInner) {
                    this.inputVal = v1;
                    // 因为inputVal变化后,会触发this.handleChange(),在其中changeFromInner会再次被设置为true,
                    // 造成外面修改值,也导致被认为是内部修改的混乱,这里进行this.$nextTick延时,保证在运行周期的最后处
                    // 将changeFromInner设置为false
                    this.$nextTick(function(){
                        this.changeFromInner = false;
                    })
                }
            },
            inputVal(v1, v2) {
                // 为了让用户能够删除所有输入值,重新输入内容,删除所有值后,内容为空字符串
                if (v1 == '') return;
                let value = 0;
                // 首先判断是否数值,并且在min和max之间,如果不是,使用原来值
                let tmp = this.$u.test.number(v1);
                if (tmp && v1 >= this.min && v1 <= this.max) value = v1;
                else value = v2;
                // 判断是否只能输入大于等于0的整数
                if(this.positiveInteger) {
                    // 小于0,或者带有小数点,
                    if(v1 < 0 || String(v1).indexOf('.') !== -1) {
                        value = v2;
                        // 双向绑定input的值,必须要使用$nextTick修改显示的值
                        this.$nextTick(() => {
                            this.inputVal = v2;
                        })
                    }
                }
                // 发出change事件
                this.handleChange(value, 'change');
            }
        },
        data() {
            return {
                inputVal: 1, // 输入框中的值,不能直接使用props中的value,因为应该改变props的状态
                timer: null, // 用作长按的定时器
                changeFromInner: false, // 值发生变化,是来自内部还是外部
                innerChangeTimer: null, // 内部定时器
            };
        },
        created() {
            this.inputVal = Number(this.value);
        },
        computed: {
            getCursorSpacing() {
                // 先将值转为px单位,再转为数值
                return Number(uni.upx2px(this.cursorSpacing));
            }
        },
        methods: {
            // 点击退格键
            btnTouchStart(callback) {
                // 先执行一遍方法,否则会造成松开手时,就执行了clearTimer,导致无法实现功能
                this[callback]();
                // 如果没开启长按功能,直接返回
                if (!this.longPress) return;
                clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
                this.timer = null;
                this.timer = setInterval(() => {
                    // 执行加或减函数
                    this[callback]();
                }, this.pressTime);
            },
            clearTimer() {
                this.$nextTick(() => {
                    clearInterval(this.timer);
                    this.timer = null;
                })
            },
            minus() {
                this.computeVal('minus');
            },
            plus() {
                this.computeVal('plus');
            },
            // 为了保证小数相加减出现精度溢出的问题
            calcPlus(num1, num2) {
                let baseNum, baseNum1, baseNum2;
                try {
                    baseNum1 = num1.toString().split('.')[1].length;
                } catch (e) {
                    baseNum1 = 0;
                }
                try {
                    baseNum2 = num2.toString().split('.')[1].length;
                } catch (e) {
                    baseNum2 = 0;
                }
                baseNum = Math.pow(10, Math.max(baseNum1, baseNum2));
                let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2; //精度
                return ((num1 * baseNum + num2 * baseNum) / baseNum).toFixed(precision);
            },
            // 为了保证小数相加减出现精度溢出的问题
            calcMinus(num1, num2) {
                let baseNum, baseNum1, baseNum2;
                try {
                    baseNum1 = num1.toString().split('.')[1].length;
                } catch (e) {
                    baseNum1 = 0;
                }
                try {
                    baseNum2 = num2.toString().split('.')[1].length;
                } catch (e) {
                    baseNum2 = 0;
                }
                baseNum = Math.pow(10, Math.max(baseNum1, baseNum2));
                let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2;
                return ((num1 * baseNum - num2 * baseNum) / baseNum).toFixed(precision);
            },
            computeVal(type) {
                uni.hideKeyboard();
                if (this.disabled) return;
                let value = 0;
                // 减
                if (type === 'minus') {
                    value = this.calcMinus(this.inputVal, this.step);
                } else if (type === 'plus') {
                    value = this.calcPlus(this.inputVal, this.step);
                }
                // 判断是否小于最小值和大于最大值
                if (value < this.min || value > this.max) {
                    return;
                }
                this.inputVal = value;
                this.handleChange(value, type);
            },
            // 处理用户手动输入的情况
            onBlur(event) {
                let val = 0;
                let value = event.detail.value;
                // 如果为非0-9数字组成,或者其第一位数值为0,直接让其等于min值
                // 这里不直接判断是否正整数,是因为用户传递的props min值可能为0
                if (!/(^\d+$)/.test(value) || value[0] == 0) val = this.min;
                val = +value;
                if (val > this.max) {
                    val = this.max;
                } else if (val < this.min) {
                    val = this.min;
                }
                this.$nextTick(() => {
                    this.inputVal = val;
                })
                this.handleChange(val, 'blur');
            },
            // 输入框获得焦点事件
            onFocus() {
                this.$emit('focus');
            },
            handleChange(value, type) {
                if (this.disabled) return;
                // 清除定时器,避免造成混乱
                if(this.innerChangeTimer) {
                    clearTimeout(this.innerChangeTimer);
                    this.innerChangeTimer = null;
                }
                // 发出input事件,修改通过v-model绑定的值,达到双向绑定的效果
                this.changeFromInner = true;
                // 一定时间内,清除changeFromInner标记,否则内部值改变后
                // 外部通过程序修改value值,将会无效
                this.innerChangeTimer = setTimeout(() => {
                    this.changeFromInner = false;
                }, 150);
                this.$emit('input', Number(value));
                this.$emit(type, {
                    // 转为Number类型
                    value: Number(value),
                    index: this.index
                })
            }
        }
    };
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-numberbox {
        display: inline-flex;
        align-items: center;
    }
    .u-number-input {
        position: relative;
        text-align: center;
        padding: 0;
        margin: 0 6rpx;
        @include vue-flex;
        align-items: center;
        justify-content: center;
    }
    .u-icon-plus,
    .u-icon-minus {
        width: 60rpx;
        @include vue-flex;
        justify-content: center;
        align-items: center;
    }
    .u-icon-plus {
        border-radius: 0 8rpx 8rpx 0;
    }
    .u-icon-minus {
        border-radius: 8rpx 0 0 8rpx;
    }
    .u-icon-disabled {
        color: #c8c9cc !important;
        background: #f7f8fa !important;
    }
    .u-input-disabled {
        color: #c8c9cc !important;
        background-color: #f2f3f5 !important;
    }
</style>
uview-ui/components/u-number-keyboard/u-number-keyboard.vue
New file
@@ -0,0 +1,158 @@
<template>
    <view class="u-keyboard" @touchmove.stop.prevent="() => {}">
        <view class="u-keyboard-grids">
            <view
                class="u-keyboard-grids-item"
                :class="[btnBgGray(index) ? 'u-bg-gray' : '', index <= 2 ? 'u-border-top' : '', index < 9 ? 'u-border-bottom' : '', (index + 1) % 3 != 0 ? 'u-border-right' : '']"
                :style="[itemStyle(index)]"
                v-for="(item, index) in numList"
                :key="index"
                :hover-class="hoverClass(index)"
                :hover-stay-time="100"
                @tap="keyboardClick(item)">
                <view class="u-keyboard-grids-btn">{{ item }}</view>
            </view>
            <view class="u-keyboard-grids-item u-bg-gray" hover-class="u-hover-class" :hover-stay-time="100" @touchstart.stop="backspaceClick"
                @touchend="clearTimer">
                <view class="u-keyboard-back u-keyboard-grids-btn">
                    <u-icon name="backspace" :size="38" :bold="true"></u-icon>
                </view>
            </view>
        </view>
    </view>
</template>
<script>
    export default {
        props: {
            // 键盘的类型,number-数字键盘,card-身份证键盘
            mode: {
                type: String,
                default: 'number'
            },
            // 是否显示键盘的"."符号
            dotEnabled: {
                type: Boolean,
                default: true
            },
            // 是否打乱键盘按键的顺序
            random: {
                type: Boolean,
                default: false
            }
        },
        data() {
            return {
                backspace: 'backspace', // 退格键内容
                dot: '.', // 点
                timer: null, // 长按多次删除的事件监听
                cardX: 'X' // 身份证的X符号
            };
        },
        computed: {
            // 键盘需要显示的内容
            numList() {
                let tmp = [];
                if (!this.dotEnabled && this.mode == 'number') {
                    if (!this.random) {
                        return [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
                    } else {
                        return this.$u.randomArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]);
                    }
                } else if (this.dotEnabled && this.mode == 'number') {
                    if (!this.random) {
                        return [1, 2, 3, 4, 5, 6, 7, 8, 9, this.dot, 0];
                    } else {
                        return this.$u.randomArray([1, 2, 3, 4, 5, 6, 7, 8, 9, this.dot, 0]);
                    }
                } else if (this.mode == 'card') {
                    if (!this.random) {
                        return [1, 2, 3, 4, 5, 6, 7, 8, 9, this.cardX, 0];
                    } else {
                        return this.$u.randomArray([1, 2, 3, 4, 5, 6, 7, 8, 9, this.cardX, 0]);
                    }
                }
            },
            // 按键的样式,在非乱序&&数字键盘&&不显示点按钮时,index为9时,按键占位两个空间
            itemStyle() {
                return index => {
                    let style = {};
                    if (this.mode == 'number' && !this.dotEnabled && index == 9) style.flex = '0 0 66.6666666666%';
                    return style;
                };
            },
            // 是否让按键显示灰色,只在非乱序&&数字键盘&&且允许点按键的时候
            btnBgGray() {
                return index => {
                    if (!this.random && index == 9 && (this.mode != 'number' || (this.mode == 'number' && this.dotEnabled))) return true;
                    else return false;
                };
            },
            hoverClass() {
                return index => {
                    if (!this.random && index == 9 && (this.mode == 'number' && this.dotEnabled || this.mode == 'card')) return 'u-hover-class';
                    else return 'u-keyboard-hover';
                }
            }
        },
        methods: {
            // 点击退格键
            backspaceClick() {
                this.$emit('backspace');
                clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
                this.timer = null;
                this.timer = setInterval(() => {
                    this.$emit('backspace');
                }, 250);
            },
            clearTimer() {
                clearInterval(this.timer);
                this.timer = null;
            },
            // 获取键盘显示的内容
            keyboardClick(val) {
                // 允许键盘显示点模式和触发非点按键时,将内容转为数字类型
                if (this.dotEnabled && val != this.dot && val != this.cardX) val = Number(val);
                this.$emit('change', val);
            }
        }
    };
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-keyboard {
        position: relative;
        z-index: 1003;
    }
    .u-keyboard-grids {
        @include vue-flex;
        flex-wrap: wrap;
    }
    .u-keyboard-grids-item {
        flex: 0 0 33.3333333333%;
        text-align: center;
        font-size: 50rpx;
        color: #333;
        @include vue-flex;
        align-items: center;
        justify-content: center;
        height: 110rpx;
        font-weight: 500;
    }
    .u-bg-gray {
        background-color: $u-border-color;
    }
    .u-keyboard-back {
        font-size: 36rpx;
    }
    .u-keyboard-hover {
        background-color: #e7e6eb;
    }
</style>
uview-ui/components/u-parse/libs/CssHandler.js
New file
@@ -0,0 +1,100 @@
const cfg = require('./config.js'),
    isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
function CssHandler(tagStyle) {
    var styles = Object.assign(Object.create(null), cfg.userAgentStyles);
    for (var item in tagStyle)
        styles[item] = (styles[item] ? styles[item] + ';' : '') + tagStyle[item];
    this.styles = styles;
}
CssHandler.prototype.getStyle = function(data) {
    this.styles = new parser(data, this.styles).parse();
}
CssHandler.prototype.match = function(name, attrs) {
    var tmp, matched = (tmp = this.styles[name]) ? tmp + ';' : '';
    if (attrs.class) {
        var items = attrs.class.split(' ');
        for (var i = 0, item; item = items[i]; i++)
            if (tmp = this.styles['.' + item])
                matched += tmp + ';';
    }
    if (tmp = this.styles['#' + attrs.id])
        matched += tmp + ';';
    return matched;
}
module.exports = CssHandler;
function parser(data, init) {
    this.data = data;
    this.floor = 0;
    this.i = 0;
    this.list = [];
    this.res = init;
    this.state = this.Space;
}
parser.prototype.parse = function() {
    for (var c; c = this.data[this.i]; this.i++)
        this.state(c);
    return this.res;
}
parser.prototype.section = function() {
    return this.data.substring(this.start, this.i);
}
// 状态机
parser.prototype.Space = function(c) {
    if (c == '.' || c == '#' || isLetter(c)) {
        this.start = this.i;
        this.state = this.Name;
    } else if (c == '/' && this.data[this.i + 1] == '*')
        this.Comment();
    else if (!cfg.blankChar[c] && c != ';')
        this.state = this.Ignore;
}
parser.prototype.Comment = function() {
    this.i = this.data.indexOf('*/', this.i) + 1;
    if (!this.i) this.i = this.data.length;
    this.state = this.Space;
}
parser.prototype.Ignore = function(c) {
    if (c == '{') this.floor++;
    else if (c == '}' && !--this.floor) {
        this.list = [];
        this.state = this.Space;
    }
}
parser.prototype.Name = function(c) {
    if (cfg.blankChar[c]) {
        this.list.push(this.section());
        this.state = this.NameSpace;
    } else if (c == '{') {
        this.list.push(this.section());
        this.Content();
    } else if (c == ',') {
        this.list.push(this.section());
        this.Comma();
    } else if (!isLetter(c) && (c < '0' || c > '9') && c != '-' && c != '_')
        this.state = this.Ignore;
}
parser.prototype.NameSpace = function(c) {
    if (c == '{') this.Content();
    else if (c == ',') this.Comma();
    else if (!cfg.blankChar[c]) this.state = this.Ignore;
}
parser.prototype.Comma = function() {
    while (cfg.blankChar[this.data[++this.i]]);
    if (this.data[this.i] == '{') this.Content();
    else {
        this.start = this.i--;
        this.state = this.Name;
    }
}
parser.prototype.Content = function() {
    this.start = ++this.i;
    if ((this.i = this.data.indexOf('}', this.i)) == -1) this.i = this.data.length;
    var content = this.section();
    for (var i = 0, item; item = this.list[i++];)
        if (this.res[item]) this.res[item] += ';' + content;
        else this.res[item] = content;
    this.list = [];
    this.state = this.Space;
}
uview-ui/components/u-parse/libs/MpHtmlParser.js
New file
@@ -0,0 +1,580 @@
/**
 * html 解析器
 * @tutorial https://github.com/jin-yufeng/Parser
 * @version 20201029
 * @author JinYufeng
 * @listens MIT
 */
const cfg = require('./config.js'),
    blankChar = cfg.blankChar,
    CssHandler = require('./CssHandler.js'),
    windowWidth = uni.getSystemInfoSync().windowWidth;
var emoji;
function MpHtmlParser(data, options = {}) {
    this.attrs = {};
    this.CssHandler = new CssHandler(options.tagStyle, windowWidth);
    this.data = data;
    this.domain = options.domain;
    this.DOM = [];
    this.i = this.start = this.audioNum = this.imgNum = this.videoNum = 0;
    options.prot = (this.domain || '').includes('://') ? this.domain.split('://')[0] : 'http';
    this.options = options;
    this.state = this.Text;
    this.STACK = [];
    // 工具函数
    this.bubble = () => {
        for (var i = this.STACK.length, item; item = this.STACK[--i];) {
            if (cfg.richOnlyTags[item.name]) return false;
            item.c = 1;
        }
        return true;
    }
    this.decode = (val, amp) => {
        var i = -1,
            j, en;
        while (1) {
            if ((i = val.indexOf('&', i + 1)) == -1) break;
            if ((j = val.indexOf(';', i + 2)) == -1) break;
            if (val[i + 1] == '#') {
                en = parseInt((val[i + 2] == 'x' ? '0' : '') + val.substring(i + 2, j));
                if (!isNaN(en)) val = val.substr(0, i) + String.fromCharCode(en) + val.substr(j + 1);
            } else {
                en = val.substring(i + 1, j);
                if (cfg.entities[en] || en == amp)
                    val = val.substr(0, i) + (cfg.entities[en] || '&') + val.substr(j + 1);
            }
        }
        return val;
    }
    this.getUrl = url => {
        if (url[0] == '/') {
            if (url[1] == '/') url = this.options.prot + ':' + url;
            else if (this.domain) url = this.domain + url;
        } else if (this.domain && url.indexOf('data:') != 0 && !url.includes('://'))
            url = this.domain + '/' + url;
        return url;
    }
    this.isClose = () => this.data[this.i] == '>' || (this.data[this.i] == '/' && this.data[this.i + 1] == '>');
    this.section = () => this.data.substring(this.start, this.i);
    this.parent = () => this.STACK[this.STACK.length - 1];
    this.siblings = () => this.STACK.length ? this.parent().children : this.DOM;
}
MpHtmlParser.prototype.parse = function() {
    if (emoji) this.data = emoji.parseEmoji(this.data);
    for (var c; c = this.data[this.i]; this.i++)
        this.state(c);
    if (this.state == this.Text) this.setText();
    while (this.STACK.length) this.popNode(this.STACK.pop());
    return this.DOM;
}
// 设置属性
MpHtmlParser.prototype.setAttr = function() {
    var name = this.attrName.toLowerCase(),
        val = this.attrVal;
    if (cfg.boolAttrs[name]) this.attrs[name] = 'T';
    else if (val) {
        if (name == 'src' || (name == 'data-src' && !this.attrs.src)) this.attrs.src = this.getUrl(this.decode(val, 'amp'));
        else if (name == 'href' || name == 'style') this.attrs[name] = this.decode(val, 'amp');
        else if (name.substr(0, 5) != 'data-') this.attrs[name] = val;
    }
    this.attrVal = '';
    while (blankChar[this.data[this.i]]) this.i++;
    if (this.isClose()) this.setNode();
    else {
        this.start = this.i;
        this.state = this.AttrName;
    }
}
// 设置文本节点
MpHtmlParser.prototype.setText = function() {
    var back, text = this.section();
    if (!text) return;
    text = (cfg.onText && cfg.onText(text, () => back = true)) || text;
    if (back) {
        this.data = this.data.substr(0, this.start) + text + this.data.substr(this.i);
        let j = this.start + text.length;
        for (this.i = this.start; this.i < j; this.i++) this.state(this.data[this.i]);
        return;
    }
    if (!this.pre) {
        // 合并空白符
        var flag, tmp = [];
        for (let i = text.length, c; c = text[--i];)
            if (!blankChar[c]) {
                tmp.unshift(c);
                if (!flag) flag = 1;
            } else {
                if (tmp[0] != ' ') tmp.unshift(' ');
                if (c == '\n' && flag == void 0) flag = 0;
            }
        if (flag == 0) return;
        text = tmp.join('');
    }
    this.siblings().push({
        type: 'text',
        text: this.decode(text)
    });
}
// 设置元素节点
MpHtmlParser.prototype.setNode = function() {
    var node = {
            name: this.tagName.toLowerCase(),
            attrs: this.attrs
        },
        close = cfg.selfClosingTags[node.name];
    if (this.options.nodes.length) node.type = 'node';
    this.attrs = {};
    if (!cfg.ignoreTags[node.name]) {
        // 处理属性
        var attrs = node.attrs,
            style = this.CssHandler.match(node.name, attrs, node) + (attrs.style || ''),
            styleObj = {};
        if (attrs.id) {
            if (this.options.compress & 1) attrs.id = void 0;
            else if (this.options.useAnchor) this.bubble();
        }
        if ((this.options.compress & 2) && attrs.class) attrs.class = void 0;
        switch (node.name) {
            case 'a':
            case 'ad': // #ifdef APP-PLUS
            case 'iframe':
                // #endif
                this.bubble();
                break;
            case 'font':
                if (attrs.color) {
                    styleObj['color'] = attrs.color;
                    attrs.color = void 0;
                }
                if (attrs.face) {
                    styleObj['font-family'] = attrs.face;
                    attrs.face = void 0;
                }
                if (attrs.size) {
                    var size = parseInt(attrs.size);
                    if (size < 1) size = 1;
                    else if (size > 7) size = 7;
                    var map = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'];
                    styleObj['font-size'] = map[size - 1];
                    attrs.size = void 0;
                }
                break;
            case 'embed':
                // #ifndef APP-PLUS
                var src = node.attrs.src || '',
                    type = node.attrs.type || '';
                if (type.includes('video') || src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8'))
                    node.name = 'video';
                else if (type.includes('audio') || src.includes('.m4a') || src.includes('.wav') || src.includes('.mp3') || src.includes(
                        '.aac'))
                    node.name = 'audio';
                else break;
                if (node.attrs.autostart)
                    node.attrs.autoplay = 'T';
                node.attrs.controls = 'T';
                // #endif
                // #ifdef APP-PLUS
                this.bubble();
                break;
                // #endif
            case 'video':
            case 'audio':
                if (!attrs.id) attrs.id = node.name + (++this[`${node.name}Num`]);
                else this[`${node.name}Num`]++;
                if (node.name == 'video') {
                    if (this.videoNum > 3)
                        node.lazyLoad = 1;
                    if (attrs.width) {
                        styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px');
                        attrs.width = void 0;
                    }
                    if (attrs.height) {
                        styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px');
                        attrs.height = void 0;
                    }
                }
                if (!attrs.controls && !attrs.autoplay) attrs.controls = 'T';
                attrs.source = [];
                if (attrs.src) {
                    attrs.source.push(attrs.src);
                    attrs.src = void 0;
                }
                this.bubble();
                break;
            case 'td':
            case 'th':
                if (attrs.colspan || attrs.rowspan)
                    for (var k = this.STACK.length, item; item = this.STACK[--k];)
                        if (item.name == 'table') {
                            item.flag = 1;
                            break;
                        }
        }
        if (attrs.align) {
            if (node.name == 'table') {
                if (attrs.align == 'center') styleObj['margin-inline-start'] = styleObj['margin-inline-end'] = 'auto';
                else styleObj['float'] = attrs.align;
            } else styleObj['text-align'] = attrs.align;
            attrs.align = void 0;
        }
        // 压缩 style
        var styles = style.split(';');
        style = '';
        for (var i = 0, len = styles.length; i < len; i++) {
            var info = styles[i].split(':');
            if (info.length < 2) continue;
            let key = info[0].trim().toLowerCase(),
                value = info.slice(1).join(':').trim();
            if (value[0] == '-' || value.includes('safe'))
                style += `;${key}:${value}`;
            else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import'))
                styleObj[key] = value;
        }
        if (node.name == 'img') {
            if (attrs.src && !attrs.ignore) {
                if (this.bubble())
                    attrs.i = (this.imgNum++).toString();
                else attrs.ignore = 'T';
            }
            if (attrs.ignore) {
                style += ';-webkit-touch-callout:none';
                styleObj['max-width'] = '100%';
            }
            var width;
            if (styleObj.width) width = styleObj.width;
            else if (attrs.width) width = attrs.width.includes('%') ? attrs.width : parseFloat(attrs.width) + 'px';
            if (width) {
                styleObj.width = width;
                attrs.width = '100%';
                if (parseInt(width) > windowWidth) {
                    styleObj.height = '';
                    if (attrs.height) attrs.height = void 0;
                }
            }
            if (styleObj.height) {
                attrs.height = styleObj.height;
                styleObj.height = '';
            } else if (attrs.height && !attrs.height.includes('%'))
                attrs.height = parseFloat(attrs.height) + 'px';
        }
        for (var key in styleObj) {
            var value = styleObj[key];
            if (!value) continue;
            if (key.includes('flex') || key == 'order' || key == 'self-align') node.c = 1;
            // 填充链接
            if (value.includes('url')) {
                var j = value.indexOf('(');
                if (j++ != -1) {
                    while (value[j] == '"' || value[j] == "'" || blankChar[value[j]]) j++;
                    value = value.substr(0, j) + this.getUrl(value.substr(j));
                }
            }
            // 转换 rpx
            else if (value.includes('rpx'))
                value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px');
            else if (key == 'white-space' && value.includes('pre') && !close)
                this.pre = node.pre = true;
            style += `;${key}:${value}`;
        }
        style = style.substr(1);
        if (style) attrs.style = style;
        if (!close) {
            node.children = [];
            if (node.name == 'pre' && cfg.highlight) {
                this.remove(node);
                this.pre = node.pre = true;
            }
            this.siblings().push(node);
            this.STACK.push(node);
        } else if (!cfg.filter || cfg.filter(node, this) != false)
            this.siblings().push(node);
    } else {
        if (!close) this.remove(node);
        else if (node.name == 'source') {
            var parent = this.parent();
            if (parent && (parent.name == 'video' || parent.name == 'audio') && node.attrs.src)
                parent.attrs.source.push(node.attrs.src);
        } else if (node.name == 'base' && !this.domain) this.domain = node.attrs.href;
    }
    if (this.data[this.i] == '/') this.i++;
    this.start = this.i + 1;
    this.state = this.Text;
}
// 移除标签
MpHtmlParser.prototype.remove = function(node) {
    var name = node.name,
        j = this.i;
    // 处理 svg
    var handleSvg = () => {
        var src = this.data.substring(j, this.i + 1);
        node.attrs.xmlns = 'http://www.w3.org/2000/svg';
        for (var key in node.attrs) {
            if (key == 'viewbox') src = ` viewBox="${node.attrs.viewbox}"` + src;
            else if (key != 'style') src = ` ${key}="${node.attrs[key]}"` + src;
        }
        src = '<svg' + src;
        var parent = this.parent();
        if (node.attrs.width == '100%' && parent && (parent.attrs.style || '').includes('inline'))
            parent.attrs.style = 'width:300px;max-width:100%;' + parent.attrs.style;
        this.siblings().push({
            name: 'img',
            attrs: {
                src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
                style: node.attrs.style,
                ignore: 'T'
            }
        })
    }
    if (node.name == 'svg' && this.data[j] == '/') return handleSvg(this.i++);
    while (1) {
        if ((this.i = this.data.indexOf('</', this.i + 1)) == -1) {
            if (name == 'pre' || name == 'svg') this.i = j;
            else this.i = this.data.length;
            return;
        }
        this.start = (this.i += 2);
        while (!blankChar[this.data[this.i]] && !this.isClose()) this.i++;
        if (this.section().toLowerCase() == name) {
            // 代码块高亮
            if (name == 'pre') {
                this.data = this.data.substr(0, j + 1) + cfg.highlight(this.data.substring(j + 1, this.i - 5), node.attrs) + this.data
                    .substr(this.i - 5);
                return this.i = j;
            } else if (name == 'style')
                this.CssHandler.getStyle(this.data.substring(j + 1, this.i - 7));
            else if (name == 'title')
                this.DOM.title = this.data.substring(j + 1, this.i - 7);
            if ((this.i = this.data.indexOf('>', this.i)) == -1) this.i = this.data.length;
            if (name == 'svg') handleSvg();
            return;
        }
    }
}
// 节点出栈处理
MpHtmlParser.prototype.popNode = function(node) {
    // 空白符处理
    if (node.pre) {
        node.pre = this.pre = void 0;
        for (let i = this.STACK.length; i--;)
            if (this.STACK[i].pre)
                this.pre = true;
    }
    var siblings = this.siblings(),
        len = siblings.length,
        childs = node.children;
    if (node.name == 'head' || (cfg.filter && cfg.filter(node, this) == false))
        return siblings.pop();
    var attrs = node.attrs;
    // 替换一些标签名
    if (cfg.blockTags[node.name]) node.name = 'div';
    else if (!cfg.trustTags[node.name]) node.name = 'span';
    // 处理列表
    if (node.c && (node.name == 'ul' || node.name == 'ol')) {
        if ((node.attrs.style || '').includes('list-style:none')) {
            for (let i = 0, child; child = childs[i++];)
                if (child.name == 'li')
                    child.name = 'div';
        } else if (node.name == 'ul') {
            var floor = 1;
            for (let i = this.STACK.length; i--;)
                if (this.STACK[i].name == 'ul') floor++;
            if (floor != 1)
                for (let i = childs.length; i--;)
                    childs[i].floor = floor;
        } else {
            for (let i = 0, num = 1, child; child = childs[i++];)
                if (child.name == 'li') {
                    child.type = 'ol';
                    child.num = ((num, type) => {
                        if (type == 'a') return String.fromCharCode(97 + (num - 1) % 26);
                        if (type == 'A') return String.fromCharCode(65 + (num - 1) % 26);
                        if (type == 'i' || type == 'I') {
                            num = (num - 1) % 99 + 1;
                            var one = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'],
                                ten = ['X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'],
                                res = (ten[Math.floor(num / 10) - 1] || '') + (one[num % 10 - 1] || '');
                            if (type == 'i') return res.toLowerCase();
                            return res;
                        }
                        return num;
                    })(num++, attrs.type) + '.';
                }
        }
    }
    // 处理表格
    if (node.name == 'table') {
        var padding = parseFloat(attrs.cellpadding),
            spacing = parseFloat(attrs.cellspacing),
            border = parseFloat(attrs.border);
        if (node.c) {
            if (isNaN(padding)) padding = 2;
            if (isNaN(spacing)) spacing = 2;
        }
        if (border) attrs.style = `border:${border}px solid gray;${attrs.style || ''}`;
        if (node.flag && node.c) {
            // 有 colspan 或 rowspan 且含有链接的表格转为 grid 布局实现
            attrs.style = `${attrs.style || ''};${spacing ? `;grid-gap:${spacing}px` : ';border-left:0;border-top:0'}`;
            var row = 1,
                col = 1,
                colNum,
                trs = [],
                children = [],
                map = {};
            (function f(ns) {
                for (var i = 0; i < ns.length; i++) {
                    if (ns[i].name == 'tr') trs.push(ns[i]);
                    else f(ns[i].children || []);
                }
            })(node.children)
            for (let i = 0; i < trs.length; i++) {
                for (let j = 0, td; td = trs[i].children[j]; j++) {
                    if (td.name == 'td' || td.name == 'th') {
                        while (map[row + '.' + col]) col++;
                        var cell = {
                            name: 'div',
                            c: 1,
                            attrs: {
                                style: (td.attrs.style || '') + (border ? `;border:${border}px solid gray` + (spacing ? '' :
                                    ';border-right:0;border-bottom:0') : '') + (padding ? `;padding:${padding}px` : '')
                            },
                            children: td.children
                        }
                        if (td.attrs.colspan) {
                            cell.attrs.style += ';grid-column-start:' + col + ';grid-column-end:' + (col + parseInt(td.attrs.colspan));
                            if (!td.attrs.rowspan) cell.attrs.style += ';grid-row-start:' + row + ';grid-row-end:' + (row + 1);
                            col += parseInt(td.attrs.colspan) - 1;
                        }
                        if (td.attrs.rowspan) {
                            cell.attrs.style += ';grid-row-start:' + row + ';grid-row-end:' + (row + parseInt(td.attrs.rowspan));
                            if (!td.attrs.colspan) cell.attrs.style += ';grid-column-start:' + col + ';grid-column-end:' + (col + 1);
                            for (var k = 1; k < td.attrs.rowspan; k++) map[(row + k) + '.' + col] = 1;
                        }
                        children.push(cell);
                        col++;
                    }
                }
                if (!colNum) {
                    colNum = col - 1;
                    attrs.style += `;grid-template-columns:repeat(${colNum},auto)`
                }
                col = 1;
                row++;
            }
            node.children = children;
        } else {
            attrs.style = `border-spacing:${spacing}px;${attrs.style || ''}`;
            if (border || padding)
                (function f(ns) {
                    for (var i = 0, n; n = ns[i]; i++) {
                        if (n.name == 'th' || n.name == 'td') {
                            if (border) n.attrs.style = `border:${border}px solid gray;${n.attrs.style || ''}`;
                            if (padding) n.attrs.style = `padding:${padding}px;${n.attrs.style || ''}`;
                        } else f(n.children || []);
                    }
                })(childs)
        }
        if (this.options.autoscroll) {
            var table = Object.assign({}, node);
            node.name = 'div';
            node.attrs = {
                style: 'overflow:scroll'
            }
            node.children = [table];
        }
    }
    this.CssHandler.pop && this.CssHandler.pop(node);
    // 自动压缩
    if (node.name == 'div' && !Object.keys(attrs).length && childs.length == 1 && childs[0].name == 'div')
        siblings[len - 1] = childs[0];
}
// 状态机
MpHtmlParser.prototype.Text = function(c) {
    if (c == '<') {
        var next = this.data[this.i + 1],
            isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
        if (isLetter(next)) {
            this.setText();
            this.start = this.i + 1;
            this.state = this.TagName;
        } else if (next == '/') {
            this.setText();
            if (isLetter(this.data[++this.i + 1])) {
                this.start = this.i + 1;
                this.state = this.EndTag;
            } else this.Comment();
        } else if (next == '!' || next == '?') {
            this.setText();
            this.Comment();
        }
    }
}
MpHtmlParser.prototype.Comment = function() {
    var key;
    if (this.data.substring(this.i + 2, this.i + 4) == '--') key = '-->';
    else if (this.data.substring(this.i + 2, this.i + 9) == '[CDATA[') key = ']]>';
    else key = '>';
    if ((this.i = this.data.indexOf(key, this.i + 2)) == -1) this.i = this.data.length;
    else this.i += key.length - 1;
    this.start = this.i + 1;
    this.state = this.Text;
}
MpHtmlParser.prototype.TagName = function(c) {
    if (blankChar[c]) {
        this.tagName = this.section();
        while (blankChar[this.data[this.i]]) this.i++;
        if (this.isClose()) this.setNode();
        else {
            this.start = this.i;
            this.state = this.AttrName;
        }
    } else if (this.isClose()) {
        this.tagName = this.section();
        this.setNode();
    }
}
MpHtmlParser.prototype.AttrName = function(c) {
    if (c == '=' || blankChar[c] || this.isClose()) {
        this.attrName = this.section();
        if (blankChar[c])
            while (blankChar[this.data[++this.i]]);
        if (this.data[this.i] == '=') {
            while (blankChar[this.data[++this.i]]);
            this.start = this.i--;
            this.state = this.AttrValue;
        } else this.setAttr();
    }
}
MpHtmlParser.prototype.AttrValue = function(c) {
    if (c == '"' || c == "'") {
        this.start++;
        if ((this.i = this.data.indexOf(c, this.i + 1)) == -1) return this.i = this.data.length;
        this.attrVal = this.section();
        this.i++;
    } else {
        for (; !blankChar[this.data[this.i]] && !this.isClose(); this.i++);
        this.attrVal = this.section();
    }
    this.setAttr();
}
MpHtmlParser.prototype.EndTag = function(c) {
    if (blankChar[c] || c == '>' || c == '/') {
        var name = this.section().toLowerCase();
        for (var i = this.STACK.length; i--;)
            if (this.STACK[i].name == name) break;
        if (i != -1) {
            var node;
            while ((node = this.STACK.pop()).name != name) this.popNode(node);
            this.popNode(node);
        } else if (name == 'p' || name == 'br')
            this.siblings().push({
                name,
                attrs: {}
            });
        this.i = this.data.indexOf('>', this.i);
        this.start = this.i + 1;
        if (this.i == -1) this.i = this.data.length;
        else this.state = this.Text;
    }
}
module.exports = MpHtmlParser;
uview-ui/components/u-parse/libs/config.js
New file
@@ -0,0 +1,80 @@
/* 配置文件 */
var cfg = {
    // 出错占位图
    errorImg: null,
    // 过滤器函数
    filter: null,
    // 代码高亮函数
    highlight: null,
    // 文本处理函数
    onText: null,
    // 实体编码列表
    entities: {
        quot: '"',
        apos: "'",
        semi: ';',
        nbsp: '\xA0',
        ensp: '\u2002',
        emsp: '\u2003',
        ndash: '–',
        mdash: '—',
        middot: '·',
        lsquo: '‘',
        rsquo: '’',
        ldquo: '“',
        rdquo: '”',
        bull: '•',
        hellip: '…'
    },
    blankChar: makeMap(' ,\xA0,\t,\r,\n,\f'),
    boolAttrs: makeMap('allowfullscreen,autoplay,autostart,controls,ignore,loop,muted'),
    // 块级标签,将被转为 div
    blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section'),
    // 将被移除的标签
    ignoreTags: makeMap('area,base,canvas,frame,iframe,input,link,map,meta,param,script,source,style,svg,textarea,title,track,wbr'),
    // 只能被 rich-text 显示的标签
    richOnlyTags: makeMap('a,colgroup,fieldset,legend'),
    // 自闭合的标签
    selfClosingTags: makeMap('area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'),
    // 信任的标签
    trustTags: makeMap('a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'),
    // 默认的标签样式
    userAgentStyles: {
        address: 'font-style:italic',
        big: 'display:inline;font-size:1.2em',
        blockquote: 'background-color:#f6f6f6;border-left:3px solid #dbdbdb;color:#6c6c6c;padding:5px 0 5px 10px',
        caption: 'display:table-caption;text-align:center',
        center: 'text-align:center',
        cite: 'font-style:italic',
        dd: 'margin-left:40px',
        mark: 'background-color:yellow',
        pre: 'font-family:monospace;white-space:pre;overflow:scroll',
        s: 'text-decoration:line-through',
        small: 'display:inline;font-size:0.8em',
        u: 'text-decoration:underline'
    }
}
function makeMap(str) {
    var map = Object.create(null),
        list = str.split(',');
    for (var i = list.length; i--;)
        map[list[i]] = true;
    return map;
}
// #ifdef MP-WEIXIN
if (wx.canIUse('editor')) {
    cfg.blockTags.pre = void 0;
    cfg.ignoreTags.rp = true;
    Object.assign(cfg.richOnlyTags, makeMap('bdi,bdo,caption,rt,ruby'));
    Object.assign(cfg.trustTags, makeMap('bdi,bdo,caption,pre,rt,ruby'));
}
// #endif
// #ifdef APP-PLUS
cfg.ignoreTags.iframe = void 0;
Object.assign(cfg.trustTags, makeMap('embed,iframe'));
// #endif
module.exports = cfg;
uview-ui/components/u-parse/libs/handler.wxs
New file
@@ -0,0 +1,22 @@
var inline = {
    abbr: 1,
    b: 1,
    big: 1,
    code: 1,
    del: 1,
    em: 1,
    i: 1,
    ins: 1,
    label: 1,
    q: 1,
    small: 1,
    span: 1,
    strong: 1,
    sub: 1,
    sup: 1
}
module.exports = {
    use: function(item) {
        return !item.c && !inline[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1
    }
}
uview-ui/components/u-parse/libs/trees.vue
New file
@@ -0,0 +1,505 @@
<template>
    <view :class="'interlayer '+(c||'')" :style="s">
        <block v-for="(n, i) in nodes" v-bind:key="i">
            <!--图片-->
            <view v-if="n.name=='img'" :class="'_img '+n.attrs.class" :style="n.attrs.style" :data-attrs="n.attrs" @tap.stop="imgtap">
                <rich-text v-if="ctrl[i]!=0" :nodes="[{attrs:{src:loading&&(ctrl[i]||0)<2?loading:(lazyLoad&&!ctrl[i]?placeholder:(ctrl[i]==3?errorImg:n.attrs.src||'')),alt:n.attrs.alt||'',width:n.attrs.width||'',style:'-webkit-touch-callout:none;max-width:100%;display:block'+(n.attrs.height?';height:'+n.attrs.height:'')},name:'img'}]" />
                <image class="_image" :src="lazyLoad&&!ctrl[i]?placeholder:n.attrs.src" :lazy-load="lazyLoad"
                 :show-menu-by-longpress="!n.attrs.ignore" :data-i="i" :data-index="n.attrs.i" data-source="img" @load="loadImg"
                 @error="error" />
            </view>
            <!--文本-->
            <text v-else-if="n.type=='text'" decode>{{n.text}}</text>
            <!--#ifndef MP-BAIDU-->
            <text v-else-if="n.name=='br'">\n</text>
            <!--#endif-->
            <!--视频-->
            <view v-else-if="((n.lazyLoad&&!n.attrs.autoplay)||(n.name=='video'&&!loadVideo))&&ctrl[i]==undefined" :id="n.attrs.id"
             :class="'_video '+(n.attrs.class||'')" :style="n.attrs.style" :data-i="i" @tap.stop="_loadVideo" />
            <video v-else-if="n.name=='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay||ctrl[i]==0"
             :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :poster="n.attrs.poster" :src="n.attrs.source[ctrl[i]||0]"
             :unit-id="n.attrs['unit-id']" :data-id="n.attrs.id" :data-i="i" data-source="video" @error="error" @play="play" />
            <!--音频-->
            <audio v-else-if="n.name=='audio'" :ref="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author"
             :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster"
             :src="n.attrs.source[ctrl[i]||0]" :data-i="i" :data-id="n.attrs.id" data-source="audio" @error.native="error"
             @play.native="play" />
            <!--链接-->
            <view v-else-if="n.name=='a'" :id="n.attrs.id" :class="'_a '+(n.attrs.class||'')" hover-class="_hover" :style="n.attrs.style"
             :data-attrs="n.attrs" @tap.stop="linkpress">
                <trees class="_span" c="_span" :nodes="n.children" />
            </view>
            <!--广告-->
            <!--<ad v-else-if="n.name=='ad'" :class="n.attrs.class" :style="n.attrs.style" :unit-id="n.attrs['unit-id']" :appid="n.attrs.appid" :apid="n.attrs.apid" :type="n.attrs.type" :adpid="n.attrs.adpid" data-source="ad" @error="error" />-->
            <!--列表-->
            <view v-else-if="n.name=='li'" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:flex;flex-direction:row'">
                <view v-if="n.type=='ol'" class="_ol-bef">{{n.num}}</view>
                <view v-else class="_ul-bef">
                    <view v-if="n.floor%3==0" class="_ul-p1">█</view>
                    <view v-else-if="n.floor%3==2" class="_ul-p2" />
                    <view v-else class="_ul-p1" style="border-radius:50%">█</view>
                </view>
                <trees class="_li" c="_li" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
            </view>
            <!--表格-->
            <view v-else-if="n.name=='table'&&n.c&&n.flag" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:grid'">
                <trees v-for="(cell,n) in n.children" v-bind:key="n" :class="cell.attrs.class" :c="cell.attrs.class" :style="cell.attrs.style"
                 :s="cell.attrs.style" :nodes="cell.children" />
            </view>
            <view v-else-if="n.name=='table'&&n.c" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:table'">
                <view v-for="(tbody, o) in n.children" v-bind:key="o" :class="tbody.attrs.class" :style="(tbody.attrs.style||'')+(tbody.name[0]=='t'?';display:table-'+(tbody.name=='tr'?'row':'row-group'):'')">
                    <view v-for="(tr, p) in tbody.children" v-bind:key="p" :class="tr.attrs.class" :style="(tr.attrs.style||'')+(tr.name[0]=='t'?';display:table-'+(tr.name=='tr'?'row':'cell'):'')">
                        <trees v-if="tr.name=='td'" :nodes="tr.children" />
                        <trees v-else v-for="(td, q) in tr.children" v-bind:key="q" :class="td.attrs.class" :c="td.attrs.class" :style="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')"
                         :s="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')" :nodes="td.children" />
                    </view>
                </view>
            </view>
            <!--#ifdef APP-PLUS-->
            <iframe v-else-if="n.name=='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder"
             :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
            <embed v-else-if="n.name=='embed'" :style="n.attrs.style" :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
            <!--#endif-->
            <!--富文本-->
            <!--#ifdef MP-WEIXIN || MP-QQ || APP-PLUS-->
            <rich-text v-else-if="handler.use(n)" :id="n.attrs.id" :class="'_p __'+n.name" :nodes="[n]" />
            <!--#endif-->
            <!--#ifndef MP-WEIXIN || MP-QQ || APP-PLUS-->
            <rich-text v-else-if="!n.c" :id="n.attrs.id" :nodes="[n]" style="display:inline" />
            <!--#endif-->
            <trees v-else :class="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')" :c="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')"
             :style="n.attrs.style" :s="n.attrs.style" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
        </block>
    </view>
</template>
<script module="handler" lang="wxs" src="./handler.wxs"></script>
<script>
    global.Parser = {};
    import trees from './trees'
    const errorImg = require('../libs/config.js').errorImg;
    export default {
        components: {
            trees
        },
        name: 'trees',
        data() {
            return {
                ctrl: [],
                placeholder: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="300" height="225"/>',
                errorImg,
                loadVideo: typeof plus == 'undefined',
                // #ifndef MP-ALIPAY
                c: '',
                s: ''
                // #endif
            }
        },
        props: {
            nodes: Array,
            lazyLoad: Boolean,
            loading: String,
            // #ifdef MP-ALIPAY
            c: String,
            s: String
            // #endif
        },
        mounted() {
            for (this.top = this.$parent; this.top.$options.name != 'parser'; this.top = this.top.$parent);
            this.init();
        },
        // #ifdef APP-PLUS
        beforeDestroy() {
            this.observer && this.observer.disconnect();
        },
        // #endif
        methods: {
            init() {
                for (var i = this.nodes.length, n; n = this.nodes[--i];) {
                    if (n.name == 'img') {
                        this.top.imgList.setItem(n.attrs.i, n.attrs['original-src'] || n.attrs.src);
                        // #ifdef APP-PLUS
                        if (this.lazyLoad && !this.observer) {
                            this.observer = uni.createIntersectionObserver(this).relativeToViewport({
                                top: 500,
                                bottom: 500
                            });
                            setTimeout(() => {
                                this.observer.observe('._img', res => {
                                    if (res.intersectionRatio) {
                                        for (var j = this.nodes.length; j--;)
                                            if (this.nodes[j].name == 'img')
                                                this.$set(this.ctrl, j, 1);
                                        this.observer.disconnect();
                                    }
                                })
                            }, 0)
                        }
                        // #endif
                    } else if (n.name == 'video' || n.name == 'audio') {
                        var ctx;
                        if (n.name == 'video') {
                            ctx = uni.createVideoContext(n.attrs.id
                                // #ifndef MP-BAIDU
                                , this
                                // #endif
                            );
                        } else if (this.$refs[n.attrs.id])
                            ctx = this.$refs[n.attrs.id][0];
                        if (ctx) {
                            ctx.id = n.attrs.id;
                            this.top.videoContexts.push(ctx);
                        }
                    }
                }
                // #ifdef APP-PLUS
                // APP 上避免 video 错位需要延时渲染
                setTimeout(() => {
                    this.loadVideo = true;
                }, 1000)
                // #endif
            },
            play(e) {
                var contexts = this.top.videoContexts;
                if (contexts.length > 1 && this.top.autopause)
                    for (var i = contexts.length; i--;)
                        if (contexts[i].id != e.currentTarget.dataset.id)
                            contexts[i].pause();
            },
            imgtap(e) {
                var attrs = e.currentTarget.dataset.attrs;
                if (!attrs.ignore) {
                    var preview = true,
                        data = {
                            id: e.target.id,
                            src: attrs.src,
                            ignore: () => preview = false
                        };
                    global.Parser.onImgtap && global.Parser.onImgtap(data);
                    this.top.$emit('imgtap', data);
                    if (preview) {
                        var urls = this.top.imgList,
                            current = urls[attrs.i] ? parseInt(attrs.i) : (urls = [attrs.src], 0);
                        uni.previewImage({
                            current,
                            urls
                        })
                    }
                }
            },
            loadImg(e) {
                var i = e.currentTarget.dataset.i;
                if (this.lazyLoad && !this.ctrl[i]) {
                    // #ifdef QUICKAPP-WEBVIEW
                    this.$set(this.ctrl, i, 0);
                    this.$nextTick(function() {
                        // #endif
                        // #ifndef APP-PLUS
                        this.$set(this.ctrl, i, 1);
                        // #endif
                        // #ifdef QUICKAPP-WEBVIEW
                    })
                    // #endif
                } else if (this.loading && this.ctrl[i] != 2) {
                    // #ifdef QUICKAPP-WEBVIEW
                    this.$set(this.ctrl, i, 0);
                    this.$nextTick(function() {
                        // #endif
                        this.$set(this.ctrl, i, 2);
                        // #ifdef QUICKAPP-WEBVIEW
                    })
                    // #endif
                }
            },
            linkpress(e) {
                var jump = true,
                    attrs = e.currentTarget.dataset.attrs;
                attrs.ignore = () => jump = false;
                global.Parser.onLinkpress && global.Parser.onLinkpress(attrs);
                this.top.$emit('linkpress', attrs);
                if (jump) {
                    // #ifdef MP
                    if (attrs['app-id']) {
                        return uni.navigateToMiniProgram({
                            appId: attrs['app-id'],
                            path: attrs.path
                        })
                    }
                    // #endif
                    if (attrs.href) {
                        if (attrs.href[0] == '#') {
                            if (this.top.useAnchor)
                                this.top.navigateTo({
                                    id: attrs.href.substring(1)
                                })
                        } else if (attrs.href.indexOf('http') == 0 || attrs.href.indexOf('//') == 0) {
                            // #ifdef APP-PLUS
                            plus.runtime.openWeb(attrs.href);
                            // #endif
                            // #ifndef APP-PLUS
                            uni.setClipboardData({
                                data: attrs.href,
                                success: () =>
                                    uni.showToast({
                                        title: '链接已复制'
                                    })
                            })
                            // #endif
                        } else
                            uni.navigateTo({
                                url: attrs.href,
                                fail() {
                                    uni.switchTab({
                                        url: attrs.href,
                                    })
                                }
                            })
                    }
                }
            },
            error(e) {
                var target = e.currentTarget,
                    source = target.dataset.source,
                    i = target.dataset.i;
                if (source == 'video' || source == 'audio') {
                    // 加载其他 source
                    var index = this.ctrl[i] ? this.ctrl[i].i + 1 : 1;
                    if (index < this.nodes[i].attrs.source.length)
                        this.$set(this.ctrl, i, index);
                    if (e.detail.__args__)
                        e.detail = e.detail.__args__[0];
                } else if (errorImg && source == 'img') {
                    this.top.imgList.setItem(target.dataset.index, errorImg);
                    this.$set(this.ctrl, i, 3);
                }
                this.top && this.top.$emit('error', {
                    source,
                    target,
                    errMsg: e.detail.errMsg
                });
            },
            _loadVideo(e) {
                this.$set(this.ctrl, e.target.dataset.i, 0);
            }
        }
    }
</script>
<style>
    /* 在这里引入自定义样式 */
    /* 链接和图片效果 */
    ._a {
        display: inline;
        padding: 1.5px 0 1.5px 0;
        color: #366092;
        word-break: break-all;
    }
    ._hover {
        text-decoration: underline;
        opacity: 0.7;
    }
    ._img {
        display: inline-block;
        max-width: 100%;
        overflow: hidden;
    }
    /* #ifdef MP-WEIXIN */
    :host {
        display: inline;
    }
    /* #endif */
    /* #ifndef MP-ALIPAY || APP-PLUS */
    .interlayer {
        display: inherit;
        flex-direction: inherit;
        flex-wrap: inherit;
        align-content: inherit;
        align-items: inherit;
        justify-content: inherit;
        width: 100%;
        white-space: inherit;
    }
    /* #endif */
    ._b,
    ._strong {
        font-weight: bold;
    }
    /* #ifndef MP-ALIPAY */
    ._blockquote,
    ._div,
    ._p,
    ._ol,
    ._ul,
    ._li {
        display: block;
    }
    /* #endif */
    ._code {
        font-family: monospace;
    }
    ._del {
        text-decoration: line-through;
    }
    ._em,
    ._i {
        font-style: italic;
    }
    ._h1 {
        font-size: 2em;
    }
    ._h2 {
        font-size: 1.5em;
    }
    ._h3 {
        font-size: 1.17em;
    }
    ._h5 {
        font-size: 0.83em;
    }
    ._h6 {
        font-size: 0.67em;
    }
    ._h1,
    ._h2,
    ._h3,
    ._h4,
    ._h5,
    ._h6 {
        display: block;
        font-weight: bold;
    }
    ._image {
        display: block;
        width: 100%;
        height: 360px;
        margin-top: -360px;
        opacity: 0;
    }
    ._ins {
        text-decoration: underline;
    }
    ._li {
        flex: 1;
        width: 0;
    }
    ._ol-bef {
        width: 36px;
        margin-right: 5px;
        text-align: right;
    }
    ._ul-bef {
        display: block;
        margin: 0 12px 0 23px;
        line-height: normal;
    }
    ._ol-bef,
    ._ul-bef {
        flex: none;
        user-select: none;
    }
    ._ul-p1 {
        display: inline-block;
        width: 0.3em;
        height: 0.3em;
        overflow: hidden;
        line-height: 0.3em;
    }
    ._ul-p2 {
        display: inline-block;
        width: 0.23em;
        height: 0.23em;
        border: 0.05em solid black;
        border-radius: 50%;
    }
    ._q::before {
        content: '"';
    }
    ._q::after {
        content: '"';
    }
    ._sub {
        font-size: smaller;
        vertical-align: sub;
    }
    ._sup {
        font-size: smaller;
        vertical-align: super;
    }
    /* #ifdef MP-ALIPAY || APP-PLUS || QUICKAPP-WEBVIEW */
    ._abbr,
    ._b,
    ._code,
    ._del,
    ._em,
    ._i,
    ._ins,
    ._label,
    ._q,
    ._span,
    ._strong,
    ._sub,
    ._sup {
        display: inline;
    }
    /* #endif */
    /* #ifdef MP-WEIXIN || MP-QQ */
    .__bdo,
    .__bdi,
    .__ruby,
    .__rt {
        display: inline-block;
    }
    /* #endif */
    ._video {
        position: relative;
        display: inline-block;
        width: 300px;
        height: 225px;
        background-color: black;
    }
    ._video::after {
        position: absolute;
        top: 50%;
        left: 50%;
        margin: -15px 0 0 -15px;
        content: '';
        border-color: transparent transparent transparent white;
        border-style: solid;
        border-width: 15px 0 15px 30px;
    }
</style>
uview-ui/components/u-parse/u-parse.vue
New file
@@ -0,0 +1,645 @@
<template>
    <view>
        <slot v-if="!nodes.length" />
        <!--#ifdef APP-PLUS-NVUE-->
        <web-view id="_top" ref="web" :style="'margin-top:-2px;height:'+height+'px'" @onPostMessage="_message" />
        <!--#endif-->
        <!--#ifndef APP-PLUS-NVUE-->
        <view id="_top" :style="showAm+(selectable?';user-select:text;-webkit-user-select:text':'')">
            <!--#ifdef H5 || MP-360-->
            <div :id="'rtf'+uid"></div>
            <!--#endif-->
            <!--#ifndef H5 || MP-360-->
            <trees :nodes="nodes" :lazyLoad="lazyLoad" :loading="loadingImg" />
            <!--#endif-->
        </view>
        <!--#endif-->
    </view>
</template>
<script>
    var search;
    // #ifndef H5 || APP-PLUS-NVUE || MP-360
    import trees from './libs/trees';
    var cache = {},
        // #ifdef MP-WEIXIN || MP-TOUTIAO
        fs = uni.getFileSystemManager ? uni.getFileSystemManager() : null,
        // #endif
        Parser = require('./libs/MpHtmlParser.js');
    var dom;
    // 计算 cache 的 key
    function hash(str) {
        for (var i = str.length, val = 5381; i--;)
            val += (val << 5) + str.charCodeAt(i);
        return val;
    }
    // #endif
    // #ifdef H5 || APP-PLUS-NVUE || MP-360
    var {
        windowWidth,
        platform
    } = uni.getSystemInfoSync(),
        cfg = require('./libs/config.js');
    // #endif
    // #ifdef APP-PLUS-NVUE
    var weexDom = weex.requireModule('dom');
    // #endif
    /**
     * Parser 富文本组件
     * @tutorial https://github.com/jin-yufeng/Parser
     * @property {String} html 富文本数据
     * @property {Boolean} autopause 是否在播放一个视频时自动暂停其他视频
     * @property {Boolean} autoscroll 是否自动给所有表格添加一个滚动层
     * @property {Boolean} autosetTitle 是否自动将 title 标签中的内容设置到页面标题
     * @property {Number} compress 压缩等级
     * @property {String} domain 图片、视频等链接的主域名
     * @property {Boolean} lazyLoad 是否开启图片懒加载
     * @property {String} loadingImg 图片加载完成前的占位图
     * @property {Boolean} selectable 是否开启长按复制
     * @property {Object} tagStyle 标签的默认样式
     * @property {Boolean} showWithAnimation 是否使用渐显动画
     * @property {Boolean} useAnchor 是否使用锚点
     * @property {Boolean} useCache 是否缓存解析结果
     * @event {Function} parse 解析完成事件
     * @event {Function} load dom 加载完成事件
     * @event {Function} ready 所有图片加载完毕事件
     * @event {Function} error 错误事件
     * @event {Function} imgtap 图片点击事件
     * @event {Function} linkpress 链接点击事件
     * @author JinYufeng
     * @version 20201029
     * @listens MIT
     */
    export default {
        name: 'parser',
        data() {
            return {
                // #ifdef H5 || MP-360
                uid: this._uid,
                // #endif
                // #ifdef APP-PLUS-NVUE
                height: 1,
                // #endif
                // #ifndef APP-PLUS-NVUE
                showAm: '',
                // #endif
                nodes: []
            }
        },
        // #ifndef H5 || APP-PLUS-NVUE || MP-360
        components: {
            trees
        },
        // #endif
        props: {
            html: String,
            autopause: {
                type: Boolean,
                default: true
            },
            autoscroll: Boolean,
            autosetTitle: {
                type: Boolean,
                default: true
            },
            // #ifndef H5 || APP-PLUS-NVUE || MP-360
            compress: Number,
            loadingImg: String,
            useCache: Boolean,
            // #endif
            domain: String,
            lazyLoad: Boolean,
            selectable: Boolean,
            tagStyle: Object,
            showWithAnimation: Boolean,
            useAnchor: Boolean
        },
        watch: {
            html(html) {
                this.setContent(html);
            }
        },
        created() {
            // 图片数组
            this.imgList = [];
            this.imgList.each = function(f) {
                for (var i = 0, len = this.length; i < len; i++)
                    this.setItem(i, f(this[i], i, this));
            }
            this.imgList.setItem = function(i, src) {
                if (i == void 0 || !src) return;
                // #ifndef MP-ALIPAY || APP-PLUS
                // 去重
                if (src.indexOf('http') == 0 && this.includes(src)) {
                    var newSrc = src.split('://')[0];
                    for (var j = newSrc.length, c; c = src[j]; j++) {
                        if (c == '/' && src[j - 1] != '/' && src[j + 1] != '/') break;
                        newSrc += Math.random() > 0.5 ? c.toUpperCase() : c;
                    }
                    newSrc += src.substr(j);
                    return this[i] = newSrc;
                }
                // #endif
                this[i] = src;
                // 暂存 data src
                if (src.includes('data:image')) {
                    var filePath, info = src.match(/data:image\/(\S+?);(\S+?),(.+)/);
                    if (!info) return;
                    // #ifdef MP-WEIXIN || MP-TOUTIAO
                    filePath = `${wx.env.USER_DATA_PATH}/${Date.now()}.${info[1]}`;
                    fs && fs.writeFile({
                        filePath,
                        data: info[3],
                        encoding: info[2],
                        success: () => this[i] = filePath
                    })
                    // #endif
                    // #ifdef APP-PLUS
                    filePath = `_doc/parser_tmp/${Date.now()}.${info[1]}`;
                    var bitmap = new plus.nativeObj.Bitmap();
                    bitmap.loadBase64Data(src, () => {
                        bitmap.save(filePath, {}, () => {
                            bitmap.clear()
                            this[i] = filePath;
                        })
                    })
                    // #endif
                }
            }
        },
        mounted() {
            // #ifdef H5 || MP-360
            this.document = document.getElementById('rtf' + this._uid);
            // #endif
            // #ifndef H5 || APP-PLUS-NVUE || MP-360
            if (dom) this.document = new dom(this);
            // #endif
            if (search) this.search = args => search(this, args);
            // #ifdef APP-PLUS-NVUE
            this.document = this.$refs.web;
            setTimeout(() => {
                // #endif
                if (this.html) this.setContent(this.html);
                // #ifdef APP-PLUS-NVUE
            }, 30)
            // #endif
        },
        beforeDestroy() {
            // #ifdef H5 || MP-360
            if (this._observer) this._observer.disconnect();
            // #endif
            this.imgList.each(src => {
                // #ifdef APP-PLUS
                if (src && src.includes('_doc')) {
                    plus.io.resolveLocalFileSystemURL(src, entry => {
                        entry.remove();
                    });
                }
                // #endif
                // #ifdef MP-WEIXIN || MP-TOUTIAO
                if (src && src.includes(uni.env.USER_DATA_PATH))
                    fs && fs.unlink({
                        filePath: src
                    })
                // #endif
            })
            clearInterval(this._timer);
        },
        methods: {
            // 设置富文本内容
            setContent(html, append) {
                // #ifdef APP-PLUS-NVUE
                if (!html)
                    return this.height = 1;
                if (append)
                    this.$refs.web.evalJs("var b=document.createElement('div');b.innerHTML='" + html.replace(/'/g, "\\'") +
                        "';document.getElementById('parser').appendChild(b)");
                else {
                    html =
                        '<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><style>html,body{width:100%;height:100%;overflow:hidden}body{margin:0}</style><base href="' +
                        this.domain + '"><div id="parser"' + (this.selectable ? '>' : ' style="user-select:none">') + this._handleHtml(html).replace(/\n/g, '\\n') +
                        '</div><script>"use strict";function e(e){if(window.__dcloud_weex_postMessage||window.__dcloud_weex_){var t={data:[e]};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(t):window.__dcloud_weex_.postMessage(JSON.stringify(t))}}document.body.onclick=function(){e({action:"click"})},' +
                        (this.showWithAnimation ? 'document.body.style.animation="_show .5s",' : '') +
                        'setTimeout(function(){e({action:"load",text:document.body.innerText,height:document.getElementById("parser").scrollHeight})},50);\x3c/script>';
                    if (platform == 'android') html = html.replace(/%/g, '%25');
                    this.$refs.web.evalJs("document.write('" + html.replace(/'/g, "\\'") + "');document.close()");
                }
                this.$refs.web.evalJs(
                    'var t=document.getElementsByTagName("title");t.length&&e({action:"getTitle",title:t[0].innerText});for(var o,n=document.getElementsByTagName("style"),r=1;o=n[r++];)o.innerHTML=o.innerHTML.replace(/body/g,"#parser");for(var a,c=document.getElementsByTagName("img"),s=[],i=0==c.length,d=0,l=0,g=0;a=c[l];l++)parseInt(a.style.width||a.getAttribute("width"))>' +
                    windowWidth + '&&(a.style.height="auto"),a.onload=function(){++d==c.length&&(i=!0)},a.onerror=function(){++d==c.length&&(i=!0),' + (cfg.errorImg ? 'this.src="' + cfg.errorImg + '",' : '') +
                    'e({action:"error",source:"img",target:this})},a.hasAttribute("ignore")||"A"==a.parentElement.nodeName||(a.i=g++,s.push(a.getAttribute("original-src")||a.src||a.getAttribute("data-src")),a.onclick=function(t){t.stopPropagation(),e({action:"preview",img:{i:this.i,src:this.src}})});e({action:"getImgList",imgList:s});for(var u,m=document.getElementsByTagName("a"),f=0;u=m[f];f++)u.onclick=function(m){m.stopPropagation();var t,o=this.getAttribute("href");if("#"==o[0]){var n=document.getElementById(o.substr(1));n&&(t=n.offsetTop)}return e({action:"linkpress",href:o,offset:t}),!1};for(var h,y=document.getElementsByTagName("video"),v=0;h=y[v];v++)h.style.maxWidth="100%",h.onerror=function(){e({action:"error",source:"video",target:this})}' +
                    (this.autopause ? ',h.onplay=function(){for(var e,t=0;e=y[t];t++)e!=this&&e.pause()}' : '') +
                    ';for(var _,p=document.getElementsByTagName("audio"),w=0;_=p[w];w++)_.onerror=function(){e({action:"error",source:"audio",target:this})};' +
                    (this.autoscroll ? 'for(var T,E=document.getElementsByTagName("table"),B=0;T=E[B];B++){var N=document.createElement("div");N.style.overflow="scroll",T.parentNode.replaceChild(N,T),N.appendChild(T)}' : '') +
                    'var x=document.getElementById("parser");clearInterval(window.timer),window.timer=setInterval(function(){i&&clearInterval(window.timer),e({action:"ready",ready:i,height:x.scrollHeight})},350)'
                )
                this.nodes = [1];
                // #endif
                // #ifdef H5 || MP-360
                if (!html) {
                    if (this.rtf && !append) this.rtf.parentNode.removeChild(this.rtf);
                    return;
                }
                var div = document.createElement('div');
                if (!append) {
                    if (this.rtf) this.rtf.parentNode.removeChild(this.rtf);
                    this.rtf = div;
                } else {
                    if (!this.rtf) this.rtf = div;
                    else this.rtf.appendChild(div);
                }
                div.innerHTML = this._handleHtml(html, append);
                for (var styles = this.rtf.getElementsByTagName('style'), i = 0, style; style = styles[i++];) {
                    style.innerHTML = style.innerHTML.replace(/body/g, '#rtf' + this._uid);
                    style.setAttribute('scoped', 'true');
                }
                // 懒加载
                if (!this._observer && this.lazyLoad && IntersectionObserver) {
                    this._observer = new IntersectionObserver(changes => {
                        for (let item, i = 0; item = changes[i++];) {
                            if (item.isIntersecting) {
                                item.target.src = item.target.getAttribute('data-src');
                                item.target.removeAttribute('data-src');
                                this._observer.unobserve(item.target);
                            }
                        }
                    }, {
                        rootMargin: '500px 0px 500px 0px'
                    })
                }
                var _ts = this;
                // 获取标题
                var title = this.rtf.getElementsByTagName('title');
                if (title.length && this.autosetTitle)
                    uni.setNavigationBarTitle({
                        title: title[0].innerText
                    })
                // 填充 domain
                var fill = target => {
                    var src = target.getAttribute('src');
                    if (this.domain && src) {
                        if (src[0] == '/') {
                            if (src[1] == '/')
                                target.src = (this.domain.includes('://') ? this.domain.split('://')[0] : '') + ':' + src;
                            else target.src = this.domain + src;
                        } else if (!src.includes('://') && src.indexOf('data:') != 0) target.src = this.domain + '/' + src;
                    }
                }
                // 图片处理
                this.imgList.length = 0;
                var imgs = this.rtf.getElementsByTagName('img');
                for (let i = 0, j = 0, img; img = imgs[i]; i++) {
                    if (parseInt(img.style.width || img.getAttribute('width')) > windowWidth)
                        img.style.height = 'auto';
                    fill(img);
                    if (!img.hasAttribute('ignore') && img.parentElement.nodeName != 'A') {
                        img.i = j++;
                        _ts.imgList.push(img.getAttribute('original-src') || img.src || img.getAttribute('data-src'));
                        img.onclick = function(e) {
                            e.stopPropagation();
                            var preview = true;
                            this.ignore = () => preview = false;
                            _ts.$emit('imgtap', this);
                            if (preview) {
                                uni.previewImage({
                                    current: this.i,
                                    urls: _ts.imgList
                                });
                            }
                        }
                    }
                    img.onerror = function() {
                        if (cfg.errorImg)
                            _ts.imgList[this.i] = this.src = cfg.errorImg;
                        _ts.$emit('error', {
                            source: 'img',
                            target: this
                        });
                    }
                    if (_ts.lazyLoad && this._observer && img.src && img.i != 0) {
                        img.setAttribute('data-src', img.src);
                        img.removeAttribute('src');
                        this._observer.observe(img);
                    }
                }
                // 链接处理
                var links = this.rtf.getElementsByTagName('a');
                for (var link of links) {
                    link.onclick = function(e) {
                        e.stopPropagation();
                        var jump = true,
                            href = this.getAttribute('href');
                        _ts.$emit('linkpress', {
                            href,
                            ignore: () => jump = false
                        });
                        if (jump && href) {
                            if (href[0] == '#') {
                                if (_ts.useAnchor) {
                                    _ts.navigateTo({
                                        id: href.substr(1)
                                    })
                                }
                            } else if (href.indexOf('http') == 0 || href.indexOf('//') == 0)
                                return true;
                            else
                                uni.navigateTo({
                                    url: href
                                })
                        }
                        return false;
                    }
                }
                // 视频处理
                var videos = this.rtf.getElementsByTagName('video');
                _ts.videoContexts = videos;
                for (let video, i = 0; video = videos[i++];) {
                    fill(video);
                    video.style.maxWidth = '100%';
                    video.onerror = function() {
                        _ts.$emit('error', {
                            source: 'video',
                            target: this
                        });
                    }
                    video.onplay = function() {
                        if (_ts.autopause)
                            for (let item, i = 0; item = _ts.videoContexts[i++];)
                                if (item != this) item.pause();
                    }
                }
                // 音频处理
                var audios = this.rtf.getElementsByTagName('audio');
                for (var audio of audios) {
                    fill(audio);
                    audio.onerror = function() {
                        _ts.$emit('error', {
                            source: 'audio',
                            target: this
                        });
                    }
                }
                // 表格处理
                if (this.autoscroll) {
                    var tables = this.rtf.getElementsByTagName('table');
                    for (var table of tables) {
                        let div = document.createElement('div');
                        div.style.overflow = 'scroll';
                        table.parentNode.replaceChild(div, table);
                        div.appendChild(table);
                    }
                }
                if (!append) this.document.appendChild(this.rtf);
                this.$nextTick(() => {
                    this.nodes = [1];
                    this.$emit('load');
                });
                setTimeout(() => this.showAm = '', 500);
                // #endif
                // #ifndef APP-PLUS-NVUE
                // #ifndef H5 || MP-360
                var nodes;
                if (!html) return this.nodes = [];
                var parser = new Parser(html, this);
                // 缓存读取
                if (this.useCache) {
                    var hashVal = hash(html);
                    if (cache[hashVal])
                        nodes = cache[hashVal];
                    else {
                        nodes = parser.parse();
                        cache[hashVal] = nodes;
                    }
                } else nodes = parser.parse();
                this.$emit('parse', nodes);
                if (append) this.nodes = this.nodes.concat(nodes);
                else this.nodes = nodes;
                if (nodes.length && nodes.title && this.autosetTitle)
                    uni.setNavigationBarTitle({
                        title: nodes.title
                    })
                if (this.imgList) this.imgList.length = 0;
                this.videoContexts = [];
                this.$nextTick(() => {
                    (function f(cs) {
                        for (var i = cs.length; i--;) {
                            if (cs[i].top) {
                                cs[i].controls = [];
                                cs[i].init();
                                f(cs[i].$children);
                            }
                        }
                    })(this.$children)
                    this.$emit('load');
                })
                // #endif
                var height;
                clearInterval(this._timer);
                this._timer = setInterval(() => {
                    // #ifdef H5 || MP-360
                    this.rect = this.rtf.getBoundingClientRect();
                    // #endif
                    // #ifndef H5 || MP-360
                    uni.createSelectorQuery().in(this)
                        .select('#_top').boundingClientRect().exec(res => {
                            if (!res) return;
                            this.rect = res[0];
                            // #endif
                            if (this.rect.height == height) {
                                this.$emit('ready', this.rect)
                                clearInterval(this._timer);
                            }
                            height = this.rect.height;
                            // #ifndef H5 || MP-360
                        });
                    // #endif
                }, 350);
                if (this.showWithAnimation && !append) this.showAm = 'animation:_show .5s';
                // #endif
            },
            // 获取文本内容
            getText(ns = this.nodes) {
                var txt = '';
                // #ifdef APP-PLUS-NVUE
                txt = this._text;
                // #endif
                // #ifdef H5 || MP-360
                txt = this.rtf.innerText;
                // #endif
                // #ifndef H5 || APP-PLUS-NVUE || MP-360
                for (var i = 0, n; n = ns[i++];) {
                    if (n.type == 'text') txt += n.text.replace(/&nbsp;/g, '\u00A0').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
                        .replace(/&amp;/g, '&');
                    else if (n.type == 'br') txt += '\n';
                    else {
                        // 块级标签前后加换行
                        var block = n.name == 'p' || n.name == 'div' || n.name == 'tr' || n.name == 'li' || (n.name[0] == 'h' && n.name[1] >
                            '0' && n.name[1] < '7');
                        if (block && txt && txt[txt.length - 1] != '\n') txt += '\n';
                        if (n.children) txt += this.getText(n.children);
                        if (block && txt[txt.length - 1] != '\n') txt += '\n';
                        else if (n.name == 'td' || n.name == 'th') txt += '\t';
                    }
                }
                // #endif
                return txt;
            },
            // 锚点跳转
            in (obj) {
                if (obj.page && obj.selector && obj.scrollTop) this._in = obj;
            },
            navigateTo(obj) {
                if (!this.useAnchor) return obj.fail && obj.fail('Anchor is disabled');
                // #ifdef APP-PLUS-NVUE
                if (!obj.id)
                    weexDom.scrollToElement(this.$refs.web);
                else
                    this.$refs.web.evalJs('var pos=document.getElementById("' + obj.id +
                        '");if(pos)post({action:"linkpress",href:"#",offset:pos.offsetTop+' + (obj.offset || 0) + '})');
                obj.success && obj.success();
                // #endif
                // #ifndef APP-PLUS-NVUE
                var d = ' ';
                // #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
                d = '>>>';
                // #endif
                var selector = uni.createSelectorQuery().in(this._in ? this._in.page : this).select((this._in ? this._in.selector :
                    '#_top') + (obj.id ? `${d}#${obj.id},${this._in?this._in.selector:'#_top'}${d}.${obj.id}` : '')).boundingClientRect();
                if (this._in) selector.select(this._in.selector).scrollOffset().select(this._in.selector).boundingClientRect();
                else selector.selectViewport().scrollOffset();
                selector.exec(res => {
                    if (!res[0]) return obj.fail && obj.fail('Label not found')
                    var scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + (obj.offset || 0);
                    if (this._in) this._in.page[this._in.scrollTop] = scrollTop;
                    else uni.pageScrollTo({
                        scrollTop,
                        duration: 300
                    })
                    obj.success && obj.success();
                })
                // #endif
            },
            // 获取视频对象
            getVideoContext(id) {
                // #ifndef APP-PLUS-NVUE
                if (!id) return this.videoContexts;
                else
                    for (var i = this.videoContexts.length; i--;)
                        if (this.videoContexts[i].id == id) return this.videoContexts[i];
                // #endif
            },
            // #ifdef H5 || APP-PLUS-NVUE || MP-360
            _handleHtml(html, append) {
                if (!append) {
                    // 处理 tag-style 和 userAgentStyles
                    var style = '<style>@keyframes _show{0%{opacity:0}100%{opacity:1}}img{max-width:100%}';
                    for (var item in cfg.userAgentStyles)
                        style += `${item}{${cfg.userAgentStyles[item]}}`;
                    for (item in this.tagStyle)
                        style += `${item}{${this.tagStyle[item]}}`;
                    style += '</style>';
                    html = style + html;
                }
                // 处理 rpx
                if (html.includes('rpx'))
                    html = html.replace(/[0-9.]+\s*rpx/g, $ => (parseFloat($) * windowWidth / 750) + 'px');
                return html;
            },
            // #endif
            // #ifdef APP-PLUS-NVUE
            _message(e) {
                // 接收 web-view 消息
                var d = e.detail.data[0];
                switch (d.action) {
                    case 'load':
                        this.$emit('load');
                        this.height = d.height;
                        this._text = d.text;
                        break;
                    case 'getTitle':
                        if (this.autosetTitle)
                            uni.setNavigationBarTitle({
                                title: d.title
                            })
                        break;
                    case 'getImgList':
                        this.imgList.length = 0;
                        for (var i = d.imgList.length; i--;)
                            this.imgList.setItem(i, d.imgList[i]);
                        break;
                    case 'preview':
                        var preview = true;
                        d.img.ignore = () => preview = false;
                        this.$emit('imgtap', d.img);
                        if (preview)
                            uni.previewImage({
                                current: d.img.i,
                                urls: this.imgList
                            })
                        break;
                    case 'linkpress':
                        var jump = true,
                            href = d.href;
                        this.$emit('linkpress', {
                            href,
                            ignore: () => jump = false
                        })
                        if (jump && href) {
                            if (href[0] == '#') {
                                if (this.useAnchor)
                                    weexDom.scrollToElement(this.$refs.web, {
                                        offset: d.offset
                                    })
                            } else if (href.includes('://'))
                                plus.runtime.openWeb(href);
                            else
                                uni.navigateTo({
                                    url: href
                                })
                        }
                        break;
                    case 'error':
                        if (d.source == 'img' && cfg.errorImg)
                            this.imgList.setItem(d.target.i, cfg.errorImg);
                        this.$emit('error', {
                            source: d.source,
                            target: d.target
                        })
                        break;
                    case 'ready':
                        this.height = d.height;
                        if (d.ready) uni.createSelectorQuery().in(this).select('#_top').boundingClientRect().exec(res => {
                            this.rect = res[0];
                            this.$emit('ready', res[0]);
                        })
                        break;
                    case 'click':
                        this.$emit('click');
                        this.$emit('tap');
                }
            },
            // #endif
        }
    }
</script>
<style>
    @keyframes _show {
        0% {
            opacity: 0;
        }
        100% {
            opacity: 1;
        }
    }
    /* #ifdef MP-WEIXIN */
    :host {
        display: block;
        overflow: auto;
        -webkit-overflow-scrolling: touch;
    }
    /* #endif */
</style>
uview-ui/components/u-picker/u-picker.vue
New file
@@ -0,0 +1,676 @@
<template>
    <u-popup :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto" :safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex">
        <view class="u-datetime-picker">
            <view class="u-picker-header" @touchmove.stop.prevent="">
                <view class="u-btn-picker u-btn-picker--tips"
                    :style="{ color: cancelColor }"
                    hover-class="u-opacity"
                    :hover-stay-time="150"
                    @tap="getResult('cancel')"
                >{{cancelText}}</view>
                <view class="u-picker__title">{{ title }}</view>
                <view
                    class="u-btn-picker u-btn-picker--primary"
                    :style="{ color: moving ? cancelColor : confirmColor }"
                    hover-class="u-opacity"
                    :hover-stay-time="150"
                    @touchmove.stop=""
                    @tap.stop="getResult('confirm')"
                >
                    {{confirmText}}
                </view>
            </view>
            <view class="u-picker-body">
                <picker-view v-if="mode == 'region'" :value="valueArr" @change="change" class="u-picker-view" @pickstart="pickstart" @pickend="pickend">
                    <picker-view-column v-if="!reset && params.province">
                        <view class="u-column-item" v-for="(item, index) in provinces" :key="index">
                            <view class="u-line-1">{{ item.label }}</view>
                        </view>
                    </picker-view-column>
                    <picker-view-column v-if="!reset && params.city">
                        <view class="u-column-item" v-for="(item, index) in citys" :key="index">
                            <view class="u-line-1">{{ item.label }}</view>
                        </view>
                    </picker-view-column>
                    <picker-view-column v-if="!reset && params.area">
                        <view class="u-column-item" v-for="(item, index) in areas" :key="index">
                            <view class="u-line-1">{{ item.label }}</view>
                        </view>
                    </picker-view-column>
                </picker-view>
                <picker-view v-else-if="mode == 'time'" :value="valueArr" @change="change" class="u-picker-view" @pickstart="pickstart" @pickend="pickend">
                    <picker-view-column v-if="!reset && params.year">
                        <view class="u-column-item" v-for="(item, index) in years" :key="index">
                            {{ item }}
                            <text class="u-text" v-if="showTimeTag">年</text>
                        </view>
                    </picker-view-column>
                    <picker-view-column v-if="!reset && params.month">
                        <view class="u-column-item" v-for="(item, index) in months" :key="index">
                            {{ formatNumber(item) }}
                            <text class="u-text" v-if="showTimeTag">月</text>
                        </view>
                    </picker-view-column>
                    <picker-view-column v-if="!reset && params.day">
                        <view class="u-column-item" v-for="(item, index) in days" :key="index">
                            {{ formatNumber(item) }}
                            <text class="u-text" v-if="showTimeTag">日</text>
                        </view>
                    </picker-view-column>
                    <picker-view-column v-if="!reset && params.hour">
                        <view class="u-column-item" v-for="(item, index) in hours" :key="index">
                            {{ formatNumber(item) }}
                            <text class="u-text" v-if="showTimeTag">时</text>
                        </view>
                    </picker-view-column>
                    <picker-view-column v-if="!reset && params.minute">
                        <view class="u-column-item" v-for="(item, index) in minutes" :key="index">
                            {{ formatNumber(item) }}
                            <text class="u-text" v-if="showTimeTag">分</text>
                        </view>
                    </picker-view-column>
                    <picker-view-column v-if="!reset && params.second">
                        <view class="u-column-item" v-for="(item, index) in seconds" :key="index">
                            {{ formatNumber(item) }}
                            <text class="u-text" v-if="showTimeTag">秒</text>
                        </view>
                    </picker-view-column>
                </picker-view>
                <picker-view v-else-if="mode == 'selector'" :value="valueArr" @change="change" class="u-picker-view" @pickstart="pickstart" @pickend="pickend">
                    <picker-view-column v-if="!reset">
                        <view class="u-column-item" v-for="(item, index) in range" :key="index">
                            <view class="u-line-1">{{ getItemValue(item, 'selector') }}</view>
                        </view>
                    </picker-view-column>
                </picker-view>
                <picker-view v-else-if="mode == 'multiSelector'" :value="valueArr" @change="change" class="u-picker-view" @pickstart="pickstart" @pickend="pickend">
                    <picker-view-column v-if="!reset" v-for="(item, index) in range" :key="index">
                        <view class="u-column-item" v-for="(item1, index1) in item" :key="index1">
                            <view class="u-line-1">{{ getItemValue(item1, 'multiSelector') }}</view>
                        </view>
                    </picker-view-column>
                </picker-view>
            </view>
        </view>
    </u-popup>
</template>
<script>
import provinces from '../../libs/util/province.js';
import citys from '../../libs/util/city.js';
import areas from '../../libs/util/area.js';
/**
 * picker picker弹出选择器
 * @description 此选择器有两种弹出模式:一是时间模式,可以配置年,日,月,时,分,秒参数 二是地区模式,可以配置省,市,区参数
 * @tutorial https://www.uviewui.com/components/picker.html
 * @property {Object} params 需要显示的参数,见官网说明
 * @property {String} mode 模式选择,region-地区类型,time-时间类型(默认time)
 * @property {String Number} start-year 可选的开始年份,mode=time时有效(默认1950)
 * @property {String Number} end-year 可选的结束年份,mode=time时有效(默认2050)
 * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
 * @property {Boolean} show-time-tag 时间模式时,是否显示后面的年月日中文提示
 * @property {String} cancel-color 取消按钮的颜色(默认#606266)
 * @property {String} confirm-color 确认按钮的颜色(默认#2979ff)
 * @property {String} default-time 默认选中的时间,mode=time时有效
 * @property {String} confirm-text 确认按钮的文字
 * @property {String} cancel-text 取消按钮的文字
 * @property {String} default-region 默认选中的地区,中文形式,mode=region时有效
 * @property {String} default-code 默认选中的地区,编号形式,mode=region时有效
 * @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭Picker(默认true)
 * @property {String Number} z-index 弹出时的z-index值(默认1075)
 * @property {Array} default-selector 数组形式,其中每一项表示选择了range对应项中的第几个
 * @property {Array} range 自定义选择的数据,mode=selector或mode=multiSelector时有效
 * @property {String} range-key 当range参数的元素为对象时,指定Object中的哪个key的值作为选择器显示内容
 * @event {Function} confirm 点击确定按钮,返回当前选择的值
 * @event {Function} cancel 点击取消按钮,返回当前选择的值
 * @example <u-picker v-model="show" mode="time"></u-picker>
 */
export default {
    name: 'u-picker',
    props: {
        // picker中需要显示的参数
        params: {
            type: Object,
            default() {
                return {
                    year: true,
                    month: true,
                    day: true,
                    hour: false,
                    minute: false,
                    second: false,
                    province: true,
                    city: true,
                    area: true,
                    timestamp: true,
                };
            }
        },
        // 当mode=selector或者mode=multiSelector时,提供的数组
        range: {
            type: Array,
            default() {
                return [];
            }
        },
        // 当mode=selector或者mode=multiSelector时,提供的默认选中的下标
        defaultSelector: {
            type: Array,
            default() {
                return [0];
            }
        },
        // 当 range 是一个 Array<Object> 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容
        rangeKey: {
            type: String,
            default: ''
        },
        // 模式选择,region-地区类型,time-时间类型,selector-单列模式,multiSelector-多列模式
        mode: {
            type: String,
            default: 'time'
        },
        // 年份开始时间
        startYear: {
            type: [String, Number],
            default: 1950
        },
        // 年份结束时间
        endYear: {
            type: [String, Number],
            default: 2050
        },
        // "取消"按钮的颜色
        cancelColor: {
            type: String,
            default: '#606266'
        },
        // "确定"按钮的颜色
        confirmColor: {
            type: String,
            default: '#2979ff'
        },
        // 默认显示的时间,2025-07-02 || 2025-07-02 13:01:00 || 2025/07/02
        defaultTime: {
            type: String,
            default: ''
        },
        // 默认显示的地区,可传类似["河北省", "秦皇岛市", "北戴河区"]
        defaultRegion: {
            type: Array,
            default() {
                return [];
            }
        },
        // 时间模式时,是否显示后面的年月日中文提示
        showTimeTag: {
            type: Boolean,
            default: true
        },
        // 默认显示地区的编码,defaultRegion和areaCode同时存在,areaCode优先,可传类似["13", "1303", "130304"]
        areaCode: {
            type: Array,
            default() {
                return [];
            }
        },
        safeAreaInsetBottom: {
            type: Boolean,
            default: false
        },
        // 是否允许通过点击遮罩关闭Picker
        maskCloseAble: {
            type: Boolean,
            default: true
        },
        // 通过双向绑定控制组件的弹出与收起
        value: {
            type: Boolean,
            default: false
        },
        // 弹出的z-index值
        zIndex: {
            type: [String, Number],
            default: 0
        },
        // 顶部标题
        title: {
            type: String,
            default: ''
        },
        // 取消按钮的文字
        cancelText: {
            type: String,
            default: '取消'
        },
        // 确认按钮的文字
        confirmText: {
            type: String,
            default: '确认'
        }
    },
    data() {
        return {
            years: [],
            months: [],
            days: [],
            hours: [],
            minutes: [],
            seconds: [],
            year: 0,
            month: 0,
            day: 0,
            hour: 0,
            minute: 0,
            second: 0,
            reset: false,
            startDate: '',
            endDate: '',
            valueArr: [],
            provinces: provinces,
            citys: citys[0],
            areas: areas[0][0],
            province: 0,
            city: 0,
            area: 0,
            moving: false // 列是否还在滑动中,微信小程序如果在滑动中就点确定,结果可能不准确
        };
    },
    mounted() {
        this.init();
    },
    computed: {
        propsChange() {
            // 引用这几个变量,是为了监听其变化
            return `${this.mode}-${this.defaultTime}-${this.startYear}-${this.endYear}-${this.defaultRegion}-${this.areaCode}`;
        },
        regionChange() {
            // 引用这几个变量,是为了监听其变化
            return `${this.province}-${this.city}`;
        },
        yearAndMonth() {
            return `${this.year}-${this.month}`;
        },
        uZIndex() {
            // 如果用户有传递z-index值,优先使用
            return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
        }
    },
    watch: {
        propsChange() {
            this.reset = true;
            setTimeout(() => this.init(), 10);
        },
        // 如果地区发生变化,为了让picker联动起来,必须重置this.citys和this.areas
        regionChange(val) {
            this.citys = citys[this.province];
            this.areas = areas[this.province][this.city];
        },
        // watch监听月份的变化,实时变更日的天数,因为不同月份,天数不一样
        // 一个月可能有30,31天,甚至闰年2月的29天,平年2月28天
        yearAndMonth(val) {
            if (this.params.year) this.setDays();
        },
        // 微信和QQ小程序由于一些奇怪的原因(故同时对所有平台均初始化一遍),需要重新初始化才能显示正确的值
        value(n) {
            if (n) {
                this.reset = true;
                setTimeout(() => this.init(), 10);
            }
        }
    },
    methods: {
        // 标识滑动开始,只有微信小程序才有这样的事件
        pickstart() {
            // #ifdef MP-WEIXIN
            this.moving = true;
            // #endif
        },
        // 标识滑动结束
        pickend() {
            // #ifdef MP-WEIXIN
            this.moving = false;
            // #endif
        },
        // 对单列和多列形式的判断是否有传入变量的情况
        getItemValue(item, mode) {
            // 目前(2020-05-25)uni-app对微信小程序编译有错误,导致v-if为false中的内容也执行,错误导致
            // 单列模式或者多列模式中的getItemValue同时被执行,故在这里再加一层判断
            if (this.mode == mode) {
                return typeof item == 'object' ? item[this.rangeKey] : item;
            }
        },
        // 小于10前面补0,用于月份,日期,时分秒等
        formatNumber(num) {
            return +num < 10 ? '0' + num : String(num);
        },
        // 生成递进的数组
        generateArray: function(start, end) {
            // 转为数值格式,否则用户给end-year等传递字符串值时,下面的end+1会导致字符串拼接,而不是相加
            start = Number(start);
            end = Number(end);
            end = end > start ? end : start;
            // 生成数组,获取其中的索引,并剪出来
            return [...Array(end + 1).keys()].slice(start);
        },
        getIndex: function(arr, val) {
            let index = arr.indexOf(val);
            // 如果index为-1(即找不到index值),~(-1)=-(-1)-1=0,导致条件不成立
            return ~index ? index : 0;
        },
        //日期时间处理
        initTimeValue() {
            // 格式化时间,在IE浏览器(uni不存在此情况),无法识别日期间的"-"间隔符号
            let fdate = this.defaultTime.replace(/\-/g, '/');
            fdate = fdate && fdate.indexOf('/') == -1 ? `2020/01/01 ${fdate}` : fdate;
            let time = null;
            if (fdate) time = new Date(fdate);
            else time = new Date();
            // 获取年日月时分秒
            this.year = time.getFullYear();
            this.month = Number(time.getMonth()) + 1;
            this.day = time.getDate();
            this.hour = time.getHours();
            this.minute = time.getMinutes();
            this.second = time.getSeconds();
        },
        init() {
            this.valueArr = [];
            this.reset = false;
            if (this.mode == 'time') {
                this.initTimeValue();
                if (this.params.year) {
                    this.valueArr.push(0);
                    this.setYears();
                }
                if (this.params.month) {
                    this.valueArr.push(0);
                    this.setMonths();
                }
                if (this.params.day) {
                    this.valueArr.push(0);
                    this.setDays();
                }
                if (this.params.hour) {
                    this.valueArr.push(0);
                    this.setHours();
                }
                if (this.params.minute) {
                    this.valueArr.push(0);
                    this.setMinutes();
                }
                if (this.params.second) {
                    this.valueArr.push(0);
                    this.setSeconds();
                }
            } else if (this.mode == 'region') {
                if (this.params.province) {
                    this.valueArr.push(0);
                    this.setProvinces();
                }
                if (this.params.city) {
                    this.valueArr.push(0);
                    this.setCitys();
                }
                if (this.params.area) {
                    this.valueArr.push(0);
                    this.setAreas();
                }
            } else if (this.mode == 'selector') {
                this.valueArr = this.defaultSelector;
            } else if (this.mode == 'multiSelector') {
                this.valueArr = this.defaultSelector;
                this.multiSelectorValue = this.defaultSelector;
            }
            this.$forceUpdate();
        },
        // 设置picker的某一列值
        setYears() {
            // 获取年份集合
            this.years = this.generateArray(this.startYear, this.endYear);
            // 设置this.valueArr某一项的值,是为了让picker预选中某一个值
            this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.years, this.year));
        },
        setMonths() {
            this.months = this.generateArray(1, 12);
            this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.months, this.month));
        },
        setDays() {
            let totalDays = new Date(this.year, this.month, 0).getDate();
            this.days = this.generateArray(1, totalDays);
            let index = 0;
            // 这里不能使用类似setMonths()中的this.valueArr.splice(this.valueArr.length - 1, xxx)做法
            // 因为this.month和this.year变化时,会触发watch中的this.setDays(),导致this.valueArr.length计算有误
            if (this.params.year && this.params.month) index = 2;
            else if (this.params.month) index = 1;
            else if (this.params.year) index = 1;
            else index = 0;
            // 当月份变化时,会导致日期的天数也会变化,如果原来选的天数大于变化后的天数,则重置为变化后的最大值
            // 比如原来选中3月31日,调整为2月后,日期变为最大29,这时如果day值继续为31显然不合理,于是将其置为29(picker-column从1开始)
            if(this.day > this.days.length) this.day = this.days.length;
            this.valueArr.splice(index, 1, this.getIndex(this.days, this.day));
        },
        setHours() {
            this.hours = this.generateArray(0, 23);
            this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.hours, this.hour));
        },
        setMinutes() {
            this.minutes = this.generateArray(0, 59);
            this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.minutes, this.minute));
        },
        setSeconds() {
            this.seconds = this.generateArray(0, 59);
            this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.seconds, this.second));
        },
        setProvinces() {
            // 判断是否需要province参数
            if (!this.params.province) return;
            let tmp = '';
            let useCode = false;
            // 如果同时配置了defaultRegion和areaCode,优先使用areaCode参数
            if (this.areaCode.length) {
                tmp = this.areaCode[0];
                useCode = true;
            } else if (this.defaultRegion.length) tmp = this.defaultRegion[0];
            else tmp = 0;
            // 历遍省份数组匹配
            provinces.map((v, k) => {
                if (useCode ? v.value == tmp : v.label == tmp) {
                    tmp = k;
                }
            });
            this.province = tmp;
            this.provinces = provinces;
            // 设置默认省份的值
            this.valueArr.splice(0, 1, this.province);
        },
        setCitys() {
            if (!this.params.city) return;
            let tmp = '';
            let useCode = false;
            if (this.areaCode.length) {
                tmp = this.areaCode[1];
                useCode = true;
            } else if (this.defaultRegion.length) tmp = this.defaultRegion[1];
            else tmp = 0;
            citys[this.province].map((v, k) => {
                if (useCode ? v.value == tmp : v.label == tmp) {
                    tmp = k;
                }
            });
            this.city = tmp;
            this.citys = citys[this.province];
            this.valueArr.splice(1, 1, this.city);
        },
        setAreas() {
            if (!this.params.area) return;
            let tmp = '';
            let useCode = false;
            if (this.areaCode.length) {
                tmp = this.areaCode[2];
                useCode = true;
            } else if (this.defaultRegion.length) tmp = this.defaultRegion[2];
            else tmp = 0;
            areas[this.province][this.city].map((v, k) => {
                if (useCode ? v.value == tmp : v.label == tmp) {
                    tmp = k;
                }
            });
            this.area = tmp;
            this.areas = areas[this.province][this.city];
            this.valueArr.splice(2, 1, this.area);
        },
        close() {
            this.$emit('input', false);
        },
        // 用户更改picker的列选项
        change(e) {
            this.valueArr = e.detail.value;
            let i = 0;
            if (this.mode == 'time') {
                // 这里使用i++,是因为this.valueArr数组的长度是不确定长度的,它根据this.params的值来配置长度
                // 进入if规则,i会加1,保证了能获取准确的值
                if (this.params.year) this.year = this.years[this.valueArr[i++]];
                if (this.params.month) this.month = this.months[this.valueArr[i++]];
                if (this.params.day) this.day = this.days[this.valueArr[i++]];
                if (this.params.hour) this.hour = this.hours[this.valueArr[i++]];
                if (this.params.minute) this.minute = this.minutes[this.valueArr[i++]];
                if (this.params.second) this.second = this.seconds[this.valueArr[i++]];
            } else if (this.mode == 'region') {
                if (this.params.province) this.province = this.valueArr[i++];
                if (this.params.city) this.city = this.valueArr[i++];
                if (this.params.area) this.area = this.valueArr[i++];
            } else if (this.mode == 'multiSelector') {
                let index = null;
                // 对比前后两个数组,寻找变更的是哪一列,如果某一个元素不同,即可判定该列发生了变化
                this.defaultSelector.map((val, idx) => {
                    if (val != e.detail.value[idx]) index = idx;
                });
                // 为了让用户对多列变化时,对动态设置其他列的变更
                if (index != null) {
                    this.$emit('columnchange', {
                        column: index,
                        index: e.detail.value[index]
                    });
                }
            }
        },
        // 用户点击确定按钮
        getResult(event = null) {
            // #ifdef MP-WEIXIN
            if (this.moving) return;
            // #endif
            let result = {};
            // 只返回用户在this.params中配置了为true的字段
            if (this.mode == 'time') {
                if (this.params.year) result.year = this.formatNumber(this.year || 0);
                if (this.params.month) result.month = this.formatNumber(this.month || 0);
                if (this.params.day) result.day = this.formatNumber(this.day || 0);
                if (this.params.hour) result.hour = this.formatNumber(this.hour || 0);
                if (this.params.minute) result.minute = this.formatNumber(this.minute || 0);
                if (this.params.second) result.second = this.formatNumber(this.second || 0);
                if (this.params.timestamp) result.timestamp = this.getTimestamp();
            } else if (this.mode == 'region') {
                if (this.params.province) result.province = provinces[this.province];
                if (this.params.city) result.city = citys[this.province][this.city];
                if (this.params.area) result.area = areas[this.province][this.city][this.area];
            } else if (this.mode == 'selector') {
                result = this.valueArr;
            } else if (this.mode == 'multiSelector') {
                result = this.valueArr;
            }
            if (event) this.$emit(event, result);
            this.close();
        },
        // 获取时间戳
        getTimestamp() {
            // yyyy-mm-dd为安卓写法,不支持iOS,需要使用"/"分隔,才能二者兼容
            let time = this.year + '/' + this.month + '/' + this.day + ' ' + this.hour + ':' + this.minute + ':' + this.second;
            return new Date(time).getTime() / 1000;
        }
    }
};
</script>
<style lang="scss" scoped>
@import '../../libs/css/style.components.scss';
.u-datetime-picker {
    position: relative;
    z-index: 999;
}
.u-picker-view {
    height: 100%;
    box-sizing: border-box;
}
.u-picker-header {
    width: 100%;
    height: 90rpx;
    padding: 0 40rpx;
    @include vue-flex;
    justify-content: space-between;
    align-items: center;
    box-sizing: border-box;
    font-size: 30rpx;
    background: #fff;
    position: relative;
}
.u-picker-header::after {
    content: '';
    position: absolute;
    border-bottom: 1rpx solid #eaeef1;
    -webkit-transform: scaleY(0.5);
    transform: scaleY(0.5);
    bottom: 0;
    right: 0;
    left: 0;
}
.u-picker__title {
    color: $u-content-color;
}
.u-picker-body {
    width: 100%;
    height: 500rpx;
    overflow: hidden;
    background-color: #fff;
}
.u-column-item {
    @include vue-flex;
    align-items: center;
    justify-content: center;
    font-size: 32rpx;
    color: $u-main-color;
    padding: 0 8rpx;
}
.u-text {
    font-size: 24rpx;
    padding-left: 8rpx;
}
.u-btn-picker {
    padding: 16rpx;
    box-sizing: border-box;
    text-align: center;
    text-decoration: none;
}
.u-opacity {
    opacity: 0.5;
}
.u-btn-picker--primary {
    color: $u-type-primary;
}
.u-btn-picker--tips {
    color: $u-tips-color;
}
</style>
uview-ui/components/u-popup/u-popup.vue
New file
@@ -0,0 +1,456 @@
<template>
    <view v-if="visibleSync" :style="[customStyle, {
        zIndex: uZindex - 1
    }]" class="u-drawer" hover-stop-propagation>
        <u-mask :duration="duration" :custom-style="maskCustomStyle" :maskClickAble="maskCloseAble" :z-index="uZindex - 2" :show="showDrawer && mask" @click="maskClick"></u-mask>
        <view
            class="u-drawer-content"
            @tap="modeCenterClose(mode)"
            :class="[
                safeAreaInsetBottom ? 'safe-area-inset-bottom' : '',
                'u-drawer-' + mode,
                showDrawer ? 'u-drawer-content-visible' : '',
                zoom && mode == 'center' ? 'u-animation-zoom' : ''
            ]"
            @touchmove.stop.prevent
            @tap.stop.prevent
            :style="[style]"
        >
            <view class="u-mode-center-box" @tap.stop.prevent @touchmove.stop.prevent v-if="mode == 'center'" :style="[centerStyle]">
                <u-icon
                    @click="close"
                    v-if="closeable"
                    class="u-close"
                    :class="['u-close--' + closeIconPos]"
                    :name="closeIcon"
                    :color="closeIconColor"
                    :size="closeIconSize"
                ></u-icon>
                <scroll-view class="u-drawer__scroll-view" scroll-y="true">
                    <slot />
                </scroll-view>
            </view>
            <scroll-view class="u-drawer__scroll-view" scroll-y="true" v-else>
                <slot />
            </scroll-view>
            <view @tap="close" class="u-close" :class="['u-close--' + closeIconPos]">
                <u-icon
                    v-if="mode != 'center' && closeable"
                    :name="closeIcon"
                    :color="closeIconColor"
                    :size="closeIconSize"
                ></u-icon>
            </view>
        </view>
    </view>
</template>
<script>
/**
 * popup 弹窗
 * @description 弹出层容器,用于展示弹窗、信息提示等内容,支持上、下、左、右和中部弹出。组件只提供容器,内部内容由用户自定义
 * @tutorial https://www.uviewui.com/components/popup.html
 * @property {String} mode 弹出方向(默认left)
 * @property {Boolean} mask 是否显示遮罩(默认true)
 * @property {Stringr | Number} length mode=left | 见官网说明(默认auto)
 * @property {Boolean} zoom 是否开启缩放动画,只在mode为center时有效(默认true)
 * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
 * @property {Boolean} mask-close-able 点击遮罩是否可以关闭弹出层(默认true)
 * @property {Object} custom-style 用户自定义样式
 * @property {Stringr | Number} negative-top 中部弹出时,往上偏移的值
 * @property {Numberr | String} border-radius 弹窗圆角值(默认0)
 * @property {Numberr | String} z-index 弹出内容的z-index值(默认1075)
 * @property {Boolean} closeable 是否显示关闭图标(默认false)
 * @property {String} close-icon 关闭图标的名称,只能uView的内置图标
 * @property {String} close-icon-pos 自定义关闭图标位置(默认top-right)
 * @property {String} close-icon-color 关闭图标的颜色(默认#909399)
 * @property {Number | String} close-icon-size 关闭图标的大小,单位rpx(默认30)
 * @event {Function} open 弹出层打开
 * @event {Function} close 弹出层收起
 * @example <u-popup v-model="show"><view>出淤泥而不染,濯清涟而不妖</view></u-popup>
 */
export default {
    name: 'u-popup',
    props: {
        /**
         * 显示状态
         */
        show: {
            type: Boolean,
            default: false
        },
        /**
         * 弹出方向,left|right|top|bottom|center
         */
        mode: {
            type: String,
            default: 'left'
        },
        /**
         * 是否显示遮罩
         */
        mask: {
            type: Boolean,
            default: true
        },
        // 抽屉的宽度(mode=left|right),或者高度(mode=top|bottom),单位rpx,或者"auto"
        // 或者百分比"50%",表示由内容撑开高度或者宽度
        length: {
            type: [Number, String],
            default: 'auto'
        },
        // 是否开启缩放动画,只在mode=center时有效
        zoom: {
            type: Boolean,
            default: true
        },
        // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
        safeAreaInsetBottom: {
            type: Boolean,
            default: false
        },
        // 是否可以通过点击遮罩进行关闭
        maskCloseAble: {
            type: Boolean,
            default: true
        },
        // 用户自定义样式
        customStyle: {
            type: Object,
            default() {
                return {};
            }
        },
        value: {
            type: Boolean,
            default: false
        },
        // 此为内部参数,不在文档对外使用,为了解决Picker和keyboard等融合了弹窗的组件
        // 对v-model双向绑定多层调用造成报错不能修改props值的问题
        popup: {
            type: Boolean,
            default: true
        },
        // 显示显示弹窗的圆角,单位rpx
        borderRadius: {
            type: [Number, String],
            default: 0
        },
        zIndex: {
            type: [Number, String],
            default: ''
        },
        // 是否显示关闭图标
        closeable: {
            type: Boolean,
            default: false
        },
        // 关闭图标的名称,只能uView的内置图标
        closeIcon: {
            type: String,
            default: 'close'
        },
        // 自定义关闭图标位置,top-left为左上角,top-right为右上角,bottom-left为左下角,bottom-right为右下角
        closeIconPos: {
            type: String,
            default: 'top-right'
        },
        // 关闭图标的颜色
        closeIconColor: {
            type: String,
            default: '#909399'
        },
        // 关闭图标的大小,单位rpx
        closeIconSize: {
            type: [String, Number],
            default: '30'
        },
        // 宽度,只对左,右,中部弹出时起作用,单位rpx,或者"auto"
        // 或者百分比"50%",表示由内容撑开高度或者宽度,优先级高于length参数
        width: {
            type: String,
            default: ''
        },
        // 高度,只对上,下,中部弹出时起作用,单位rpx,或者"auto"
        // 或者百分比"50%",表示由内容撑开高度或者宽度,优先级高于length参数
        height: {
            type: String,
            default: ''
        },
        // 给一个负的margin-top,往上偏移,避免和键盘重合的情况,仅在mode=center时有效
        negativeTop: {
            type: [String, Number],
            default: 0
        },
        // 遮罩的样式,一般用于修改遮罩的透明度
        maskCustomStyle: {
            type: Object,
            default() {
                return {}
            }
        },
        // 遮罩打开或收起的动画过渡时间,单位ms
        duration: {
            type: [String, Number],
            default: 250
        }
    },
    data() {
        return {
            visibleSync: false,
            showDrawer: false,
            timer: null,
            closeFromInner: false, // value的值改变,是发生在内部还是外部
        };
    },
    computed: {
        // 根据mode的位置,设定其弹窗的宽度(mode = left|right),或者高度(mode = top|bottom)
        style() {
            let style = {};
            // 如果是左边或者上边弹出时,需要给translate设置为负值,用于隐藏
            if (this.mode == 'left' || this.mode == 'right') {
                style = {
                    width: this.width ? this.getUnitValue(this.width) : this.getUnitValue(this.length),
                    height: '100%',
                    transform: `translate3D(${this.mode == 'left' ? '-100%' : '100%'},0px,0px)`
                };
            } else if (this.mode == 'top' || this.mode == 'bottom') {
                style = {
                    width: '100%',
                    height: this.height ? this.getUnitValue(this.height) : this.getUnitValue(this.length),
                    transform: `translate3D(0px,${this.mode == 'top' ? '-100%' : '100%'},0px)`
                };
            }
            style.zIndex = this.uZindex;
            // 如果用户设置了borderRadius值,添加弹窗的圆角
            if (this.borderRadius) {
                switch (this.mode) {
                    case 'left':
                        style.borderRadius = `0 ${this.borderRadius}rpx ${this.borderRadius}rpx 0`;
                        break;
                    case 'top':
                        style.borderRadius = `0 0 ${this.borderRadius}rpx ${this.borderRadius}rpx`;
                        break;
                    case 'right':
                        style.borderRadius = `${this.borderRadius}rpx 0 0 ${this.borderRadius}rpx`;
                        break;
                    case 'bottom':
                        style.borderRadius = `${this.borderRadius}rpx ${this.borderRadius}rpx 0 0`;
                        break;
                    default:
                }
                // 不加可能圆角无效
                style.overflow = 'hidden';
            }
            if(this.duration) style.transition = `all ${this.duration / 1000}s linear`;
            return style;
        },
        // 中部弹窗的特有样式
        centerStyle() {
            let style = {};
            style.width = this.width ? this.getUnitValue(this.width) : this.getUnitValue(this.length);
            // 中部弹出的模式,如果没有设置高度,就用auto值,由内容撑开高度
            style.height = this.height ? this.getUnitValue(this.height) : 'auto';
            style.zIndex = this.uZindex;
            style.marginTop = `-${this.$u.addUnit(this.negativeTop)}`;
            if (this.borderRadius) {
                style.borderRadius = `${this.borderRadius}rpx`;
                // 不加可能圆角无效
                style.overflow = 'hidden';
            }
            return style;
        },
        // 计算整理后的z-index值
        uZindex() {
            return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
        }
    },
    watch: {
        value(val) {
            if (val) {
                this.open();
            } else if(!this.closeFromInner) {
                this.close();
            }
            this.closeFromInner = false;
        }
    },
    mounted() {
        // 组件渲染完成时,检查value是否为true,如果是,弹出popup
        this.value && this.open();
    },
    methods: {
        // 判断传入的值,是否带有单位,如果没有,就默认用rpx单位
        getUnitValue(val) {
            if(/(%|px|rpx|auto)$/.test(val)) return val;
            else return val + 'rpx'
        },
        // 遮罩被点击
        maskClick() {
            this.close();
        },
        close() {
            // 标记关闭是内部发生的,否则修改了value值,导致watch中对value检测,导致再执行一遍close
            // 造成@close事件触发两次
            this.closeFromInner = true;
            this.change('showDrawer', 'visibleSync', false);
        },
        // 中部弹出时,需要.u-drawer-content将居中内容,此元素会铺满屏幕,点击需要关闭弹窗
        // 让其只在mode=center时起作用
        modeCenterClose(mode) {
            if (mode != 'center' || !this.maskCloseAble) return;
            this.close();
        },
        open() {
            this.change('visibleSync', 'showDrawer', true);
        },
        // 此处的原理是,关闭时先通过动画隐藏弹窗和遮罩,再移除整个组件
        // 打开时,先渲染组件,延时一定时间再让遮罩和弹窗的动画起作用
        change(param1, param2, status) {
            // 如果this.popup为false,意味着为picker,actionsheet等组件调用了popup组件
            if (this.popup == true) {
                this.$emit('input', status);
            }
            this[param1] = status;
            if(status) {
                // #ifdef H5 || MP
                this.timer = setTimeout(() => {
                    this[param2] = status;
                    this.$emit(status ? 'open' : 'close');
                }, 50);
                // #endif
                // #ifndef H5 || MP
                this.$nextTick(() => {
                    this[param2] = status;
                    this.$emit(status ? 'open' : 'close');
                })
                // #endif
            } else {
                this.timer = setTimeout(() => {
                    this[param2] = status;
                    this.$emit(status ? 'open' : 'close');
                }, this.duration);
            }
        }
    }
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-drawer {
    /* #ifndef APP-NVUE */
    display: block;
    /* #endif */
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    overflow: hidden;
}
.u-drawer-content {
    /* #ifndef APP-NVUE */
    display: block;
    /* #endif */
    position: absolute;
    z-index: 1003;
    transition: all 0.25s linear;
}
.u-drawer__scroll-view {
    width: 100%;
    height: 100%;
}
.u-drawer-left {
    top: 0;
    bottom: 0;
    left: 0;
    background-color: #ffffff;
}
.u-drawer-right {
    right: 0;
    top: 0;
    bottom: 0;
    background-color: #ffffff;
}
.u-drawer-top {
    top: 0;
    left: 0;
    right: 0;
    background-color: #ffffff;
}
.u-drawer-bottom {
    bottom: 0;
    left: 0;
    right: 0;
    background-color: #ffffff;
}
.u-drawer-center {
    @include vue-flex;
    flex-direction: column;
    bottom: 0;
    left: 0;
    right: 0;
    top: 0;
    justify-content: center;
    align-items: center;
    opacity: 0;
    z-index: 99999;
}
.u-mode-center-box {
    min-width: 100rpx;
    min-height: 100rpx;
    /* #ifndef APP-NVUE */
    display: block;
    /* #endif */
    position: relative;
    background-color: #ffffff;
}
.u-drawer-content-visible.u-drawer-center {
    transform: scale(1);
    opacity: 1;
}
.u-animation-zoom {
    transform: scale(1.15);
}
.u-drawer-content-visible {
    transform: translate3D(0px, 0px, 0px) !important;
}
.u-close {
    position: absolute;
    z-index: 3;
}
.u-close--top-left {
    top: 30rpx;
    left: 30rpx;
}
.u-close--top-right {
    top: 30rpx;
    right: 30rpx;
}
.u-close--bottom-left {
    bottom: 30rpx;
    left: 30rpx;
}
.u-close--bottom-right {
    right: 30rpx;
    bottom: 30rpx;
}
</style>
uview-ui/components/u-radio-group/u-radio-group.vue
New file
@@ -0,0 +1,128 @@
<template>
    <view class="u-radio-group u-clearfix">
        <slot></slot>
    </view>
</template>
<script>
    import Emitter from '../../libs/util/emitter.js';
    /**
     * radioRroup 单选框父组件
     * @description 单选框用于有一个选择,用户只能选择其中一个的场景。搭配u-radio使用
     * @tutorial https://www.uviewui.com/components/radio.html
     * @property {Boolean} disabled 是否禁用所有radio(默认false)
     * @property {String Number} size 组件整体的大小,单位rpx(默认40)
     * @property {String} active-color 选中时的颜色,应用到所有子Radio组件(默认#2979ff)
     * @property {String Number} icon-size 图标大小,单位rpx(默认20)
     * @property {String} shape 外观形状,shape-方形,circle-圆形(默认circle)
     * @property {Boolean} label-disabled 是否禁止点击文本操作checkbox(默认false)
     * @property {String} width 宽度,需带单位
     * @property {Boolean} wrap 是否每个radio都换行(默认false)
     * @event {Function} change 任一个radio状态发生变化时触发
     * @example <u-radio-group v-model="value"></u-radio-group>
     */
    export default {
        name: "u-radio-group",
        mixins: [Emitter],
        props: {
            // 是否禁用所有单选框
            disabled: {
                type: Boolean,
                default: false
            },
            // 匹配某一个radio组件,如果某个radio的name值等于此值,那么这个radio就被会选中
            value: {
                type: [String, Number],
                default: ''
            },
            // 选中状态下的颜色
            activeColor: {
                type: String,
                default: '#2979ff'
            },
            // 组件的整体大小
            size: {
                type: [String, Number],
                default: 34
            },
            // 是否禁止点击提示语选中复选框
            labelDisabled: {
                type: Boolean,
                default: false
            },
            // 形状,square为方形,circle为原型
            shape: {
                type: String,
                default: 'circle'
            },
            // 图标的大小,单位rpx
            iconSize: {
                type: [String, Number],
                default: 20
            },
            // 每个checkbox占u-checkbox-group的宽度
            width: {
                type: [String, Number],
                default: 'auto'
            },
            // 是否每个checkbox都换行
            wrap: {
                type: Boolean,
                default: false
            }
        },
        created() {
            // 如果将children定义在data中,在微信小程序会造成循环引用而报错
            this.children = [];
        },
        watch: {
            // 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件
            parentData() {
                if(this.children.length) {
                    this.children.map(child => {
                        // 判断子组件(u-radio)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
                        typeof(child.updateParentData) == 'function' && child.updateParentData();
                    })
                }
            },
        },
        computed: {
            // 这里computed的变量,都是子组件u-radio需要用到的,由于头条小程序的兼容性差异,子组件无法实时监听父组件参数的变化
            // 所以需要手动通知子组件,这里返回一个parentData变量,供watch监听,在其中去通知每一个子组件重新从父组件(u-radio-group)
            // 拉取父组件新的变化后的参数
            parentData() {
                return [this.value, this.disabled, this.activeColor, this.size, this.labelDisabled, this.shape, this.iconSize, this.width, this.wrap];
            }
        },
        methods: {
            // 该方法有子组件radio调用,当一个radio被选中的时候,给父组件设置value值(props传递的value)
            setValue(val) {
                // 通过子组件传递过来的val值(此被选中的子组件内部已将parentValue设置等于val的值),将其他
                // u-radio设置未选中的状态
                this.children.map(child => {
                    if(child.parentData.value != val) child.parentData.value = '';
                })
                // 通过emit事件,设置父组件通过v-model双向绑定的值
                this.$emit('input', val);
                this.$emit('change', val);
                // 等待下一个周期再执行,因为this.$emit('input')作用于父组件,再反馈到子组件内部,需要时间
                // 由于头条小程序执行迟钝,故需要用几十毫秒的延时
                setTimeout(() => {
                    // 将当前的值发送到 u-form-item 进行校验
                    this.dispatch('u-form-item', 'on-form-change', val);
                }, 60)
            }
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-radio-group {
        /* #ifndef MP || APP-NVUE */
        display: inline-flex;
        flex-wrap: wrap;
        /* #endif */
    }
</style>
uview-ui/components/u-radio/u-radio.vue
New file
@@ -0,0 +1,271 @@
<template>
    <view class="u-radio" :style="[radioStyle]">
        <view class="u-radio__icon-wrap" @tap="toggle" :class="[iconClass]" :style="[iconStyle]">
            <u-icon
                class="u-radio__icon-wrap__icon"
                name="checkbox-mark"
                :size="elIconSize"
                :color="iconColor"/>
        </view>
        <view class="u-radio__label" @tap="onClickLabel" :style="{
            fontSize: $u.addUnit(labelSize)
        }">
            <slot />
        </view>
    </view>
</template>
<script>
    /**
     * radio 单选框
     * @description 单选框用于有一个选择,用户只能选择其中一个的场景。搭配u-radio-group使用
     * @tutorial https://www.uviewui.com/components/radio.html
     * @property {String Number} icon-size 图标大小,单位rpx(默认24)
     * @property {String Number} label-size label字体大小,单位rpx(默认28)
     * @property {String Number} name radio组件的标示符
     * @property {String} shape 形状,见上方说明(默认circle)
     * @property {Boolean} disabled 是否禁用(默认false)
     * @property {Boolean} label-disabled 点击文本是否可以操作radio(默认true)
     * @property {String} active-color 选中时的颜色,如设置parent的active-color将失效
     * @event {Function} change 某个radio状态发生变化时触发(选中状态)
     * @example <u-radio :label-disabled="false">门掩黄昏,无计留春住</u-radio>
     */
    export default {
        name: "u-radio",
        props: {
            // radio的名称
            name: {
                type: [String, Number],
                default: ''
            },
            // 形状,square为方形,circle为原型
            shape: {
                type: String,
                default: ''
            },
            // 是否禁用
            disabled: {
                type: [String, Boolean],
                default: ''
            },
            // 是否禁止点击提示语选中复选框
            labelDisabled: {
                type: [String, Boolean],
                default: ''
            },
            // 选中状态下的颜色,如设置此值,将会覆盖parent的activeColor值
            activeColor: {
                type: String,
                default: ''
            },
            // 图标的大小,单位rpx
            iconSize: {
                type: [String, Number],
                default: ''
            },
            // label的字体大小,rpx单位
            labelSize: {
                type: [String, Number],
                default: ''
            },
        },
        data() {
            return {
                // 父组件的默认值,因为头条小程序不支持在computed中使用this.parent.shape的形式
                // 故只能使用如此方法
                parentData: {
                    iconSize: null,
                    labelDisabled: null,
                    disabled: null,
                    shape: null,
                    activeColor: null,
                    size: null,
                    width: null,
                    height: null,
                    value: null,
                    wrap: null
                }
            };
        },
        created() {
            this.parent = false;
            // 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环引用
            this.updateParentData();
            this.parent.children.push(this);
        },
        computed: {
            // 是否禁用,如果父组件u-raios-group禁用的话,将会忽略子组件的配置
            elDisabled() {
                return this.disabled !== '' ? this.disabled : this.parentData.disabled !== null ? this.parentData.disabled : false;
            },
            // 是否禁用label点击
            elLabelDisabled() {
                return this.labelDisabled !== '' ? this.labelDisabled : this.parentData.labelDisabled !== null ? this.parentData.labelDisabled : false;
            },
            // 组件尺寸,对应size的值,默认值为34rpx
            elSize() {
                return this.size ? this.size : (this.parentData.size ? this.parentData.size : 34);
            },
            // 组件的勾选图标的尺寸,默认20
            elIconSize() {
                return this.iconSize ? this.iconSize : (this.parentData.iconSize ? this.parentData.iconSize : 20);
            },
            // 组件选中激活时的颜色
            elActiveColor() {
                return this.activeColor ? this.activeColor : (this.parentData.activeColor ? this.parentData.activeColor : 'primary');
            },
            // 组件的形状
            elShape() {
                return this.shape ? this.shape : (this.parentData.shape ? this.parentData.shape : 'circle');
            },
            // 设置radio的状态,要求radio的name等于parent的value时才为选中状态
            iconStyle() {
                let style = {};
                if (this.elActiveColor && this.parentData.value == this.name && !this.elDisabled) {
                    style.borderColor = this.elActiveColor;
                    style.backgroundColor = this.elActiveColor;
                }
                style.width = this.$u.addUnit(this.elSize);
                style.height = this.$u.addUnit(this.elSize);
                return style;
            },
            iconColor() {
                return this.name ==  this.parentData.value ? '#ffffff' : 'transparent';
            },
            iconClass() {
                let classes = [];
                classes.push('u-radio__icon-wrap--' + this.elShape);
                if (this.name == this.parentData.value) classes.push('u-radio__icon-wrap--checked');
                if (this.elDisabled) classes.push('u-radio__icon-wrap--disabled');
                if (this.name == this.parentData.value && this.elDisabled) classes.push(
                    'u-radio__icon-wrap--disabled--checked');
                // 支付宝小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
                return classes.join(' ');
            },
            radioStyle() {
                let style = {};
                if (this.parentData.width) {
                    style.width = this.$u.addUnit(this.parentData.width);
                    // #ifdef MP
                    // 各家小程序因为它们特殊的编译结构,使用float布局
                    style.float = 'left';
                    // #endif
                    // #ifndef MP
                    // H5和APP使用flex布局
                    style.flex = `0 0 ${this.$u.addUnit(this.parentData.width)}`;
                    // #endif
                }
                if (this.parentData.wrap) {
                    style.width = '100%';
                    // #ifndef MP
                    // H5和APP使用flex布局,将宽度设置100%,即可自动换行
                    style.flex = '0 0 100%';
                    // #endif
                }
                return style;
            }
        },
        methods: {
            updateParentData() {
                this.getParentData('u-radio-group');
            },
            onClickLabel() {
                if (!this.elLabelDisabled && !this.elDisabled) {
                    this.setRadioCheckedStatus();
                }
            },
            toggle() {
                if (!this.elDisabled) {
                    this.setRadioCheckedStatus();
                }
            },
            emitEvent() {
                // u-radio的name不等于父组件的v-model的值时(意味着未选中),才发出事件,避免多次点击触发事件
                if(this.parentData.value != this.name) this.$emit('change', this.name);
            },
            // 改变组件选中状态
            // 这里的改变的依据是,更改本组件的parentData.value值为本组件的name值,同时通过父组件遍历所有u-radio实例
            // 将本组件外的其他u-radio的parentData.value都设置为空(由computed计算后,都被取消选中状态),因而只剩下一个为选中状态
            setRadioCheckedStatus() {
                this.emitEvent();
                if(this.parent) {
                    this.parent.setValue(this.name);
                    this.parentData.value = this.name;
                }
            }
        }
    };
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-radio {
        /* #ifndef APP-NVUE */
        display: inline-flex;
        /* #endif */
        align-items: center;
        overflow: hidden;
        user-select: none;
        line-height: 1.8;
        &__icon-wrap {
            color: $u-content-color;
            @include vue-flex;
            flex: none;
            align-items: center;
            justify-content: center;
            box-sizing: border-box;
            width: 42rpx;
            height: 42rpx;
            color: transparent;
            text-align: center;
            transition-property: color, border-color, background-color;
            font-size: 20px;
            border: 1px solid #c8c9cc;
            transition-duration: 0.2s;
            /* #ifdef MP-TOUTIAO */
            // 头条小程序兼容性问题,需要设置行高为0,否则图标偏下
            &__icon {
                line-height: 0;
            }
            /* #endif */
            &--circle {
                border-radius: 100%;
            }
            &--square {
                border-radius: 3px;
            }
            &--checked {
                color: #fff;
                background-color: #2979ff;
                border-color: #2979ff;
            }
            &--disabled {
                background-color: #ebedf0;
                border-color: #c8c9cc;
            }
            &--disabled--checked {
                color: #c8c9cc !important;
            }
        }
        &__label {
            word-wrap: break-word;
            margin-left: 10rpx;
            margin-right: 24rpx;
            color: $u-content-color;
            font-size: 30rpx;
            &--disabled {
                color: #c8c9cc;
            }
        }
    }
</style>
uview-ui/components/u-rate/u-rate.vue
New file
@@ -0,0 +1,275 @@
<template>
    <view class="u-rate" :id="elId" @touchmove.stop.prevent="touchMove">
        <view class="u-star-wrap" v-for="(item, index) in count" :key="index" :class="[elClass]">
            <u-icon
                :name="activeIndex > index ? elActiveIcon : inactiveIcon"
                @click="click(index + 1, $event)"
                :color="activeIndex > index ? elActiveColor : inactiveColor"
                :custom-style="{
                    fontSize: size + 'rpx',
                    padding: `0 ${gutter / 2 + 'rpx'}`
                }"
                :custom-prefix="customPrefix"
                :show-decimal-icon="showDecimalIcon(index)"
                :percent="decimal"
                :inactive-color="inactiveColor"
            ></u-icon>
        </view>
    </view>
</template>
<script>/**
 * rate 评分
 * @description 该组件一般用于满意度调查,星型评分的场景
 * @tutorial https://www.uviewui.com/components/rate.html
 * @property {String Number} count 最多可选的星星数量(默认5)
 * @property {String Number} current 默认选中的星星数量(默认0)
 * @property {Boolean} disabled 是否禁止用户操作(默认false)
 * @property {String Number} size 星星的大小,单位rpx(默认32)
 * @property {String} inactive-color 未选中星星的颜色(默认#b2b2b2)
 * @property {String} active-color 选中的星星颜色(默认#FA3534)
 * @property {String} active-icon 选中时的图标名,只能为uView的内置图标(默认star-fill)
 * @property {String} inactive-icon 未选中时的图标名,只能为uView的内置图标(默认star)
 * @property {String} gutter 星星之间的距离(默认10)
 * @property {String Number} min-count 最少选中星星的个数(默认0)
 * @property {Boolean} allow-half 是否允许半星选择(默认false)
 * @event {Function} change 选中的星星发生变化时触发
 * @example <u-rate :count="count" :current="2"></u-rate>
 */
export default {
    name: 'u-rate',
    props: {
        // 用于v-model双向绑定选中的星星数量
        // 1.4.5版新增
        value: {
            type: [Number, String],
            default: -1
        },
        // 要显示的星星数量
        count: {
            type: [Number, String],
            default: 5
        },
        // 当前需要默认选中的星星(选中的个数)
        // 1.4.5后通过value双向绑定,不再建议使用此参数
        current: {
            type: [Number, String],
            default: 0
        },
        // 是否不可选中
        disabled: {
            type: Boolean,
            default: false
        },
        // 星星的大小,单位rpx
        size: {
            type: [Number, String],
            default: 32
        },
        // 未选中时的颜色
        inactiveColor: {
            type: String,
            default: '#b2b2b2'
        },
        // 选中的颜色
        activeColor: {
            type: String,
            default: '#FA3534'
        },
        // 星星之间的间距,单位rpx
        gutter: {
            type: [Number, String],
            default: 10
        },
        // 最少能选择的星星个数
        minCount: {
            type: [Number, String],
            default: 0
        },
        // 是否允许半星(功能尚未实现)
        allowHalf: {
            type: Boolean,
            default: false
        },
        // 选中时的图标(星星)
        activeIcon: {
            type: String,
            default: 'star-fill'
        },
        // 未选中时的图标(星星)
        inactiveIcon: {
            type: String,
            default: 'star'
        },
        // 自定义扩展前缀,方便用户扩展自己的图标库
        customPrefix: {
            type: String,
            default: 'uicon'
        },
        colors: {
            type: Array,
            default() {
                return []
            }
        },
        icons: {
            type: Array,
            default() {
                return []
            }
        }
    },
    data() {
        return {
            // 生成一个唯一id,否则一个页面多个评分组件,会造成冲突
            elId: this.$u.guid(),
            elClass: this.$u.guid(),
            starBoxLeft: 0, // 评分盒子左边到屏幕左边的距离,用于滑动选择时计算距离
            // 当前激活的星星的index,如果存在value,优先使用value,因为它可以双向绑定(1.4.5新增)
            activeIndex: this.value != -1 ? this.value : this.current,
            starWidth: 0, // 每个星星的宽度
            starWidthArr: [] //每个星星最右边到组件盒子最左边的距离
        }
    },
    watch: {
        current(val) {
            this.activeIndex = val
        },
        value(val) {
            this.activeIndex = val
        }
    },
    computed: {
        decimal() {
            if (this.disabled) {
                return this.activeIndex * 100 % 100
            } else if (this.allowHalf) {
                return 50
            }
        },
        elActiveIcon() {
            const len = this.icons.length
            // 此处规则类似于下方的elActiveColor参数,都是根据一定的规则,显示不同的图标
            // 结果可能如此:icons参数传递了3个图标,当选中两个时,用第一个图标,4个时,用第二个图标
            // 第三个时,用第三个图标作为激活的图标
            if (len && len <= this.count) {
                const step = Math.round(this.activeIndex / Math.round(this.count / len))
                if (step < 1) return this.icons[0]
                if (step > len) return this.icons[len - 1]
                return this.icons[step - 1]
            }
            return this.activeIcon
        },
        elActiveColor() {
            const len = this.colors.length
            // 如果有设置colors参数(此参数用于将图标分段,比如一共5颗星,colors传3个颜色值,那么根据一定的规则,2颗星可能为第一个颜色
            // 4颗星为第二个颜色值,5颗星为第三个颜色值)
            if (len && len <= this.count) {
                const step = Math.round(this.activeIndex / Math.round(this.count / len))
                if (step < 1) return this.colors[0]
                if (step > len) return this.colors[len - 1]
                return this.colors[step - 1]
            }
            return this.activeColor
        }
    },
    methods: {
        // 获取评分组件盒子的布局信息
        getElRectById() {
            // uView封装的获取节点的方法,详见文档
            this.$uGetRect('#' + this.elId).then(res => {
                this.starBoxLeft = res.left
            })
        },
        // 获取单个星星的尺寸
        getElRectByClass() {
            // uView封装的获取节点的方法,详见文档
            this.$uGetRect('.' + this.elClass).then(res => {
                this.starWidth = res.width
                // 把每个星星右边到组件盒子左边的距离放入数组中
                for (let i = 0; i < this.count; i++) {
                    this.starWidthArr[i] = (i + 1) * this.starWidth
                }
            })
        },
        // 手指滑动
        touchMove(e) {
            if (this.disabled) {
                return
            }
            if (!e.changedTouches[0]) {
                return
            }
            const movePageX = e.changedTouches[0].pageX
            // 滑动点相对于评分盒子左边的距离
            const distance = movePageX - this.starBoxLeft
            // 如果滑动到了评分盒子的左边界,就设置为0星
            if (distance <= 0) {
                this.activeIndex = 0
            }
            // 滑动的距离,相当于多少颗星星
            let index = Math.ceil(distance / this.starWidth)
            this.activeIndex = index > this.count ? this.count : index
            // 对最少颗星星的限制
            if (this.activeIndex < this.minCount) this.activeIndex = this.minCount
            this.emitEvent()
        },
        // 通过点击,直接选中
        click(index, e) {
            if (this.disabled) {
                return
            }
            // 半星选择,尚未实现
            if (this.allowHalf) {
            }
            // 对第一个星星特殊处理,只有一个的时候,点击可以取消,否则无法作0星评价
            if (index == 1) {
                if (this.activeIndex == 1) {
                    this.activeIndex = 0
                } else {
                    this.activeIndex = 1
                }
            } else {
                this.activeIndex = index
            }
            // 对最少颗星星的限制
            if (this.activeIndex < this.minCount) this.activeIndex = this.minCount
            this.emitEvent()
        },
        // 发出事件
        emitEvent() {
            // 发出change事件
            this.$emit('change', this.activeIndex)
            // 同时修改双向绑定的value的值
            if (this.value != -1) {
                this.$emit('input', this.activeIndex)
            }
        },
        showDecimalIcon(index) {
            return this.disabled && parseInt(this.activeIndex) === index
        }
    },
    mounted() {
        this.getElRectById()
        this.getElRectByClass()
    }
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-rate {
    display: -webkit-inline-flex;
    display: inline-flex;
    align-items: center;
    margin: 0;
    padding: 0;
}
.u-icon {
    box-sizing: border-box;
}
</style>
uview-ui/components/u-read-more/u-read-more.vue
New file
@@ -0,0 +1,179 @@
<template>
    <view class="">
        <view class="u-content" :class="[elId]" :style="{
            height: isLongContent && !showMore ? showHeight + 'rpx' : 'auto',
            textIndent: textIndent
        }">
            <slot></slot>
        </view>
        <view @tap="toggleReadMore" v-if="isLongContent" class="u-content__showmore-wrap"
            :class="{ 'u-content__show-more': showMore }"
            :style="[innerShadowStyle]">
            <text class="u-content__showmore-wrap__readmore-btn" :style="{
                fontSize: fontSize + 'rpx',
                color: color
            }">
                {{ showMore ? openText : closeText }}
            </text>
            <view class="u-content__showmore-wrap__readmore-btn__icon u-flex">
                <u-icon :color="color" :size="fontSize" :name="showMore ? 'arrow-up' : 'arrow-down'"></u-icon>
            </view>
        </view>
    </view>
</template>
<script>
    /**
     * readMore 阅读更多
     * @description 该组件一般用于内容较长,预先收起一部分,点击展开全部内容的场景。
     * @tutorial https://www.uviewui.com/components/readMore.html
     * @property {String Number} show-height 内容超出此高度才会显示展开全文按钮,单位rpx(默认400)
     * @property {Boolean} toggle 展开后是否显示收起按钮(默认false)
     * @property {String} close-text 关闭时的提示文字(默认“展开阅读全文”)
     * @property {String Number} font-size 提示文字的大小,单位rpx(默认28)
     * @property {String} text-indent 段落首行缩进的字符个数(默认2em)
     * @property {String} open-text 展开时的提示文字(默认“收起”)
     * @property {String} color 提示文字的颜色(默认#2979ff)
     * @example <u-read-more><rich-text :nodes="content"></rich-text></u-read-more>
     */
    export default {
        name: "u-read-more",
        props: {
            // 默认的显示占位高度,单位为rpx
            showHeight: {
                type: [Number, String],
                default: 400
            },
            // 展开后是否显示"收起"按钮
            toggle: {
                type: Boolean,
                default: false
            },
            // 关闭时的提示文字
            closeText: {
                type: String,
                default: '展开阅读全文'
            },
            // 展开时的提示文字
            openText: {
                type: String,
                default: '收起'
            },
            // 提示的文字颜色
            color: {
                type: String,
                default: '#2979ff'
            },
            // 提示文字的大小
            fontSize: {
                type: [String, Number],
                default: 28
            },
            // 是否显示阴影
            shadowStyle: {
                type: Object,
                default () {
                    return {
                        backgroundImage: "linear-gradient(-180deg, rgba(255, 255, 255, 0) 0%, #fff 80%)",
                        paddingTop: "300rpx",
                        marginTop: "-300rpx"
                    }
                }
            },
            // 段落首行缩进的字符个数
            textIndent: {
                type: String,
                default: '2em'
            },
            // open和close事件时,将此参数返回在回调参数中
            index: {
                type: [Number, String],
                default: ''
            }
        },
        watch: {
            paramsChange(val) {
                this.init();
            }
        },
        computed: {
            paramsChange() {
                return `${this.toggle}-${this.showHeight}`;
            },
            // 展开后无需阴影,收起时才需要阴影样式
            innerShadowStyle() {
                if (this.showMore) return {};
                else return this.shadowStyle
            }
        },
        data() {
            return {
                isLongContent: false, // 是否需要隐藏一部分内容
                showMore: false, // 当前隐藏与显示的状态,true-显示,false-收起
                elId: this.$u.guid(), // 生成唯一class
            };
        },
        mounted() {
            this.$nextTick(() => {
                this.init();
            })
        },
        methods: {
            init() {
                this.$uGetRect('.' + this.elId).then(res => {
                    // 判断高度,如果真实内容高度大于占位高度,则显示收起与展开的控制按钮
                    if (res.height > uni.upx2px(this.showHeight)) {
                        this.isLongContent = true;
                        this.showMore = false;
                    }
                })
            },
            // 展开或者收起
            toggleReadMore() {
                this.showMore = !this.showMore;
                // 如果toggle为false,隐藏"收起"部分的内容
                if (this.toggle == false) this.isLongContent = false;
                // 发出打开或者收齐的事件
                this.$emit(this.showMore ? 'open' : 'close', this.index);
            }
        }
    };
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-content {
        font-size: 30rpx;
        color: $u-content-color;
        line-height: 1.8;
        text-align: left;
        overflow: hidden;
        &__show-more {
            padding-top: 0;
            background: none;
            margin-top: 20rpx;
        }
        &__showmore-wrap {
            position: relative;
            width: 100%;
            padding-bottom: 26rpx;
            @include vue-flex;
            align-items: center;
            justify-content: center;
            &__readmore-btn {
                @include vue-flex;
                align-items: center;
                justify-content: center;
                line-height: 1;
                &__icon {
                    margin-left: 14rpx;
                }
            }
        }
    }
</style>
uview-ui/components/u-row-notice/u-row-notice.vue
New file
@@ -0,0 +1,269 @@
<template>
    <view
        v-if="show"
        class="u-notice-bar"
        :style="{
            background: computeBgColor,
            padding: padding
        }"
        :class="[
            type ? `u-type-${type}-light-bg` : ''
        ]"
    >
        <view class="u-direction-row">
            <view class="u-icon-wrap">
                <u-icon class="u-left-icon" v-if="volumeIcon" name="volume-fill" :size="volumeSize" :color="computeColor"></u-icon>
            </view>
            <view class="u-notice-box" id="u-notice-box">
                <view
                    class="u-notice-content"
                    id="u-notice-content"
                    :style="{
                        animationDuration: animationDuration,
                        animationPlayState: animationPlayState,
                    }"
                >
                    <text class="u-notice-text" @tap="click" :style="[textStyle]"
                    :class="['u-type-' + type]">{{showText}}</text>
                </view>
            </view>
            <view class="u-icon-wrap">
                <u-icon @click="getMore" class="u-right-icon" v-if="moreIcon" name="arrow-right" :size="26" :color="computeColor"></u-icon>
                <u-icon @click="close" class="u-right-icon" v-if="closeIcon" name="close" :size="24" :color="computeColor"></u-icon>
            </view>
        </view>
    </view>
</template>
<script>
export default {
    props: {
        // 显示的内容,数组
        list: {
            type: Array,
            default() {
                return [];
            }
        },
        // 显示的主题,success|error|primary|info|warning|none
        // none主题默认为透明背景,黑色(contentColor)字体
        type: {
            type: String,
            default: 'warning'
        },
        // 是否显示左侧的音量图标
        volumeIcon: {
            type: Boolean,
            default: true
        },
        // 是否显示右侧的右箭头图标
        moreIcon: {
            type: Boolean,
            default: false
        },
        // 是否显示右侧的关闭图标
        closeIcon: {
            type: Boolean,
            default: false
        },
        // 是否自动播放
        autoplay: {
            type: Boolean,
            default: true
        },
        // 文字颜色,各图标也会使用文字颜色
        color: {
            type: String,
            default: ''
        },
        // 背景颜色
        bgColor: {
            type: String,
            default: ''
        },
        // 是否显示
        show: {
            type: Boolean,
            default: true
        },
        // 字体大小,单位rpx
        fontSize: {
            type: [Number, String],
            default: 26
        },
        // 音量喇叭的大小
        volumeSize: {
            type: [Number, String],
            default: 34
        },
        // 水平滚动时的滚动速度,即每秒滚动多少rpx,这有利于控制文字无论多少时,都能有一个恒定的速度
        speed: {
            type: [Number, String],
            default: 160
        },
        // 播放状态,play-播放,paused-暂停
        playState: {
            type: String,
            default: 'play'
        },
        // 通知的边距
        padding: {
            type: [Number, String],
            default: '18rpx 24rpx'
        }
    },
    data() {
        return {
            textWidth: 0, // 滚动的文字宽度
            boxWidth: 0, // 供文字滚动的父盒子的宽度,和前者一起为了计算滚动速度
            animationDuration: '10s', // 动画执行时间
            animationPlayState: 'paused', // 动画的开始和结束执行
            showText: '' // 显示的文本
        };
    },
    watch: {
        list: {
            immediate: true,
            handler(val) {
                this.showText = val.join(',');
                this.$nextTick(() => {
                    this.initSize();
                });
            }
        },
        playState(val) {
            if(val == 'play') this.animationPlayState = 'running';
            else this.animationPlayState = 'paused';
        },
        speed(val) {
            this.initSize();
        }
    },
    computed: {
        // 计算字体颜色,如果没有自定义的,就用uview主题颜色
        computeColor() {
            if (this.color) return this.color;
            // 如果是无主题,就默认使用content-color
            else if(this.type == 'none') return '#606266';
            else return this.type;
        },
        // 文字内容的样式
        textStyle() {
            let style = {};
            if (this.color) style.color = this.color;
            else if(this.type == 'none') style.color = '#606266';
            style.fontSize = this.fontSize + 'rpx';
            return style;
        },
        // 计算背景颜色
        computeBgColor() {
            if (this.bgColor) return this.bgColor;
            else if(this.type == 'none') return 'transparent';
        }
    },
    mounted() {
        this.$nextTick(() => {
            this.initSize();
        });
    },
    methods: {
        initSize() {
            let query = [],
                boxWidth = 0,
                textWidth = 0;
            let textQuery = new Promise((resolve, reject) => {
                uni.createSelectorQuery()
                    .in(this)
                    .select(`#u-notice-content`)
                    .boundingClientRect()
                    .exec(ret => {
                        this.textWidth = ret[0].width;
                        resolve();
                    });
            });
            query.push(textQuery);
            Promise.all(query).then(() => {
                // 根据t=s/v(时间=路程/速度),这里为何不需要加上#u-notice-box的宽度,因为中设置了.u-notice-content样式中设置了padding-left: 100%
                // 恰巧计算出来的结果中已经包含了#u-notice-box的宽度
                this.animationDuration = `${this.textWidth / uni.upx2px(this.speed)}s`;
                // 这里必须这样开始动画,否则在APP上动画速度不会改变(HX版本2.4.6,IOS13)
                this.animationPlayState = 'paused';
                setTimeout(() => {
                    if(this.playState == 'play' && this.autoplay) this.animationPlayState = 'running';
                }, 10);
            });
        },
        // 点击通告栏
        click(index) {
            this.$emit('click');
        },
        // 点击关闭按钮
        close() {
            this.$emit('close');
        },
        // 点击更多箭头按钮
        getMore() {
            this.$emit('getMore');
        }
    }
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-notice-bar {
    padding: 18rpx 24rpx;
    overflow: hidden;
}
.u-direction-row {
    @include vue-flex;
    align-items: center;
    justify-content: space-between;
}
.u-left-icon {
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
    align-items: center;
}
.u-notice-box {
    flex: 1;
    @include vue-flex;
    overflow: hidden;
    margin-left: 12rpx;
}
.u-right-icon {
    margin-left: 12rpx;
    display: inline-flex;
    align-items: center;
}
.u-notice-content {
    animation: u-loop-animation 10s linear infinite both;
    text-align: right;
    // 这一句很重要,为了能让滚动左右连接起来
    padding-left: 100%;
    @include vue-flex;
    flex-wrap: nowrap;
}
.u-notice-text {
    font-size: 26rpx;
    word-break: keep-all;
    white-space: nowrap
}
@keyframes u-loop-animation {
    0% {
        transform: translate3d(0, 0, 0);
    }
    100% {
        transform: translate3d(-100%, 0, 0);
    }
}
</style>
uview-ui/components/u-row/u-row.vue
New file
@@ -0,0 +1,84 @@
<template>
    <view class="u-row" :style="{
            alignItems: uAlignItem,
            justifyContent: uJustify
        }"
        @tap="click"
    >
        <slot />
    </view>
</template>
<script>
    /**
     * row 行布局
     * @description 通过基础的 12 分栏,迅速简便地创建布局。
     * @tutorial https://www.uviewui.com/components/layout.html#row-props
     * @property {String Number} gutter 栅格间隔,左右各为此值的一半,单位rpx(默认0)
     * @property {String} justify 水平排列方式(微信小程序暂不支持)默认(start(或flex-start))
     * @property {String} align 垂直排列方式(默认center)
     * @example <u-row gutter="16"></u-row>
     */
    export default {
        name: "u-row",
        props: {
            // 给col添加间距,左右边距各占一半
            gutter: {
                type: [String, Number],
                default: 20
            },
            // 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`)
            justify: {
                type: String,
                default: 'start'
            },
            // 垂直对齐方式,可选值为top、center、bottom
            align: {
                type: String,
                default: 'center'
            },
            // 是否阻止事件传播
            stop: {
                type: Boolean,
                default: true
            }
        },
        computed: {
            uJustify() {
                if (this.justify == 'end' || this.justify == 'start') return 'flex-' + this.justify;
                else if (this.justify == 'around' || this.justify == 'between') return 'space-' + this.justify;
                else return this.justify;
            },
            uAlignItem() {
                if (this.align == 'top') return 'flex-start';
                if (this.align == 'bottom') return 'flex-end';
                else return this.align;
            }
        },
        methods: {
            click(e) {
                this.$emit('click');
            }
        }
    }
</script>
<style lang="scss">
    @import "../../libs/css/style.components.scss";
    .u-row {
        // 由于微信小程序编译后奇怪的页面结构,只能使用float布局实现,flex无法实现
        /* #ifndef MP-WEIXIN || MP-QQ || MP-TOUTIAO */
        @include vue-flex;
        /* #endif */
        flex-wrap: wrap;
    }
    .u-row:after {
        /* #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO */
        display: table;
        clear: both;
        content: "";
        /* #endif */
    }
</style>
uview-ui/components/u-search/u-search.vue
New file
@@ -0,0 +1,342 @@
<template>
    <view class="u-search" @tap="clickHandler" :style="{
        margin: margin,
    }">
        <view
            class="u-content"
            :style="{
                backgroundColor: bgColor,
                borderRadius: shape == 'round' ? '100rpx' : '10rpx',
                border: borderStyle,
                height: height + 'rpx'
            }"
        >
            <view class="u-icon-wrap">
                <u-icon class="u-clear-icon" :size="30" :name="searchIcon" :color="searchIconColor ? searchIconColor : color"></u-icon>
            </view>
            <input
                confirm-type="search"
                @blur="blur"
                :value="value"
                @confirm="search"
                @input="inputChange"
                :disabled="disabled"
                @focus="getFocus"
                :focus="focus"
                :maxlength="maxlength"
                placeholder-class="u-placeholder-class"
                :placeholder="placeholder"
                :placeholder-style="`color: ${placeholderColor}`"
                class="u-input"
                type="text"
                :style="[{
                    textAlign: inputAlign,
                    color: color,
                    backgroundColor: bgColor,
                }, inputStyle]"
            />
            <view class="u-close-wrap" v-if="keyword && clearabled && focused" @tap="clear">
                <u-icon class="u-clear-icon" name="close-circle-fill" size="34" color="#c0c4cc"></u-icon>
            </view>
        </view>
        <view :style="[actionStyle]" class="u-action"
            :class="[showActionBtn || show ? 'u-action-active' : '']"
            @tap.stop.prevent="custom"
        >{{ actionText }}</view>
    </view>
</template>
<script>
/**
 * search 搜索框
 * @description 搜索组件,集成了常见搜索框所需功能,用户可以一键引入,开箱即用。
 * @tutorial https://www.uviewui.com/components/search.html
 * @property {String} shape 搜索框形状,round-圆形,square-方形(默认round)
 * @property {String} bg-color 搜索框背景颜色(默认#f2f2f2)
 * @property {String} border-color 边框颜色,配置了颜色,才会有边框
 * @property {String} placeholder 占位文字内容(默认“请输入关键字”)
 * @property {Boolean} clearabled 是否启用清除控件(默认true)
 * @property {Boolean} focus 是否自动获得焦点(默认false)
 * @property {Boolean} show-action 是否显示右侧控件(默认true)
 * @property {String} action-text 右侧控件文字(默认“搜索”)
 * @property {Object} action-style 右侧控件的样式,对象形式
 * @property {String} input-align 输入框内容水平对齐方式(默认left)
 * @property {Object} input-style 自定义输入框样式,对象形式
 * @property {Boolean} disabled 是否启用输入框(默认false)
 * @property {String} search-icon-color 搜索图标的颜色,默认同输入框字体颜色
 * @property {String} color 输入框字体颜色(默认#606266)
 * @property {String} placeholder-color placeholder的颜色(默认#909399)
 * @property {String} search-icon 输入框左边的图标,可以为uView图标名称或图片路径
 * @property {String} margin 组件与其他上下左右元素之间的距离,带单位的字符串形式,如"30rpx"
 * @property {Boolean} animation 是否开启动画,见上方说明(默认false)
 * @property {String} value 输入框初始值
 * @property {String | Number} maxlength 输入框最大能输入的长度,-1为不限制长度
 * @property {Boolean} input-style input输入框的样式,可以定义文字颜色,大小等,对象形式
 * @property {String | Number} height 输入框高度,单位rpx(默认64)
 * @event {Function} change 输入框内容发生变化时触发
 * @event {Function} search 用户确定搜索时触发,用户按回车键,或者手机键盘右下角的"搜索"键时触发
 * @event {Function} custom 用户点击右侧控件时触发
 * @event {Function} clear 用户点击清除按钮时触发
 * @example <u-search placeholder="日照香炉生紫烟" v-model="keyword"></u-search>
 */
export default {
    name: "u-search",
    props: {
        // 搜索框形状,round-圆形,square-方形
        shape: {
            type: String,
            default: 'round'
        },
        // 搜索框背景色,默认值#f2f2f2
        bgColor: {
            type: String,
            default: '#f2f2f2'
        },
        // 占位提示文字
        placeholder: {
            type: String,
            default: '请输入关键字'
        },
        // 是否启用清除控件
        clearabled: {
            type: Boolean,
            default: true
        },
        // 是否自动聚焦
        focus: {
            type: Boolean,
            default: false
        },
        // 是否在搜索框右侧显示取消按钮
        showAction: {
            type: Boolean,
            default: true
        },
        // 右边控件的样式
        actionStyle: {
            type: Object,
            default() {
                return {};
            }
        },
        // 取消按钮文字
        actionText: {
            type: String,
            default: '搜索'
        },
        // 输入框内容对齐方式,可选值为 left|center|right
        inputAlign: {
            type: String,
            default: 'left'
        },
        // 是否启用输入框
        disabled: {
            type: Boolean,
            default: false
        },
        // 开启showAction时,是否在input获取焦点时才显示
        animation: {
            type: Boolean,
            default: false
        },
        // 边框颜色,只要配置了颜色,才会有边框
        borderColor: {
            type: String,
            default: 'none'
        },
        // 输入框的初始化内容
        value: {
            type: String,
            default: ''
        },
        // 搜索框高度,单位rpx
        height: {
            type: [Number, String],
            default: 64
        },
        // input输入框的样式,可以定义文字颜色,大小等,对象形式
        inputStyle: {
            type: Object,
            default() {
                return {}
            }
        },
        // 输入框最大能输入的长度,-1为不限制长度(来自uniapp文档)
        maxlength: {
            type: [Number, String],
            default: '-1'
        },
        // 搜索图标的颜色,默认同输入框字体颜色
        searchIconColor: {
            type: String,
            default: ''
        },
        // 输入框字体颜色
        color: {
            type: String,
            default: '#606266'
        },
        // placeholder的颜色
        placeholderColor: {
            type: String,
            default: '#909399'
        },
        // 组件与其他上下左右元素之间的距离,带单位的字符串形式,如"30rpx"、"30rpx 20rpx"等写法
        margin: {
            type: String,
            default: '0'
        },
        // 左边输入框的图标,可以为uView图标名称或图片路径
        searchIcon: {
            type: String,
            default: 'search'
        }
    },
    data() {
        return {
            keyword: '',
            showClear: false, // 是否显示右边的清除图标
            show: false,
            // 标记input当前状态是否处于聚焦中,如果是,才会显示右侧的清除控件
            focused: this.focus
            // 绑定输入框的值
            // inputValue: this.value
        };
    },
    watch: {
        keyword(nVal) {
            // 双向绑定值,让v-model绑定的值双向变化
            this.$emit('input', nVal);
            // 触发change事件,事件效果和v-model双向绑定的效果一样,让用户多一个选择
            this.$emit('change', nVal);
        },
        value: {
            immediate: true,
            handler(nVal) {
                this.keyword = nVal;
            }
        }
    },
    computed: {
        showActionBtn() {
            if (!this.animation && this.showAction) return true;
            else return false;
        },
        // 样式,根据用户传入的颜色值生成,如果不传入,默认为none
        borderStyle() {
            if (this.borderColor) return `1px solid ${this.borderColor}`;
            else return 'none';
        },
    },
    methods: {
        // 目前HX2.6.9 v-model双向绑定无效,故监听input事件获取输入框内容的变化
        inputChange(e) {
            this.keyword = e.detail.value;
        },
        // 清空输入
        // 也可以作为用户通过this.$refs形式调用清空输入框内容
        clear() {
            this.keyword = '';
            // 延后发出事件,避免在父组件监听clear事件时,value为更新前的值(不为空)
            this.$nextTick(() => {
                this.$emit('clear');
            })
        },
        // 确定搜索
        search(e) {
            this.$emit('search', e.detail.value);
            try{
                // 收起键盘
                uni.hideKeyboard();
            }catch(e){}
        },
        // 点击右边自定义按钮的事件
        custom() {
            this.$emit('custom', this.keyword);
            try{
                // 收起键盘
                uni.hideKeyboard();
            }catch(e){}
        },
        // 获取焦点
        getFocus() {
            this.focused = true;
            // 开启右侧搜索按钮展开的动画效果
            if (this.animation && this.showAction) this.show = true;
            this.$emit('focus', this.keyword);
        },
        // 失去焦点
        blur() {
            // 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错
            // 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时
            setTimeout(() => {
                this.focused = false;
            }, 100)
            this.show = false;
            this.$emit('blur', this.keyword);
        },
        // 点击搜索框,只有disabled=true时才发出事件,因为禁止了输入,意味着是想跳转真正的搜索页
        clickHandler() {
            if(this.disabled) this.$emit('click');
        }
    }
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-search {
    @include vue-flex;
    align-items: center;
    flex: 1;
}
.u-content {
    @include vue-flex;
    align-items: center;
    padding: 0 18rpx;
    flex: 1;
}
.u-clear-icon {
    @include vue-flex;
    align-items: center;
}
.u-input {
    flex: 1;
    font-size: 28rpx;
    line-height: 1;
    margin: 0 10rpx;
    color: $u-tips-color;
}
.u-close-wrap {
    width: 40rpx;
    height: 100%;
    @include vue-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
}
.u-placeholder-class {
    color: $u-tips-color;
}
.u-action {
    font-size: 28rpx;
    color: $u-main-color;
    width: 0;
    overflow: hidden;
    transition: all 0.3s;
    white-space: nowrap;
    text-align: center;
}
.u-action-active {
    width: 80rpx;
    margin-left: 10rpx;
}
</style>
uview-ui/components/u-section/u-section.vue
New file
@@ -0,0 +1,154 @@
<template>
    <view class="u-section">
        <view class="u-section__title" :style="{
            fontWeight: bold ? 'bold' : 'normal',
            color: color,
            fontSize: fontSize + 'rpx',
            paddingLeft: showLine ? (fontSize * 0.7) + 'rpx' : 0
        }" :class="{
            'u-section--line': showLine
        }">
            <view class="u-section__title__icon-wrap u-flex" :style="[lineStyle]" v-if="showLine">
                <u-icon top="0" name="column-line" :size="fontSize * 1.25" bold :color="lineColor ? lineColor : color"></u-icon>
            </view>
            <text class="u-flex u-section__title__text">{{title}}</text>
        </view>
        <view class="u-section__right-info" v-if="right || $slots.right" :style="{
            color: subColor
        }" @tap="rightClick">
            <slot name="right" v-if="$slots.right" />
            <block v-else>
                {{subTitle}}
                <view class="u-section__right-info__icon-arrow u-flex" v-if="arrow">
                    <u-icon name="arrow-right" size="24" :color="subColor"></u-icon>
                </view>
            </block>
        </view>
    </view>
</template>
<script>
    /**
     * section 查看更多
     * @description 该组件一般用于分类信息有很多,但是限于篇幅只能列出一部分,让用户通过"查看更多"获得更多信息的场景,实际效果见演示。
     * @tutorial https://www.uviewui.com/components/section.html
     * @property {String} title 左边主标题
     * @property {String} sub-title 右边副标题(默认更多)
     * @property {Boolean} right 是否显示右边的内容(默认true)
     * @property {Boolean} showLine 是否显示左边的竖条(默认true)
     * @property {Boolean} arrow 是否显示右边箭头(默认true)
     * @property {String Number} font-size 主标题的字体大小(默认28)
     * @property {Boolean} bold 主标题是否加粗(默认true)
     * @property {String} color 主标题颜色(默认#303133)
     * @event {Function} click 组件右侧的内容被点击时触发,用于跳转"更多"
     * @example <u-section title="今日热门" :right="false"></u-section>
     */
    export default {
        name: "u-section",
        props: {
            // 标题信息
            title: {
                type: String,
                default: ''
            },
            // 右边副标题内容
            subTitle: {
                type: String,
                default: '更多'
            },
            // 是否显示右边的内容
            right: {
                type: Boolean,
                default: true
            },
            fontSize: {
                type: [Number, String],
                default: 28
            },
            // 主标题是否加粗
            bold: {
                type: Boolean,
                default: true
            },
            // 主标题的颜色
            color: {
                type: String,
                default: '#303133'
            },
            // 右边副标题的颜色
            subColor: {
                type: String,
                default: '#909399'
            },
            // 是否显示左边的竖条
            showLine: {
                type: Boolean,
                default: true
            },
            // 左边竖线的颜色
            lineColor: {
                type: String,
                default: ''
            },
            // 是否显示右边箭头
            arrow: {
                type: Boolean,
                default: true
            },
        },
        computed: {
            // 左边竖条的样式
            lineStyle() {
                // 由于安卓和iOS的,需要稍微调整绝对定位的top值,才能让左边的竖线和右边的文字垂直居中
                return {
                    // 由于竖线为字体图标,具有比实际线宽更宽的宽度,所以也需要根据字体打下动态调整
                    left: -(Number(this.fontSize) * 0.9) + 'rpx',
                    top: -(Number(this.fontSize) * (this.$u.os() == 'ios' ? 0.14 : 0.15)) + 'rpx',
                }
            }
        },
        methods: {
            rightClick() {
                this.$emit('click');
            }
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .u-section {
        @include vue-flex;
        justify-content: space-between;
        align-items: center;
        width: 100%;
        &__title {
            position: relative;
            font-size: 28rpx;
            padding-left: 20rpx;
            @include vue-flex;
            align-items: center;
            &__icon-wrap {
                position: absolute;
            }
            &__text {
                line-height: 1;
            }
        }
        &__right-info {
            color: $u-tips-color;
            font-size: 26rpx;
            @include vue-flex;
            align-items: center;
            &__icon-arrow {
                margin-left: 6rpx;
            }
        }
    }
</style>
uview-ui/components/u-select/u-select.vue
New file
@@ -0,0 +1,417 @@
<template>
    <view class="u-select">
        <!-- <view class="u-select__action" :class="{
            'u-select--border': border
        }" @tap.stop="selectHandler">
            <view class="u-select__action__icon" :class="{
                'u-select__action__icon--reverse': value == true
            }">
                <u-icon name="arrow-down-fill" size="26" color="#c0c4cc"></u-icon>
            </view>
        </view> -->
        <u-popup :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto" :safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex">
            <view class="u-select">
                <view class="u-select__header" @touchmove.stop.prevent="">
                    <view
                        class="u-select__header__cancel u-select__header__btn"
                        :style="{ color: cancelColor }"
                        hover-class="u-hover-class"
                        :hover-stay-time="150"
                        @tap="getResult('cancel')"
                    >
                        {{cancelText}}
                    </view>
                    <view class="u-select__header__title">
                        {{title}}
                    </view>
                    <view
                        class="u-select__header__confirm u-select__header__btn"
                        :style="{ color: moving ? cancelColor : confirmColor }"
                        hover-class="u-hover-class"
                        :hover-stay-time="150"
                        @touchmove.stop=""
                        @tap.stop="getResult('confirm')"
                    >
                        {{confirmText}}
                    </view>
                </view>
                <view class="u-select__body">
                    <picker-view @change="columnChange" class="u-select__body__picker-view" :value="defaultSelector" @pickstart="pickstart" @pickend="pickend">
                        <picker-view-column v-for="(item, index) in columnData" :key="index">
                            <view class="u-select__body__picker-view__item" v-for="(item1, index1) in item" :key="index1">
                                <view class="u-line-1">{{ item1[labelName] }}</view>
                            </view>
                        </picker-view-column>
                    </picker-view>
                </view>
            </view>
        </u-popup>
    </view>
</template>
<script>
    /**
     * select 列选择器
     * @description 此选择器用于单列,多列,多列联动的选择场景。(从1.3.0版本起,不建议使用Picker组件的单列和多列模式,Select组件是专门为列选择而构造的组件,更简单易用。)
     * @tutorial http://uviewui.com/components/select.html
     * @property {String} mode 模式选择,"single-column"-单列模式,"mutil-column"-多列模式,"mutil-column-auto"-多列联动模式
     * @property {Array} list 列数据,数组形式,见官网说明
     * @property {Boolean} v-model 布尔值变量,用于控制选择器的弹出与收起
     * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
     * @property {String} cancel-color 取消按钮的颜色(默认#606266)
     * @property {String} confirm-color 确认按钮的颜色(默认#2979ff)
     * @property {String} confirm-text 确认按钮的文字
     * @property {String} cancel-text 取消按钮的文字
     * @property {String} default-value 提供的默认选中的下标,见官网说明
     * @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭Picker(默认true)
     * @property {String Number} z-index 弹出时的z-index值(默认10075)
     * @property {String} value-name 自定义list数据的value属性名 1.3.6
     * @property {String} label-name 自定义list数据的label属性名 1.3.6
     * @property {String} child-name 自定义list数据的children属性名,只对多列联动模式有效 1.3.7
     * @event {Function} confirm 点击确定按钮,返回当前选择的值
     * @example <u-select v-model="show" :list="list"></u-select>
     */
export default {
    props: {
        // 列数据
        list: {
            type: Array,
            default() {
                return [];
            }
        },
        // 是否显示边框
        border: {
            type: Boolean,
            default: true
        },
        // 通过双向绑定控制组件的弹出与收起
        value: {
            type: Boolean,
            default: false
        },
        // "取消"按钮的颜色
        cancelColor: {
            type: String,
            default: '#606266'
        },
        // "确定"按钮的颜色
        confirmColor: {
            type: String,
            default: '#2979ff'
        },
        // 弹出的z-index值
        zIndex: {
            type: [String, Number],
            default: 0
        },
        safeAreaInsetBottom: {
            type: Boolean,
            default: false
        },
        // 是否允许通过点击遮罩关闭Picker
        maskCloseAble: {
            type: Boolean,
            default: true
        },
        // 提供的默认选中的下标
        defaultValue: {
            type: Array,
            default() {
                return [0];
            }
        },
        // 模式选择,single-column-单列,mutil-column-多列,mutil-column-auto-多列联动
        mode: {
            type: String,
            default: 'single-column'
        },
        // 自定义value属性名
        valueName: {
            type: String,
            default: 'value'
        },
        // 自定义label属性名
        labelName: {
            type: String,
            default: 'label'
        },
        // 自定义多列联动模式的children属性名
        childName: {
            type: String,
            default: 'children'
        },
        // 顶部标题
        title: {
            type: String,
            default: ''
        },
        // 取消按钮的文字
        cancelText: {
            type: String,
            default: '取消'
        },
        // 确认按钮的文字
        confirmText: {
            type: String,
            default: '确认'
        }
    },
    data() {
        return {
            // 用于列改变时,保存当前的索引,下一次变化时比较得出是哪一列发生了变化
            defaultSelector: [0],
            // picker-view的数据
            columnData: [],
            // 每次队列发生变化时,保存选择的结果
            selectValue: [],
            // 上一次列变化时的index
            lastSelectIndex: [],
            // 列数
            columnNum: 0,
            // 列是否还在滑动中,微信小程序如果在滑动中就点确定,结果可能不准确
            moving: false
        };
    },
    watch: {
        // 在select弹起的时候,重新初始化所有数据
        value: {
            immediate: true,
            handler(val) {
                if(val) setTimeout(() => this.init(), 10);
            }
        },
    },
    computed: {
        uZIndex() {
            // 如果用户有传递z-index值,优先使用
            return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
        },
    },
    methods: {
        // 标识滑动开始,只有微信小程序才有这样的事件
        pickstart() {
            // #ifdef MP-WEIXIN
            this.moving = true;
            // #endif
        },
        // 标识滑动结束
        pickend() {
            // #ifdef MP-WEIXIN
            this.moving = false;
            // #endif
        },
        init() {
            this.setColumnNum();
            this.setDefaultSelector();
            this.setColumnData();
            this.setSelectValue();
        },
        // 获取默认选中列下标
        setDefaultSelector() {
            // 如果没有传入默认选中的值,生成长度为columnNum,用0填充的数组
            this.defaultSelector = this.defaultValue.length == this.columnNum ? this.defaultValue : Array(this.columnNum).fill(0);
            this.lastSelectIndex = this.$u.deepClone(this.defaultSelector);
        },
        // 计算列数
        setColumnNum() {
            // 单列的列数为1
            if(this.mode == 'single-column') this.columnNum = 1;
            // 多列时,this.list数组长度就是列数
            else if(this.mode == 'mutil-column') this.columnNum = this.list.length;
            // 多列联动时,通过历遍this.list的第一个元素,得出有多少列
            else if(this.mode == 'mutil-column-auto') {
                let num = 1;
                let column = this.list;
                // 只要有元素并且第一个元素有children属性,继续历遍
                while(column[0][this.childName]) {
                    column = column[0] ? column[0][this.childName] : {};
                    num ++;
                }
                this.columnNum = num;
            }
        },
        // 获取需要展示在picker中的列数据
        setColumnData() {
            let data = [];
            this.selectValue = [];
            if(this.mode == 'mutil-column-auto') {
                // 获得所有数据中的第一个元素
                let column = this.list[this.defaultSelector.length ? this.defaultSelector[0] : 0];
                // 通过循环所有的列数,再根据设定列的数组,得出当前需要渲染的整个列数组
                for (let i = 0; i < this.columnNum; i++) {
                    // 第一列默认为整个list数组
                    if (i == 0) {
                        data[i] = this.list;
                        column = column[this.childName];
                    } else {
                        // 大于第一列时,判断是否有默认选中的,如果没有就用该列的第一项
                        data[i] = column;
                        column = column[this.defaultSelector[i]][this.childName];
                    }
                }
            } else if(this.mode == 'single-column') {
                data[0] = this.list;
            } else {
                data = this.list;
            }
            this.columnData = data;
        },
        // 获取默认选中的值,如果没有设置defaultValue,就默认选中每列的第一个
        setSelectValue() {
            let tmp = null;
            for(let i = 0; i < this.columnNum; i++) {
                tmp = this.columnData[i][this.defaultSelector[i]];
                let data = {
                    value: tmp ? tmp[this.valueName] : null,
                    label: tmp ? tmp[this.labelName] : null
                };
                // 判断是否存在额外的参数,如果存在,就返回
                if(tmp && tmp.extra) data.extra = tmp.extra;
                this.selectValue.push(data)
            }
        },
        // 列选项
        columnChange(e) {
            let index = null;
            let columnIndex = e.detail.value;
            // 由于后面是需要push进数组的,所以需要先清空数组
            this.selectValue = [];
            if(this.mode == 'mutil-column-auto') {
                // 对比前后两个数组,寻找变更的是哪一列,如果某一个元素不同,即可判定该列发生了变化
                this.lastSelectIndex.map((val, idx) => {
                    if (val != columnIndex[idx]) index = idx;
                });
                this.defaultSelector = columnIndex;
                for (let i = index + 1; i < this.columnNum; i++) {
                    // 当前变化列的下一列的数据,需要获取上一列的数据,同时需要指定是上一列的第几个的children,再往后的
                    // 默认是队列的第一个为默认选项
                    this.columnData[i] = this.columnData[i - 1][i - 1 == index ? columnIndex[index] : 0][this.childName];
                    // 改变的列之后的所有列,默认选中第一个
                    this.defaultSelector[i] = 0;
                }
                // 在历遍的过程中,可能由于上一步修改this.columnData,导致产生连锁反应,程序触发columnChange,会有多次调用
                // 只有在最后一次数据稳定后的结果是正确的,此前的历遍中,可能会产生undefined,故需要判断
                columnIndex.map((item, index) => {
                    let data = this.columnData[index][columnIndex[index]];
                    let tmp = {
                        value: data ? data[this.valueName] : null,
                        label: data ? data[this.labelName] : null,
                    };
                    // 判断是否有需要额外携带的参数
                    if(data && data.extra !== undefined) tmp.extra = data.extra;
                    this.selectValue.push(tmp);
                })
                // 保存这一次的结果,用于下次列发生变化时作比较
                this.lastSelectIndex = columnIndex;
            } else if(this.mode == 'single-column') {
                let data = this.columnData[0][columnIndex[0]];
                // 初始默认选中值
                let tmp = {
                    value: data ? data[this.valueName] : null,
                    label: data ? data[this.labelName] : null,
                };
                // 判断是否有需要额外携带的参数
                if(data && data.extra !== undefined) tmp.extra = data.extra;
                this.selectValue.push(tmp);
            } else if(this.mode == 'mutil-column') {
                // 初始默认选中值
                columnIndex.map((item, index) => {
                    let data = this.columnData[index][columnIndex[index]];
                    // 初始默认选中值
                    let tmp = {
                        value: data ? data[this.valueName] : null,
                        label: data ? data[this.labelName] : null,
                    };
                    // 判断是否有需要额外携带的参数
                    if(data && data.extra !== undefined) tmp.extra = data.extra;
                    this.selectValue.push(tmp);
                })
            }
        },
        close() {
            this.$emit('input', false);
        },
        // 点击确定或者取消
        getResult(event = null) {
            // #ifdef MP-WEIXIN
            if (this.moving) return;
            // #endif
            if (event) this.$emit(event, this.selectValue);
            this.close();
        },
        selectHandler() {
            this.$emit('click');
        }
    }
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-select {
    &__action {
        position: relative;
        line-height: $u-form-item-height;
        height: $u-form-item-height;
        &__icon {
            position: absolute;
            right: 20rpx;
            top: 50%;
            transition: transform .4s;
            transform: translateY(-50%);
            z-index: 1;
            &--reverse {
                transform: rotate(-180deg) translateY(50%);
            }
        }
    }
    &__hader {
        &__title {
            color: $u-content-color;
        }
    }
    &--border {
        border-radius: 6rpx;
        border-radius: 4px;
        border: 1px solid $u-form-item-border-color;
    }
    &__header {
        @include vue-flex;
        align-items: center;
        justify-content: space-between;
        height: 80rpx;
        padding: 0 40rpx;
    }
    &__body {
        width: 100%;
        height: 500rpx;
        overflow: hidden;
        background-color: #fff;
        &__picker-view {
            height: 100%;
            box-sizing: border-box;
            &__item {
                @include vue-flex;
                align-items: center;
                justify-content: center;
                font-size: 32rpx;
                color: $u-main-color;
                padding: 0 8rpx;
            }
        }
    }
}
</style>
uview-ui/components/u-skeleton/u-skeleton.vue
New file
@@ -0,0 +1,199 @@
<template>
    <view v-if="loading" :style="{
        width: windowWinth + 'px',
        height: windowHeight + 'px',
        backgroundColor: bgColor,
        position: 'absolute',
        left: left + 'px',
        top: top + 'px',
        zIndex: 9998,
        overflow: 'hidden'
    }"
     @touchmove.stop.prevent>
        <view v-for="(item, index) in RectNodes" :key="$u.guid()" :class="[animation ? 'skeleton-fade' : '']" :style="{
            width: item.width + 'px',
            height: item.height + 'px',
            backgroundColor: elColor,
            position: 'absolute',
            left: (item.left - left) + 'px',
            top: (item.top - top) + 'px'
        }"></view>
        <view v-for="(item, index) in circleNodes" :key="$u.guid()" :class="animation ? 'skeleton-fade' : ''" :style="{
            width: item.width + 'px',
            height: item.height + 'px',
            backgroundColor: elColor,
            borderRadius: item.width/2 + 'px',
            position: 'absolute',
            left: (item.left - left) + 'px',
            top: (item.top - top) + 'px'
        }"></view>
        <view v-for="(item, index) in filletNodes" :key="$u.guid()" :class="animation ? 'skeleton-fade' : ''" :style="{
            width: item.width + 'px',
            height: item.height + 'px',
            backgroundColor: elColor,
            borderRadius: borderRadius + 'rpx',
            position: 'absolute',
            left: (item.left - left) + 'px',
            top: (item.top - top) + 'px'
        }"></view>
    </view>
</template>
<script>
    /**
     * skeleton 骨架屏
     * @description 骨架屏一般用于页面在请求远程数据尚未完成时,页面用灰色块预显示本来的页面结构,给用户更好的体验。
     * @tutorial https://www.uviewui.com/components/skeleton.html
     * @property {String} el-color 骨架块状元素的背景颜色(默认#e5e5e5)
     * @property {String} bg-color 骨架组件背景颜色(默认#ffffff)
     * @property {Boolean} animation 骨架块是否显示动画效果(默认false)
     * @property {String Number} border-radius u-skeleton-fillet类名元素,对应的骨架块的圆角大小,单位rpx(默认10)
     * @property {Boolean} loading 是否显示骨架组件,请求完成后,将此值设置为false(默认true)
     * @example <u-skeleton :loading="true" :animation="true"></u-skeleton>
     */
    export default {
        name: "u-skeleton",
        props: {
            // 需要渲染的元素背景颜色,十六进制或者rgb等都可以
            elColor: {
                type: String,
                default: '#e5e5e5'
            },
            // 整个骨架屏页面的背景颜色
            bgColor: {
                type: String,
                default: '#ffffff'
            },
            // 是否显示加载动画
            animation: {
                type: Boolean,
                default: false
            },
            // 圆角值,只对类名为u-skeleton-fillet的元素生效,为数值,不带单位
            borderRadius: {
                type: [String, Number],
                default: "10"
            },
            // 是否显示骨架,true-显示,false-隐藏
            loading: {
                type: Boolean,
                default: true
            }
        },
        data() {
            return {
                windowWinth: 750, // 骨架屏宽度
                windowHeight: 1500, // 骨架屏高度
                filletNodes: [], // 圆角元素
                circleNodes: [], // 圆形元素
                RectNodes: [], // 矩形元素
                top: 0,
                left: 0,
            }
        },
        methods: {
            // 查询各节点的信息
            selecterQueryInfo() {
                // 获取整个父组件容器的高度,当做骨架屏的高度
                // 在微信小程序中,如果把骨架屏放入组件中使用的话,需要调in(this)上下文为父组件才有效
                let query = '';
                // #ifdef MP-WEIXIN
                query = uni.createSelectorQuery().in(this.$parent);
                // #endif
                // #ifndef MP-WEIXIN
                query = uni.createSelectorQuery()
                // #endif
                query.selectAll('.u-skeleton').boundingClientRect().exec((res) => {
                    this.windowHeight = res[0][0].height;
                    this.windowWinth = res[0][0].width;
                    this.top = res[0][0].bottom - res[0][0].height;
                    this.left = res[0][0].left;
                });
                // 矩形骨架元素
                this.getRectEls();
                // 圆形骨架元素
                this.getCircleEls();
                // 圆角骨架元素
                this.getFilletEls();
            },
            // 矩形元素列表
            getRectEls() {
                let query = '';
                // 在微信小程序中,如果把骨架屏放入组件中使用的话,需要调in(this)上下文为父组件才有效
                // #ifdef MP-WEIXIN
                query = uni.createSelectorQuery().in(this.$parent);
                // #endif
                // #ifndef MP-WEIXIN
                query = uni.createSelectorQuery()
                // #endif
                query.selectAll('.u-skeleton-rect').boundingClientRect().exec((res) => {
                    this.RectNodes = res[0];
                });
            },
            // 圆角元素列表
            getFilletEls() {
                let query = '';
                // 在微信小程序中,如果把骨架屏放入组件中使用的话,需要调in(this)上下文为父组件才有效
                // #ifdef MP-WEIXIN
                query = uni.createSelectorQuery().in(this.$parent);
                // #endif
                // #ifndef MP-WEIXIN
                query = uni.createSelectorQuery()
                // #endif
                query.selectAll('.u-skeleton-fillet').boundingClientRect().exec((res) => {
                    this.filletNodes = res[0];
                });
            },
            // 圆形元素列表
            getCircleEls() {
                let query = '';
                // 在微信小程序中,如果把骨架屏放入组件中使用的话,需要调in(this)上下文为父组件才有效
                // #ifdef MP-WEIXIN
                query = uni.createSelectorQuery().in(this.$parent);
                // #endif
                // #ifndef MP-WEIXIN
                query = uni.createSelectorQuery()
                // #endif
                query.selectAll('.u-skeleton-circle').boundingClientRect().exec((res) => {
                    this.circleNodes = res[0];
                });
            }
        },
        // 组件被挂载
        mounted() {
            // 获取系统信息
            let systemInfo = uni.getSystemInfoSync();
            this.windowHeight = systemInfo.windowHeight;
            this.windowWinth = systemInfo.windowWidth;
            this.selecterQueryInfo();
        }
    }
</script>
<style lang="scss" scoped>
    @import "../../libs/css/style.components.scss";
    .skeleton-fade {
        width: 100%;
        height: 100%;
        background: rgb(194, 207, 214);
        animation-duration: 1.5s;
        animation-name: blink;
        animation-timing-function: ease-in-out;
        animation-iteration-count: infinite;
    }
    @keyframes blink {
        0% {
            opacity: 1;
        }
        50% {
            opacity: 0.4;
        }
        100% {
            opacity: 1;
        }
    }
</style>
Diff truncated after the above file
uview-ui/components/u-slider/u-slider.vue uview-ui/components/u-steps/u-steps.vue uview-ui/components/u-sticky/u-sticky.vue uview-ui/components/u-subsection/u-subsection.vue uview-ui/components/u-swipe-action/u-swipe-action.vue uview-ui/components/u-swiper/u-swiper.vue uview-ui/components/u-switch/u-switch.vue uview-ui/components/u-tabbar/u-tabbar.vue uview-ui/components/u-table/u-table.vue uview-ui/components/u-tabs-swiper/u-tabs-swiper.vue uview-ui/components/u-tabs/u-tabs.vue uview-ui/components/u-tag/u-tag.vue uview-ui/components/u-td/u-td.vue uview-ui/components/u-th/u-th.vue uview-ui/components/u-time-line-item/u-time-line-item.vue uview-ui/components/u-time-line/u-time-line.vue uview-ui/components/u-toast/u-toast.vue uview-ui/components/u-top-tips/u-top-tips.vue uview-ui/components/u-tr/u-tr.vue uview-ui/components/u-upload/u-upload.vue uview-ui/components/u-verification-code/u-verification-code.vue uview-ui/components/u-waterfall/u-waterfall.vue uview-ui/iconfont.css uview-ui/index.js uview-ui/index.scss uview-ui/libs/config/config.js uview-ui/libs/config/zIndex.js uview-ui/libs/css/color.scss uview-ui/libs/css/common.scss uview-ui/libs/css/style.components.scss uview-ui/libs/css/style.h5.scss uview-ui/libs/css/style.mp.scss uview-ui/libs/css/style.nvue.scss uview-ui/libs/css/style.vue.scss uview-ui/libs/function/$parent.js uview-ui/libs/function/addUnit.js uview-ui/libs/function/bem.js uview-ui/libs/function/color.js uview-ui/libs/function/colorGradient.js uview-ui/libs/function/debounce.js uview-ui/libs/function/deepClone.js uview-ui/libs/function/deepMerge.js uview-ui/libs/function/getParent.js uview-ui/libs/function/guid.js uview-ui/libs/function/md5.js uview-ui/libs/function/queryParams.js uview-ui/libs/function/random.js uview-ui/libs/function/randomArray.js uview-ui/libs/function/route.js uview-ui/libs/function/sys.js uview-ui/libs/function/test.js uview-ui/libs/function/throttle.js uview-ui/libs/function/timeFormat.js uview-ui/libs/function/timeFrom.js uview-ui/libs/function/toast.js uview-ui/libs/function/trim.js uview-ui/libs/function/type2icon.js uview-ui/libs/mixin/mixin.js uview-ui/libs/mixin/mpShare.js uview-ui/libs/request/index.js uview-ui/libs/store/index.js uview-ui/libs/util/area.js uview-ui/libs/util/async-validator.js uview-ui/libs/util/city.js uview-ui/libs/util/emitter.js uview-ui/libs/util/province.js uview-ui/package.json uview-ui/theme.scss