分支自 SuZhouGuanHong/TaiYuanTaiZhong

陈勇
2024-03-12 388c43ee7b741eebafa98cd8ef992005f1aa56f4
PDA源程序

PDA前端
已添加403个文件
75909 ■■■■■ 文件已修改
代码管理/PDA/.eslintignore 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/App.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/LICENSE 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/common/http.interceptor.js 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/common/uni-ui.scss 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/js_sdk/luch-request/luch-request/adapters/index.js 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/js_sdk/luch-request/luch-request/core/InterceptorManager.js 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/js_sdk/luch-request/luch-request/core/Request.js 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/js_sdk/luch-request/luch-request/core/buildFullPath.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/js_sdk/luch-request/luch-request/core/defaults.js 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/js_sdk/luch-request/luch-request/core/dispatchRequest.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/js_sdk/luch-request/luch-request/core/mergeConfig.js 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/js_sdk/luch-request/luch-request/core/settle.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/js_sdk/luch-request/luch-request/helpers/buildURL.js 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/js_sdk/luch-request/luch-request/helpers/combineURLs.js 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/js_sdk/luch-request/luch-request/helpers/isAbsoluteURL.js 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/js_sdk/luch-request/luch-request/index.d.ts 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/js_sdk/luch-request/luch-request/index.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/js_sdk/luch-request/luch-request/utils.js 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/js_sdk/luch-request/luch-request/utils/clone.js 264 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/main.js 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/pages/AGV/AGV.vue 370 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/pages/feeding/feeding.vue 347 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/pages/home/home.vue 269 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/pages/index/index.vue 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/pages/login/login.vue 439 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/pages/unpacking/unpacking.vue 439 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/static/center-selected.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/static/center.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/static/favicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/static/index-selected.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/static/index.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/static/login_bottom_bg.jpg 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/static/login_top2.jpg 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/static/login_top3.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/static/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/static/myIcon/1.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/static/myIcon/2.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/static/myIcon/qiu.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/template.h5.html 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/README.md 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-action-sheet/tn-action-sheet.vue 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-avatar-group/tn-avatar-group.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-avatar/tn-avatar.vue 298 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-badge/tn-badge.vue 173 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-button/tn-button.vue 302 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-calendar/tn-calendar.vue 707 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-car-keyboard/tn-car-keyboard.vue 320 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-cascade-selection/tn-cascade-selection.vue 654 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-checkbox-group/tn-checkbox-group.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-checkbox/tn-checkbox.vue 328 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-circle-progress/tn-circle-progress.vue 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-collapse-item/tn-collapse-item.vue 236 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-collapse/tn-collapse.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-color-icon/tn-color-icon.vue 318 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-column-notice/tn-column-notice.vue 251 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-count-down/tn-count-down.vue 314 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-count-scroll/tn-count-scroll.vue 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-count-to/tn-count-to.vue 231 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-cropper/index.wxs 328 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-cropper/tn-cropper.vue 570 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-custom-swiper-item/index.wxs 288 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-custom-swiper-item/tn-custom-swiper-item.vue 277 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-custom-swiper/tn-custom-swiper.vue 535 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-drag/index.wxs 265 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-drag/tn-drag.vue 278 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-empty/tn-empty.vue 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-fab/tn-fab.vue 523 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-form-item/tn-form-item.vue 457 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-form/tn-form.vue 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-goods-nav/tn-goods-nav.vue 382 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-grid-item/tn-grid-item.vue 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-grid/tn-grid.vue 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-image-upload-drag/tn-image-upload-drag.vue 995 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-image-upload/tn-image-upload.vue 645 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-index-anchor/tn-index-anchor.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-index-list/tn-index-list.vue 361 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-input/tn-input.vue 427 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-keyboard/tn-keyboard.vue 220 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-landscape/tn-landscape.vue 225 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-lazy-load/tn-lazy-load.vue 254 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-line-progress/tn-line-progress.vue 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-list-cell/tn-list-cell.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-list-view/tn-list-view.vue 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-load-more/tn-load-more.vue 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-loading/tn-loading.vue 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-modal/tn-modal.vue 246 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-nav-bar/tn-nav-bar.vue 355 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-notice-bar/tn-notice-bar.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-number-box/tn-number-box.vue 401 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-number-keyboard/tn-number-keyboard.vue 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-picker/tn-picker.vue 723 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-popup/tn-popup.vue 491 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-radio-group/tn-radio-group.vue 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-radio/tn-radio.vue 265 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-rate/tn-rate.vue 334 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-read-more/tn-read-more.vue 222 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-row-notice/tn-row-notice.vue 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-scroll-list/tn-scroll-list.vue 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-scroll-view/tn-scroll-view.vue 401 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-select/tn-select.vue 380 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-sign-board/tn-sign-board.vue 690 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-skeleton/tn-skeleton.vue 254 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-slider/tn-slider.vue 255 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-stack-swiper/index-h5.wxs 657 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-stack-swiper/index.wxs 657 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-stack-swiper/tn-stack-swiper.vue 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-steps/tn-steps.vue 346 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-sticky/tn-sticky.vue 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-subsection/tn-subsection.vue 410 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-swipe-action-item/index.wxs 230 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-swipe-action-item/tn-swipe-action-item.vue 236 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-swipe-action/tn-swipe-action.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-swiper/tn-swiper.vue 364 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-switch/tn-switch.vue 241 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-tabbar/tn-tabbar.vue 576 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-table/tn-table.vue 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-tabs-swiper/tn-tabs-swiper.vue 444 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-tabs/tn-tabs.vue 340 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-tag/tn-tag.vue 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-td/tn-td.vue 307 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-time-line-item/tn-time-line-item.vue 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-time-line-item/tn-time-line-item.vue_bk 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-time-line/tn-time-line.vue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-time-line/tn-time-line.vue_bk 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-tips/tn-tips.vue 240 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-toast/tn-toast.vue 227 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-tr/tn-tr.vue 210 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-tree-node/tn-tree-node.vue 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-tree-view/tn-tree-view.vue 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-verification-code-input/tn-verification-code-input.vue 324 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-verification-code/tn-verification-code.vue 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/components/tn-waterfall/tn-waterfall.vue 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/iconfont.css 1645 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/index.js 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/index.scss 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/config/color.js 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/config/zIndex.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/css/color.scss 563 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/css/main.scss 722 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/css/style.h5.scss 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/css/style.mp.scss 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/function/$parent.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/function/array.js 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/function/color.js 270 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/function/colorUtils.js 270 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/function/deepClone.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/function/message.js 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/function/messageUtils.js 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/function/number.js 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/function/string.js 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/function/test.js 232 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/function/updateCustomBarInfo.js 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/function/uuid.js 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/mixin/components_color.js 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/mixin/mixin.js 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/mixin/mpShare.js 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/mixin/touch.js 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/utils/area.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/utils/async-validator.js 1356 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/utils/calendar.js 546 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/utils/city.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/utils/emitter.js 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/libs/utils/province.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/tuniao-ui/theme.scss 183 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uni.scss 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/apk/__UNI__3B0DA10_cm.apk 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/apk/__UNI__4120374_cm.apk 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/apk/__UNI__F8536D6_cm.apk 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/apk/apkurl 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/certdata 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/cloudcertificate/certini 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/cloudcertificate/package.keystore 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappchooselocation.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniapperror.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappes6.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappopenlocation.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniapppicker.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappquill.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappquillimageresize.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappscan.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappsuccess.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappview.html 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/app-config-service.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/app-config.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/app-service.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/app-view.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/center-selected.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/center.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/favicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/index-selected.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/index.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/login_bottom_bg.jpg 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/login_top2.jpg 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/login_top3.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/myIcon/1.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/myIcon/2.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/myIcon/qiu.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/view.css 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/view.umd.min.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/__uniappchooselocation.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/__uniapperror.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/__uniappes6.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/__uniappopenlocation.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/__uniapppicker.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/__uniappquill.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/__uniappquillimageresize.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/__uniappscan.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/__uniappsuccess.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/__uniappview.html 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/app-config-service.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/app-config.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/app-service.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/app-view.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/static/center-selected.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/static/center.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/static/favicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/static/index-selected.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/static/index.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/static/login_bottom_bg.jpg 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/static/login_top2.jpg 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/static/login_top3.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/static/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/static/myIcon/1.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/static/myIcon/2.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/static/myIcon/qiu.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/view.css 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/build/app-plus/view.umd.min.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/__uniappchooselocation.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/__uniapperror.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/__uniappes6.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/__uniappopenlocation.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/__uniapppicker.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/__uniappquill.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/__uniappquillimageresize.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/__uniappscan.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/__uniappsuccess.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/__uniappview.html 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/app-config-service.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/app-config.js 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/app-service.js 4756 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/app-view.js 4841 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/static/center-selected.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/static/center.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/static/favicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/static/index-selected.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/static/index.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/static/login_bottom_bg.jpg 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/static/login_top2.jpg 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/static/login_top3.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/static/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/view.css 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/dist/dev/app-plus/view.umd.min.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/release/apk/__UNI__3B0DA10__20240304182646.apk 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/release/apk/__UNI__3B0DA10__20240312142901.apk 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/uni_modules/uni-icons/changelog.md 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/uni_modules/uni-icons/components/uni-icons/icons.js 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/uni_modules/uni-icons/components/uni-icons/uni-icons.vue 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/uni_modules/uni-icons/components/uni-icons/uni.ttf 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/unpackage/uni_modules/uni-icons/readme.md 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/LICENSE 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/README.md 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-action-sheet/u-action-sheet.vue 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-alert-tips/u-alert-tips.vue 256 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-avatar-cropper/u-avatar-cropper.vue 290 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-avatar-cropper/weCropper.js 1265 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-avatar/u-avatar.vue 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-back-top/u-back-top.vue 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-badge/u-badge.vue 216 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-button/u-button.vue 596 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-calendar/u-calendar.vue 639 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-car-keyboard/u-car-keyboard.vue 257 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-card/u-card.vue 299 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-cell-group/u-cell-group.vue 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-cell-item/u-cell-item.vue 316 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-checkbox-group/u-checkbox-group.vue 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-checkbox/u-checkbox.vue 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-circle-progress/u-circle-progress.vue 220 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-col/u-col.vue 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-collapse-item/u-collapse-item.vue 204 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-collapse/u-collapse.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-column-notice/u-column-notice.vue 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-count-down/u-count-down.vue 318 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-count-to/u-count-to.vue 241 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-divider/u-divider.vue 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-dropdown-item/u-dropdown-item.vue 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-dropdown/u-dropdown.vue 298 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-empty/u-empty.vue 193 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-field/u-field.vue 384 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-form-item/u-form-item.vue 431 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-form/u-form.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-full-screen/u-full-screen.vue 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-gap/u-gap.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-grid-item/u-grid-item.vue 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-grid/u-grid.vue 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-icon/u-icon.vue 336 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-image/u-image.vue 266 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-index-anchor/u-index-anchor.vue 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-index-list/u-index-list.vue 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-input/u-input.vue 387 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-keyboard/u-keyboard.vue 217 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-lazy-load/u-lazy-load.vue 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-line-progress/u-line-progress.vue 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-line/u-line.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-link/u-link.vue 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-loading-page/u-loading-page.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-loading/u-loading.vue 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-loadmore/u-loadmore.vue 203 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-mask/u-mask.vue 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-message-input/u-message-input.vue 311 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-modal/u-modal.vue 283 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-navbar/u-navbar.vue 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-no-network/u-no-network.vue 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-notice-bar/u-notice-bar.vue 272 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-number-box/u-number-box.vue 363 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-number-keyboard/u-number-keyboard.vue 158 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-parse/libs/CssHandler.js 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-parse/libs/MpHtmlParser.js 580 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-parse/libs/config.js 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-parse/libs/handler.wxs 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-parse/libs/trees.vue 505 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-parse/u-parse.vue 645 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-picker/u-picker.vue 676 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-popup/u-popup.vue 456 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-radio-group/u-radio-group.vue 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-radio/u-radio.vue 271 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-rate/u-rate.vue 275 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-read-more/u-read-more.vue 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-row-notice/u-row-notice.vue 269 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-row/u-row.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-search/u-search.vue 342 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-section/u-section.vue 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-select/u-select.vue 417 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-skeleton/u-skeleton.vue 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-slider/u-slider.vue 257 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-steps/u-steps.vue 200 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-sticky/u-sticky.vue 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-subsection/u-subsection.vue 355 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-swipe-action/u-swipe-action.vue 255 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-swiper/u-swiper.vue 340 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-switch/u-switch.vue 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-tabbar/u-tabbar.vue 330 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-table/u-table.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-tabs-swiper/u-tabs-swiper.vue 488 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-tabs/u-tabs.vue 368 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-tag/u-tag.vue 294 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-td/u-td.vue 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-th/u-th.vue 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-time-line-item/u-time-line-item.vue 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-time-line/u-time-line.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-toast/u-toast.vue 220 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-top-tips/u-top-tips.vue 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-tr/u-tr.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-upload/u-upload.vue 654 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-verification-code/u-verification-code.vue 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/components/u-waterfall/u-waterfall.vue 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/iconfont.css 910 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/index.js 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/index.scss 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/config/config.js 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/config/zIndex.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/css/color.scss 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/css/common.scss 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/css/style.components.scss 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/css/style.h5.scss 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/css/style.mp.scss 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/css/style.nvue.scss 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/css/style.vue.scss 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/$parent.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/addUnit.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/bem.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/color.js 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/colorGradient.js 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/debounce.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/deepClone.js 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/deepMerge.js 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/getParent.js 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/guid.js 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/md5.js 385 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/queryParams.js 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/random.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/randomArray.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/route.js 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/sys.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/test.js 232 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/throttle.js 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/timeFormat.js 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/timeFrom.js 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/toast.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/trim.js 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/function/type2icon.js 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/mixin/mixin.js 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/mixin/mpShare.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/request/index.js 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/store/index.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/util/area.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/util/async-validator.js 1356 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/util/city.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/util/emitter.js 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/libs/util/province.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/uview-ui/theme.scss 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
代码管理/PDA/vue.config.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
´úÂë¹ÜÀí/PDA/.eslintignore
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
unpackage
node_modules
uview-ui
´úÂë¹ÜÀí/PDA/App.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
<script>
    export default {
        onLaunch: function() {
            console.log('App Launch')
        },
        onShow: function() {
            console.log('App Show')
        },
        onHide: function() {
            console.log('App Hide')
        }
    }
</script>
<style lang="scss">
    /* æ³¨æ„è¦å†™åœ¨ç¬¬ä¸€è¡Œï¼ŒåŒæ—¶ç»™style标签加入lang="scss"属性 */
      @import './tuniao-ui/index.scss';
      @import './tuniao-ui/iconfont.css';
    @import "uview-ui/index.scss";
    /*每个页面公共css */
</style>
´úÂë¹ÜÀí/PDA/LICENSE
¶Ô±ÈÐÂÎļþ
@@ -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.
´úÂë¹ÜÀí/PDA/common/http.interceptor.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,121 @@
// let baseUrl = 'http://192.168.11.186:8098'
// let baseUrl = 'http://192.168.12.83:8098'
// let baseUrl = 'http://127.0.0.1:8098'
let baseUrl = 'http://192.168.12.101:8098'
const install = (Vue, vm) => {
    // æ­¤ä¸ºè‡ªå®šä¹‰é…ç½®å‚数,具体参数见上方说明
    Vue.prototype.$u.http.setConfig({
        baseUrl: baseUrl,
        loadingText: '努力加载中~',
        loadingTime: 5000,
        originalData: true,
        // ......
    });
    // è¯·æ±‚拦截,配置Token等参数
    Vue.prototype.$u.http.interceptor.request = (config) => {
        // å¼•用token
        // æ–¹å¼ä¸€ï¼Œå­˜æ”¾åœ¨vuex的token,假设使用了uView封装的vuex方式
        // è§ï¼šhttps://uviewui.com/components/globalVariable.html
        // config.header.token = vm.token;
        // æ–¹å¼äºŒï¼Œå¦‚果没有使用uView封装的vuex方法,那么需要使用$store.state获取
        // config.header.token = vm.$store.state.token;
        // æ–¹å¼ä¸‰ï¼Œå¦‚æžœtoken放在了globalData,通过getApp().globalData获取
        // config.header.token = getApp().globalData.username;
        // æ–¹å¼å››ï¼Œå¦‚æžœtoken放在了Storage本地存储中,拦截是每次请求都执行的
        // æ‰€ä»¥å“ªæ€•您重新登录修改了Storage,下一次的请求将会是最新值
        // const token = uni.getStorageSync('token');
        // config.header.token = token;
        config.header.Token = 'xxxxxx';
        config.header.Authorization = "Bearer " + uni.getStorageSync('jo_id_token');
        // å¯ä»¥å¯¹æŸä¸ªurl进行特别处理,此url参数为this.$u.get(url)中的url值
        if(config.url == '/api/User/login') config.header.noToken = true;
        // æœ€åŽéœ€è¦å°†config进行return
        return config;
        // å¦‚æžœreturn一个false值,则会取消本次请求
        // if(config.url == '/user/rest') return false; // å–消某次请求
    }
    // å“åº”拦截,判断状态码是否通过
    Vue.prototype.$u.http.interceptor.response = (res) => {
        if(res.statusCode == 200) {
            // res为服务端返回值,可能有code,result等字段
            // è¿™é‡Œå¯¹res.result进行返回,将会在this.$u.post(url).then(res => {})的then回调中的res的到
            // å¦‚果配置了originalData为true,请留意这里的返回值
            return res.data;
        } else if(res.statusCode == 401) {
            // å‡è®¾201为token失效,这里跳转登录
            vm.$u.toast('验证失败,请重新登录');
            setTimeout(() => {
                // æ­¤ä¸ºuView的方法,详见路由相关文档
                vm.$u.route('/pages/login/login')
            }, 1500)
            return false;
        } else if(res.statusCode == 202){
            // å¦‚果返回false,则会调用Promise的reject回调,
            // å¹¶å°†è¿›å…¥this.$u.post(url).then().catch(res=>{})的catch回调中,res为服务端的返回值
            vm.$u.post("/api/User/replaceToken").then(res=>{
                if (x.data.status) {
                    vm.$u.vuex('vuex_token',x.data.data)
                    vm.$u.route({
                        type: "navigateBack",
                        delta: -1
                    })
                } else {
                    console.log(x.data.message);
                    vm.$u.toast('验证过期,请重新登录');
                    setTimeout(() => {
                        // æ­¤ä¸ºuView的方法,详见路由相关文档
                        vm.$u.route('/pages/login/login')
                    }, 1500)
                }
            }).catch(err=>{
                uni.reLaunch({
                    url: '/pages/login/login'
                });
            })
            // uni.request({
            //     url: "http://192.168.12.245:8099/api/User/replaceToken",
            //     param: {},
            //     method: 'POST',
            //     responseType: "text",
            //     header: {
            //         Authorization: "Bearer " + vm.vuex_token
            //     },
            //     async: false,
            //     success: function(x) {
            //         if (x.data.status) {
            //             vm.$u.vuex('vuex_token',x.data.data)
            //             vm.$u.route({
            //                 type: "navigateBack",
            //                 delta: -1
            //             })
            //         } else {
            //             console.log(x.data.message);
            //             vm.$u.toast('验证过期,请重新登录');
            //             setTimeout(() => {
            //                 // æ­¤ä¸ºuView的方法,详见路由相关文档
            //                 vm.$u.route('/pages/user/login')
            //             }, 1500)
            //         }
            //     },
            //     errror: function(ex) {
            //         console.log(ex);
            //         uni.reLaunch({
            //             url: '/pages/user/login'
            //         });
            //     },
            // });
            return false;
        }
    }
}
export default {
    install,
    baseUrl
}
´úÂë¹ÜÀí/PDA/common/uni-ui.scss
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
.uni-flex {
    display: flex;
}
.uni-flex-row {
    @extend .uni-flex;
    flex-direction: row;
    box-sizing: border-box;
}
.uni-flex-column {
    @extend .uni-flex;
    flex-direction: column;
}
.uni-color-gary {
    color: #3b4144;
}
/* æ ‡é¢˜ */
.uni-title {
    display: flex;
    margin-bottom: $uni-spacing-col-base;
    font-size: $uni-font-size-lg;
    font-weight: bold;
    color: #3b4144;
}
.uni-title-sub {
    display: flex;
    // margin-bottom: $uni-spacing-col-base;
    font-size: $uni-font-size-base;
    font-weight: 500;
    color: #3b4144;
}
/* æè¿° é¢å¤–文本 */
.uni-note {
    margin-top: 10px;
    color: #999;
    font-size: $uni-font-size-sm;
}
/* åˆ—表内容 */
.uni-list-box {
    @extend .uni-flex-row;
    flex: 1;
    margin-top: 10px;
}
/* ç•¥ç¼©å›¾ */
.uni-thumb {
    flex-shrink: 0;
    margin-right: $uni-spacing-row-base;
    width: 125px;
    height: 75px;
    border-radius: $uni-border-radius-lg;
    overflow: hidden;
    border: 1px #f5f5f5 solid;
    image {
        width: 100%;
        height: 100%;
    }
}
.uni-media-box {
    @extend .uni-flex-row;
    // margin-bottom: $uni-spacing-col-base;
    border-radius: $uni-border-radius-lg;
    overflow: hidden;
    .uni-thumb {
        margin: 0;
        margin-left: 4px;
        flex-shrink: 1;
        width: 33%;
        border-radius:0;
        &:first-child {
            margin: 0;
        }
    }
}
/* å†…容 */
.uni-content {
    @extend .uni-flex-column;
    justify-content: space-between;
}
/* åˆ—表footer */
.uni-footer {
    @extend .uni-flex-row;
    justify-content: space-between;
    margin-top: $uni-spacing-col-lg;
}
.uni-footer-text {
    font-size: $uni-font-size-sm;
    color: $uni-text-color-grey;
    margin-left: 5px;
}
/* æ ‡ç­¾ */
.uni-tag {
    flex-shrink: 0;
    padding: 0 5px;
    border: 1px $uni-border-color solid;
    margin-right: $uni-spacing-row-sm;
    border-radius: $uni-border-radius-base;
    background: $uni-bg-color-grey;
    color: $uni-text-color;
    font-size: $uni-font-size-sm;
}
/* é“¾æŽ¥ */
.uni-link {
    margin-left: 10px;
    color: $uni-text-color;
    text-decoration: underline;
}
´úÂë¹ÜÀí/PDA/js_sdk/luch-request/luch-request/adapters/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,100 @@
import buildURL from '../helpers/buildURL'
import buildFullPath from '../core/buildFullPath'
import settle from '../core/settle'
import {isUndefined} from "../utils"
/**
 * è¿”回可选值存在的配置
 * @param {Array} keys - å¯é€‰å€¼æ•°ç»„
 * @param {Object} config2 - é…ç½®
 * @return {{}} - å­˜åœ¨çš„配置项
 */
const mergeKeys = (keys, config2) => {
  let config = {}
  keys.forEach(prop => {
    if (!isUndefined(config2[prop])) {
      config[prop] = config2[prop]
    }
  })
  return config
}
export default (config) => {
  return new Promise((resolve, reject) => {
    let fullPath = buildURL(buildFullPath(config.baseURL, config.url), config.params, config.paramsSerializer)
    const _config = {
      url: fullPath,
      header: config.header,
      complete: (response) => {
        config.fullPath = fullPath
        response.config = config
        response.rawData = response.data
        try {
          // å¯¹å¯èƒ½å­—符串不是json çš„æƒ…况容错
          if (typeof response.data === 'string') {
            response.data = JSON.parse(response.data)
          }
          // eslint-disable-next-line no-empty
        } catch (e) {
        }
        settle(resolve, reject, response)
      }
    }
    let requestTask
    if (config.method === 'UPLOAD') {
      delete _config.header['content-type']
      delete _config.header['Content-Type']
      let otherConfig = {
        // #ifdef MP-ALIPAY
        fileType: config.fileType,
        // #endif
        filePath: config.filePath,
        name: config.name
      }
      const optionalKeys = [
        // #ifdef APP-PLUS || H5
        'files',
        // #endif
        // #ifdef H5
        'file',
        // #endif
        // #ifdef H5 || APP-PLUS
        'timeout',
        // #endif
        'formData'
      ]
      requestTask = uni.uploadFile({..._config, ...otherConfig, ...mergeKeys(optionalKeys, config)})
    } else if (config.method === 'DOWNLOAD') {
      // #ifdef H5 || APP-PLUS
      if (!isUndefined(config['timeout'])) {
        _config['timeout'] = config['timeout']
      }
      // #endif
      requestTask = uni.downloadFile(_config)
    } else {
      const optionalKeys = [
        'data',
        'method',
        // #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
        'timeout',
        // #endif
        'dataType',
        // #ifndef MP-ALIPAY
        'responseType',
        // #endif
        // #ifdef APP-PLUS
        'sslVerify',
        // #endif
        // #ifdef H5
        'withCredentials',
        // #endif
        // #ifdef APP-PLUS
        'firstIpv4',
        // #endif
      ]
      requestTask = uni.request({..._config, ...mergeKeys(optionalKeys, config)})
    }
    if (config.getTask) {
      config.getTask(requestTask, config)
    }
  })
}
´úÂë¹ÜÀí/PDA/js_sdk/luch-request/luch-request/core/InterceptorManager.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
'use strict'
function InterceptorManager() {
  this.handlers = []
}
/**
 * Add a new interceptor to the stack
 *
 * @param {Function} fulfilled The function to handle `then` for a `Promise`
 * @param {Function} rejected The function to handle `reject` for a `Promise`
 *
 * @return {Number} An ID used to remove interceptor later
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  })
  return this.handlers.length - 1
}
/**
 * Remove an interceptor from the stack
 *
 * @param {Number} id The ID that was returned by `use`
 */
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null
  }
}
/**
 * Iterate over all the registered interceptors
 *
 * This method is particularly useful for skipping over any
 * interceptors that may have become `null` calling `eject`.
 *
 * @param {Function} fn The function to call for each interceptor
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
  this.handlers.forEach(h => {
    if (h !== null) {
      fn(h)
    }
  })
}
export default InterceptorManager
´úÂë¹ÜÀí/PDA/js_sdk/luch-request/luch-request/core/Request.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,197 @@
/**
 * @Class Request
 * @description luch-request http请求插件
 * @Author lu-ch
 * @Email webwork.s@qq.com
 * æ–‡æ¡£: https://www.quanzhan.co/luch-request/
 * github: https://github.com/lei-mu/luch-request
 * DCloud: http://ext.dcloud.net.cn/plugin?id=392
 */
import dispatchRequest from './dispatchRequest'
import InterceptorManager from './InterceptorManager'
import mergeConfig from './mergeConfig'
import defaults from './defaults'
import { isPlainObject } from '../utils'
import clone from '../utils/clone'
export default class Request {
  /**
   * @param {Object} arg - å…¨å±€é…ç½®
   * @param {String} arg.baseURL - å…¨å±€æ ¹è·¯å¾„
   * @param {Object} arg.header - å…¨å±€header
   * @param {String} arg.method = [GET|POST|PUT|DELETE|CONNECT|HEAD|OPTIONS|TRACE] - å…¨å±€é»˜è®¤è¯·æ±‚方式
   * @param {String} arg.dataType = [json] - å…¨å±€é»˜è®¤çš„dataType
   * @param {String} arg.responseType = [text|arraybuffer] - å…¨å±€é»˜è®¤çš„responseType。支付宝小程序不支持
   * @param {Object} arg.custom - å…¨å±€é»˜è®¤çš„自定义参数
   * @param {Number} arg.timeout - å…¨å±€é»˜è®¤çš„超时时间,单位 ms。默认60000。H5(HBuilderX 2.9.9+)、APP(HBuilderX 2.9.9+)、微信小程序(2.10.0)、支付宝小程序
   * @param {Boolean} arg.sslVerify - å…¨å±€é»˜è®¤çš„æ˜¯å¦éªŒè¯ ssl è¯ä¹¦ã€‚默认true.仅App安卓端支持(HBuilderX 2.3.3+)
   * @param {Boolean} arg.withCredentials - å…¨å±€é»˜è®¤çš„跨域请求时是否携带凭证(cookies)。默认false。仅H5支持(HBuilderX 2.6.15+)
   * @param {Boolean} arg.firstIpv4 - å…¨DNS解析时优先使用ipv4。默认false。仅 App-Android æ”¯æŒ (HBuilderX 2.8.0+)
   * @param {Function(statusCode):Boolean} arg.validateStatus - å…¨å±€é»˜è®¤çš„自定义验证器。默认statusCode >= 200 && statusCode < 300
   */
  constructor(arg = {}) {
    if (!isPlainObject(arg)) {
      arg = {}
      console.warn('设置全局参数必须接收一个Object')
    }
    this.config = clone({...defaults, ...arg})
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager()
    }
  }
  /**
   * @Function
   * @param {Request~setConfigCallback} f - è®¾ç½®å…¨å±€é»˜è®¤é…ç½®
   */
  setConfig(f) {
    this.config = f(this.config)
  }
  middleware(config) {
    config = mergeConfig(this.config, config)
    let chain = [dispatchRequest, undefined]
    let promise = Promise.resolve(config)
    this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
      chain.unshift(interceptor.fulfilled, interceptor.rejected)
    })
    this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
      chain.push(interceptor.fulfilled, interceptor.rejected)
    })
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift())
    }
    return promise
  }
  /**
   * @Function
   * @param {Object} config - è¯·æ±‚配置项
   * @prop {String} options.url - è¯·æ±‚路径
   * @prop {Object} options.data - è¯·æ±‚参数
   * @prop {Object} [options.responseType = config.responseType] [text|arraybuffer] - å“åº”的数据类型
   * @prop {Object} [options.dataType = config.dataType] - å¦‚果设为 json,会尝试对返回的数据做一次 JSON.parse
   * @prop {Object} [options.header = config.header] - è¯·æ±‚header
   * @prop {Object} [options.method = config.method] - è¯·æ±‚方法
   * @returns {Promise<unknown>}
   */
  request(config = {}) {
    return this.middleware(config)
  }
  get(url, options = {}) {
    return this.middleware({
      url,
      method: 'GET',
      ...options
    })
  }
  post(url, data, options = {}) {
    return this.middleware({
      url,
      data,
      method: 'POST',
      ...options
    })
  }
  // #ifndef MP-ALIPAY || MP-KUAISHOU || MP-JD
  put(url, data, options = {}) {
    return this.middleware({
      url,
      data,
      method: 'PUT',
      ...options
    })
  }
  // #endif
  // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
  delete(url, data, options = {}) {
    return this.middleware({
      url,
      data,
      method: 'DELETE',
      ...options
    })
  }
  // #endif
  // #ifdef H5 || MP-WEIXIN
  connect(url, data, options = {}) {
    return this.middleware({
      url,
      data,
      method: 'CONNECT',
      ...options
    })
  }
  // #endif
  // #ifdef  H5 || MP-WEIXIN || MP-BAIDU
  head(url, data, options = {}) {
    return this.middleware({
      url,
      data,
      method: 'HEAD',
      ...options
    })
  }
  // #endif
  // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
  options(url, data, options = {}) {
    return this.middleware({
      url,
      data,
      method: 'OPTIONS',
      ...options
    })
  }
  // #endif
  // #ifdef H5 || MP-WEIXIN
  trace(url, data, options = {}) {
    return this.middleware({
      url,
      data,
      method: 'TRACE',
      ...options
    })
  }
  // #endif
  upload(url, config = {}) {
    config.url = url
    config.method = 'UPLOAD'
    return this.middleware(config)
  }
  download(url, config = {}) {
    config.url = url
    config.method = 'DOWNLOAD'
    return this.middleware(config)
  }
}
/**
 * setConfig回调
 * @return {Object} - è¿”回操作后的config
 * @callback Request~setConfigCallback
 * @param {Object} config - å…¨å±€é»˜è®¤config
 */
´úÂë¹ÜÀí/PDA/js_sdk/luch-request/luch-request/core/buildFullPath.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
'use strict'
import isAbsoluteURL from '../helpers/isAbsoluteURL'
import combineURLs from '../helpers/combineURLs'
/**
 * Creates a new URL by combining the baseURL with the requestedURL,
 * only when the requestedURL is not already an absolute URL.
 * If the requestURL is absolute, this function returns the requestedURL untouched.
 *
 * @param {string} baseURL The base URL
 * @param {string} requestedURL Absolute or relative URL to combine
 * @returns {string} The combined full path
 */
export default function buildFullPath(baseURL, requestedURL) {
  if (baseURL && !isAbsoluteURL(requestedURL)) {
    return combineURLs(baseURL, requestedURL)
  }
  return requestedURL
}
´úÂë¹ÜÀí/PDA/js_sdk/luch-request/luch-request/core/defaults.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
/**
 * é»˜è®¤çš„全局配置
 */
export default {
  baseURL: '',
  header: {},
  method: 'GET',
  dataType: 'json',
  paramsSerializer: null,
  // #ifndef MP-ALIPAY
  responseType: 'text',
  // #endif
  custom: {},
  // #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
  timeout: 60000,
  // #endif
  // #ifdef APP-PLUS
  sslVerify: true,
  // #endif
  // #ifdef H5
  withCredentials: false,
  // #endif
  // #ifdef APP-PLUS
  firstIpv4: false,
  // #endif
  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300
  }
}
´úÂë¹ÜÀí/PDA/js_sdk/luch-request/luch-request/core/dispatchRequest.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
import adapter from '../adapters/index'
export default (config) => {
  return adapter(config)
}
´úÂë¹ÜÀí/PDA/js_sdk/luch-request/luch-request/core/mergeConfig.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
import {deepMerge, isUndefined} from '../utils'
/**
 * åˆå¹¶å±€éƒ¨é…ç½®ä¼˜å…ˆçš„配置,如果局部有该配置项则用局部,如果全局有该配置项则用全局
 * @param {Array} keys - é…ç½®é¡¹
 * @param {Object} globalsConfig - å½“前的全局配置
 * @param {Object} config2 - å±€éƒ¨é…ç½®
 * @return {{}}
 */
const mergeKeys = (keys, globalsConfig, config2) => {
  let config = {}
  keys.forEach(prop => {
    if (!isUndefined(config2[prop])) {
      config[prop] = config2[prop]
    } else if (!isUndefined(globalsConfig[prop])) {
      config[prop] = globalsConfig[prop]
    }
  })
  return config
}
/**
 *
 * @param globalsConfig - å½“前实例的全局配置
 * @param config2 - å½“前的局部配置
 * @return - åˆå¹¶åŽçš„配置
 */
export default (globalsConfig, config2 = {}) => {
  const method = config2.method || globalsConfig.method || 'GET'
  let config = {
    baseURL: config2.baseURL || globalsConfig.baseURL || '',
    method: method,
    url: config2.url || '',
    params: config2.params || {},
    custom: {...(globalsConfig.custom || {}), ...(config2.custom || {})},
    header: deepMerge(globalsConfig.header || {}, config2.header || {})
  }
  const defaultToConfig2Keys = ['getTask', 'validateStatus', 'paramsSerializer']
  config = {...config, ...mergeKeys(defaultToConfig2Keys, globalsConfig, config2)}
  // eslint-disable-next-line no-empty
  if (method === 'DOWNLOAD') {
    // #ifdef H5 || APP-PLUS
    if (!isUndefined(config2.timeout)) {
      config['timeout'] = config2['timeout']
    } else if (!isUndefined(globalsConfig.timeout)) {
      config['timeout'] = globalsConfig['timeout']
    }
    // #endif
  } else if (method === 'UPLOAD') {
    delete config.header['content-type']
    delete config.header['Content-Type']
    const uploadKeys = [
      // #ifdef APP-PLUS || H5
      'files',
      // #endif
      // #ifdef MP-ALIPAY
      'fileType',
      // #endif
      // #ifdef H5
      'file',
      // #endif
      'filePath',
      'name',
      // #ifdef H5 || APP-PLUS
      'timeout',
      // #endif
      'formData',
    ]
    uploadKeys.forEach(prop => {
      if (!isUndefined(config2[prop])) {
        config[prop] = config2[prop]
      }
    })
    // #ifdef H5 || APP-PLUS
    if (isUndefined(config.timeout) && !isUndefined(globalsConfig.timeout)) {
      config['timeout'] = globalsConfig['timeout']
    }
    // #endif
  } else {
    const defaultsKeys = [
      'data',
      // #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
      'timeout',
      // #endif
      'dataType',
      // #ifndef MP-ALIPAY
      'responseType',
      // #endif
      // #ifdef APP-PLUS
      'sslVerify',
      // #endif
      // #ifdef H5
      'withCredentials',
      // #endif
      // #ifdef APP-PLUS
      'firstIpv4',
      // #endif
    ]
    config = {...config, ...mergeKeys(defaultsKeys, globalsConfig, config2)}
  }
  return config
}
´úÂë¹ÜÀí/PDA/js_sdk/luch-request/luch-request/core/settle.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
/**
 * Resolve or reject a Promise based on response status.
 *
 * @param {Function} resolve A function that resolves the promise.
 * @param {Function} reject A function that rejects the promise.
 * @param {object} response The response.
 */
export default function settle(resolve, reject, response) {
  const validateStatus = response.config.validateStatus
  const status = response.statusCode
  if (status && (!validateStatus || validateStatus(status))) {
    resolve(response)
  } else {
    reject(response)
  }
}
´úÂë¹ÜÀí/PDA/js_sdk/luch-request/luch-request/helpers/buildURL.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
'use strict'
import * as utils from './../utils'
function encode(val) {
  return encodeURIComponent(val).replace(/%40/gi, '@').replace(/%3A/gi, ':').replace(/%24/g, '$').replace(/%2C/gi, ',').replace(/%20/g, '+').replace(/%5B/gi, '[').replace(/%5D/gi, ']')
}
/**
 * Build a URL by appending params to the end
 *
 * @param {string} url The base of the url (e.g., http://www.google.com)
 * @param {object} [params] The params to be appended
 * @returns {string} The formatted url
 */
export default function buildURL(url, params, paramsSerializer) {
  /*eslint no-param-reassign:0*/
  if (!params) {
    return url
  }
  var serializedParams
  if (paramsSerializer) {
    serializedParams = paramsSerializer(params)
  } else if (utils.isURLSearchParams(params)) {
    serializedParams = params.toString()
  } else {
    var parts = []
    utils.forEach(params, function serialize(val, key) {
      if (val === null || typeof val === 'undefined') {
        return
      }
      if (utils.isArray(val)) {
        key = key + '[]'
      } else {
        val = [val]
      }
      utils.forEach(val, function parseValue(v) {
        if (utils.isDate(v)) {
          v = v.toISOString()
        } else if (utils.isObject(v)) {
          v = JSON.stringify(v)
        }
        parts.push(encode(key) + '=' + encode(v))
      })
    })
    serializedParams = parts.join('&')
  }
  if (serializedParams) {
    var hashmarkIndex = url.indexOf('#')
    if (hashmarkIndex !== -1) {
      url = url.slice(0, hashmarkIndex)
    }
    url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
  }
  return url
}
´úÂë¹ÜÀí/PDA/js_sdk/luch-request/luch-request/helpers/combineURLs.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
'use strict'
/**
 * Creates a new URL by combining the specified URLs
 *
 * @param {string} baseURL The base URL
 * @param {string} relativeURL The relative URL
 * @returns {string} The combined URL
 */
export default function combineURLs(baseURL, relativeURL) {
  return relativeURL
    ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
    : baseURL
}
´úÂë¹ÜÀí/PDA/js_sdk/luch-request/luch-request/helpers/isAbsoluteURL.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
'use strict'
/**
 * Determines whether the specified URL is absolute
 *
 * @param {string} url The URL to test
 * @returns {boolean} True if the specified URL is absolute, otherwise false
 */
export default function isAbsoluteURL(url) {
  // A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).
  // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed
  // by any combination of letters, digits, plus, period, or hyphen.
  return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url)
}
´úÂë¹ÜÀí/PDA/js_sdk/luch-request/luch-request/index.d.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
type AnyObject = Record<string | number | symbol, any>
type HttpPromise<T> = Promise<HttpResponse<T>>;
type Tasks = UniApp.RequestTask | UniApp.UploadTask | UniApp.DownloadTask
export interface RequestTask {
  abort: () => void;
  offHeadersReceived: () => void;
  onHeadersReceived: () => void;
}
export interface HttpRequestConfig<T = Tasks> {
  /** è¯·æ±‚基地址 */
  baseURL?: string;
  /** è¯·æ±‚服务器接口地址 */
  url?: string;
  /** è¯·æ±‚查询参数,自动拼接为查询字符串 */
  params?: AnyObject;
  /** è¯·æ±‚体参数 */
  data?: AnyObject;
  /** æ–‡ä»¶å¯¹åº”çš„ key */
  name?: string;
  /** HTTP è¯·æ±‚中其他额外的 form data */
  formData?: AnyObject;
  /** è¦ä¸Šä¼ æ–‡ä»¶èµ„源的路径。 */
  filePath?: string;
  /** éœ€è¦ä¸Šä¼ çš„æ–‡ä»¶åˆ—表。使用 files æ—¶ï¼ŒfilePath å’Œ name ä¸ç”Ÿæ•ˆï¼ŒApp、H5( 2.6.15+) */
  files?: Array<{
    name?: string;
    file?: File;
    uri: string;
  }>;
  /** è¦ä¸Šä¼ çš„æ–‡ä»¶å¯¹è±¡ï¼Œä»…H5(2.6.15+)支持 */
  file?: File;
  /** è¯·æ±‚头信息 */
  header?: AnyObject;
  /** è¯·æ±‚方式 */
  method?: "GET" | "POST" | "PUT" | "DELETE" | "CONNECT" | "HEAD" | "OPTIONS" | "TRACE" | "UPLOAD" | "DOWNLOAD";
  /** å¦‚果设为 json,会尝试对返回的数据做一次 JSON.parse */
  dataType?: string;
  /** è®¾ç½®å“åº”的数据类型,支付宝小程序不支持 */
  responseType?: "text" | "arraybuffer";
  /** è‡ªå®šä¹‰å‚æ•° */
  custom?: AnyObject;
  /** è¶…时时间,仅微信小程序(2.10.0)、支付宝小程序支持 */
  timeout?: number;
  /** DNS解析时优先使用ipv4,仅 App-Android æ”¯æŒ (HBuilderX 2.8.0+) */
  firstIpv4?: boolean;
  /** éªŒè¯ ssl è¯ä¹¦ ä»…5+App安卓端支持(HBuilderX 2.3.3+) */
  sslVerify?: boolean;
  /** è·¨åŸŸè¯·æ±‚时是否携带凭证(cookies)仅H5支持(HBuilderX 2.6.15+) */
  withCredentials?: boolean;
  /** è¿”回当前请求的task, options。请勿在此处修改options。 */
  getTask?: (task: T, options: HttpRequestConfig<T>) => void;
  /**  å…¨å±€è‡ªå®šä¹‰éªŒè¯å™¨ */
  validateStatus?: (statusCode: number) => boolean | void;
  /** params å‚数自定义处理 */
  paramsSerializer?: (params: AnyObject) => string | void;
}
export interface HttpResponse<T = any> {
  config: HttpRequestConfig;
  statusCode: number;
  cookies: Array<string>;
  data: T;
  errMsg: string;
  header: AnyObject;
  rawData: T;
}
export interface HttpUploadResponse<T = any> {
  config: HttpRequestConfig;
  statusCode: number;
  data: T;
  errMsg: string;
  rawData: T;
}
export interface HttpDownloadResponse extends HttpResponse {
  tempFilePath: string;
}
export interface HttpError {
  config: HttpRequestConfig;
  statusCode?: number;
  cookies?: Array<string>;
  data?: any;
  errMsg: string;
  header?: AnyObject;
}
export interface HttpInterceptorManager<V, E = V> {
  use(
    onFulfilled?: (config: V) => Promise<V> | V,
    onRejected?: (config: E) => Promise<E> | E
  ): void;
  eject(id: number): void;
}
export abstract class HttpRequestAbstract {
  constructor(config?: HttpRequestConfig);
  config: HttpRequestConfig;
  interceptors: {
    request: HttpInterceptorManager<HttpRequestConfig, HttpRequestConfig>;
    response: HttpInterceptorManager<HttpResponse, HttpError>;
  }
  middleware<T = any>(config: HttpRequestConfig): HttpPromise<T>;
  request<T = any>(config: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
  get<T = any>(url: string, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
  upload<T = any>(url: string, config?: HttpRequestConfig<UniApp.UploadTask>): HttpPromise<T>;
  delete<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
  head<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
  post<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
  put<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
  connect<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
  options<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
  trace<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
  download(url: string, config?: HttpRequestConfig<UniApp.DownloadTask>): Promise<HttpDownloadResponse>;
  setConfig(onSend: (config: HttpRequestConfig) => HttpRequestConfig): void;
}
declare class HttpRequest extends HttpRequestAbstract { }
export default HttpRequest;
´úÂë¹ÜÀí/PDA/js_sdk/luch-request/luch-request/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
import Request from './core/Request'
export default Request
´úÂë¹ÜÀí/PDA/js_sdk/luch-request/luch-request/utils.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,135 @@
'use strict'
// utils is a library of generic helper functions non-specific to axios
var toString = Object.prototype.toString
/**
 * Determine if a value is an Array
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is an Array, otherwise false
 */
export function isArray (val) {
  return toString.call(val) === '[object Array]'
}
/**
 * Determine if a value is an Object
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is an Object, otherwise false
 */
export function isObject (val) {
  return val !== null && typeof val === 'object'
}
/**
 * Determine if a value is a Date
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a Date, otherwise false
 */
export function isDate (val) {
  return toString.call(val) === '[object Date]'
}
/**
 * Determine if a value is a URLSearchParams object
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a URLSearchParams object, otherwise false
 */
export function isURLSearchParams (val) {
  return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams
}
/**
 * Iterate over an Array or an Object invoking a function for each item.
 *
 * If `obj` is an Array callback will be called passing
 * the value, index, and complete array for each item.
 *
 * If 'obj' is an Object callback will be called passing
 * the value, key, and complete object for each property.
 *
 * @param {Object|Array} obj The object to iterate
 * @param {Function} fn The callback to invoke for each item
 */
export function forEach (obj, fn) {
  // Don't bother if no value provided
  if (obj === null || typeof obj === 'undefined') {
    return
  }
  // Force an array if not already something iterable
  if (typeof obj !== 'object') {
    /*eslint no-param-reassign:0*/
    obj = [obj]
  }
  if (isArray(obj)) {
    // Iterate over array values
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj)
    }
  } else {
    // Iterate over object keys
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj)
      }
    }
  }
}
/**
 * æ˜¯å¦ä¸ºboolean å€¼
 * @param val
 * @returns {boolean}
 */
export function isBoolean(val) {
  return typeof val === 'boolean'
}
/**
 * æ˜¯å¦ä¸ºçœŸæ­£çš„对象{} new Object
 * @param {any} obj - æ£€æµ‹çš„对象
 * @returns {boolean}
 */
export function isPlainObject(obj) {
  return Object.prototype.toString.call(obj) === '[object Object]'
}
/**
 * Function equal to merge with the difference being that no reference
 * to original objects is kept.
 *
 * @see merge
 * @param {Object} obj1 Object to merge
 * @returns {Object} Result of all merge properties
 */
export function deepMerge(/* obj1, obj2, obj3, ... */) {
  let result = {}
  function assignValue(val, key) {
    if (typeof result[key] === 'object' && typeof val === 'object') {
      result[key] = deepMerge(result[key], val)
    } else if (typeof val === 'object') {
      result[key] = deepMerge({}, val)
    } else {
      result[key] = val
    }
  }
  for (let i = 0, l = arguments.length; i < l; i++) {
    forEach(arguments[i], assignValue)
  }
  return result
}
export function isUndefined (val) {
  return typeof val === 'undefined'
}
´úÂë¹ÜÀí/PDA/js_sdk/luch-request/luch-request/utils/clone.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,264 @@
/* eslint-disable */
var clone = (function() {
  'use strict';
  function _instanceof(obj, type) {
    return type != null && obj instanceof type;
  }
  var nativeMap;
  try {
    nativeMap = Map;
  } catch(_) {
    // maybe a reference error because no `Map`. Give it a dummy value that no
    // value will ever be an instanceof.
    nativeMap = function() {};
  }
  var nativeSet;
  try {
    nativeSet = Set;
  } catch(_) {
    nativeSet = function() {};
  }
  var nativePromise;
  try {
    nativePromise = Promise;
  } catch(_) {
    nativePromise = function() {};
  }
  /**
   * Clones (copies) an Object using deep copying.
   *
   * This function supports circular references by default, but if you are certain
   * there are no circular references in your object, you can save some CPU time
   * by calling clone(obj, false).
   *
   * Caution: if `circular` is false and `parent` contains circular references,
   * your program may enter an infinite loop and crash.
   *
   * @param `parent` - the object to be cloned
   * @param `circular` - set to true if the object to be cloned may contain
   *    circular references. (optional - true by default)
   * @param `depth` - set to a number if the object is only to be cloned to
   *    a particular depth. (optional - defaults to Infinity)
   * @param `prototype` - sets the prototype to be used when cloning an object.
   *    (optional - defaults to parent prototype).
   * @param `includeNonEnumerable` - set to true if the non-enumerable properties
   *    should be cloned as well. Non-enumerable properties on the prototype
   *    chain will be ignored. (optional - false by default)
   */
  function clone(parent, circular, depth, prototype, includeNonEnumerable) {
    if (typeof circular === 'object') {
      depth = circular.depth;
      prototype = circular.prototype;
      includeNonEnumerable = circular.includeNonEnumerable;
      circular = circular.circular;
    }
    // maintain two arrays for circular references, where corresponding parents
    // and children have the same index
    var allParents = [];
    var allChildren = [];
    var useBuffer = typeof Buffer != 'undefined';
    if (typeof circular == 'undefined')
      circular = true;
    if (typeof depth == 'undefined')
      depth = Infinity;
    // recurse this function so we don't reset allParents and allChildren
    function _clone(parent, depth) {
      // cloning null always returns null
      if (parent === null)
        return null;
      if (depth === 0)
        return parent;
      var child;
      var proto;
      if (typeof parent != 'object') {
        return parent;
      }
      if (_instanceof(parent, nativeMap)) {
        child = new nativeMap();
      } else if (_instanceof(parent, nativeSet)) {
        child = new nativeSet();
      } else if (_instanceof(parent, nativePromise)) {
        child = new nativePromise(function (resolve, reject) {
          parent.then(function(value) {
            resolve(_clone(value, depth - 1));
          }, function(err) {
            reject(_clone(err, depth - 1));
          });
        });
      } else if (clone.__isArray(parent)) {
        child = [];
      } else if (clone.__isRegExp(parent)) {
        child = new RegExp(parent.source, __getRegExpFlags(parent));
        if (parent.lastIndex) child.lastIndex = parent.lastIndex;
      } else if (clone.__isDate(parent)) {
        child = new Date(parent.getTime());
      } else if (useBuffer && Buffer.isBuffer(parent)) {
        if (Buffer.from) {
          // Node.js >= 5.10.0
          child = Buffer.from(parent);
        } else {
          // Older Node.js versions
          child = new Buffer(parent.length);
          parent.copy(child);
        }
        return child;
      } else if (_instanceof(parent, Error)) {
        child = Object.create(parent);
      } else {
        if (typeof prototype == 'undefined') {
          proto = Object.getPrototypeOf(parent);
          child = Object.create(proto);
        }
        else {
          child = Object.create(prototype);
          proto = prototype;
        }
      }
      if (circular) {
        var index = allParents.indexOf(parent);
        if (index != -1) {
          return allChildren[index];
        }
        allParents.push(parent);
        allChildren.push(child);
      }
      if (_instanceof(parent, nativeMap)) {
        parent.forEach(function(value, key) {
          var keyChild = _clone(key, depth - 1);
          var valueChild = _clone(value, depth - 1);
          child.set(keyChild, valueChild);
        });
      }
      if (_instanceof(parent, nativeSet)) {
        parent.forEach(function(value) {
          var entryChild = _clone(value, depth - 1);
          child.add(entryChild);
        });
      }
      for (var i in parent) {
        var attrs = Object.getOwnPropertyDescriptor(parent, i);
        if (attrs) {
          child[i] = _clone(parent[i], depth - 1);
        }
        try {
          var objProperty = Object.getOwnPropertyDescriptor(parent, i);
          if (objProperty.set === 'undefined') {
            // no setter defined. Skip cloning this property
            continue;
          }
          child[i] = _clone(parent[i], depth - 1);
        } catch(e){
          if (e instanceof TypeError) {
            // when in strict mode, TypeError will be thrown if child[i] property only has a getter
            // we can't do anything about this, other than inform the user that this property cannot be set.
            continue
          } else if (e instanceof ReferenceError) {
            //this may happen in non strict mode
            continue
          }
        }
      }
      if (Object.getOwnPropertySymbols) {
        var symbols = Object.getOwnPropertySymbols(parent);
        for (var i = 0; i < symbols.length; i++) {
          // Don't need to worry about cloning a symbol because it is a primitive,
          // like a number or string.
          var symbol = symbols[i];
          var descriptor = Object.getOwnPropertyDescriptor(parent, symbol);
          if (descriptor && !descriptor.enumerable && !includeNonEnumerable) {
            continue;
          }
          child[symbol] = _clone(parent[symbol], depth - 1);
          Object.defineProperty(child, symbol, descriptor);
        }
      }
      if (includeNonEnumerable) {
        var allPropertyNames = Object.getOwnPropertyNames(parent);
        for (var i = 0; i < allPropertyNames.length; i++) {
          var propertyName = allPropertyNames[i];
          var descriptor = Object.getOwnPropertyDescriptor(parent, propertyName);
          if (descriptor && descriptor.enumerable) {
            continue;
          }
          child[propertyName] = _clone(parent[propertyName], depth - 1);
          Object.defineProperty(child, propertyName, descriptor);
        }
      }
      return child;
    }
    return _clone(parent, depth);
  }
  /**
   * Simple flat clone using prototype, accepts only objects, usefull for property
   * override on FLAT configuration object (no nested props).
   *
   * USE WITH CAUTION! This may not behave as you wish if you do not know how this
   * works.
   */
  clone.clonePrototype = function clonePrototype(parent) {
    if (parent === null)
      return null;
    var c = function () {};
    c.prototype = parent;
    return new c();
  };
// private utility functions
  function __objToStr(o) {
    return Object.prototype.toString.call(o);
  }
  clone.__objToStr = __objToStr;
  function __isDate(o) {
    return typeof o === 'object' && __objToStr(o) === '[object Date]';
  }
  clone.__isDate = __isDate;
  function __isArray(o) {
    return typeof o === 'object' && __objToStr(o) === '[object Array]';
  }
  clone.__isArray = __isArray;
  function __isRegExp(o) {
    return typeof o === 'object' && __objToStr(o) === '[object RegExp]';
  }
  clone.__isRegExp = __isRegExp;
  function __getRegExpFlags(re) {
    var flags = '';
    if (re.global) flags += 'g';
    if (re.ignoreCase) flags += 'i';
    if (re.multiline) flags += 'm';
    return flags;
  }
  clone.__getRegExpFlags = __getRegExpFlags;
  return clone;
})();
export default clone
´úÂë¹ÜÀí/PDA/main.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
import Vue from 'vue'
import App from './App'
Vue.config.productionTip = false
App.mpType = 'app'
// å¼•入全局uView
import uView from 'uview-ui'
Vue.use(uView);
// å¼•入全局TuniaoUI
import TuniaoUI from 'tuniao-ui'
Vue.use(TuniaoUI)
const app = new Vue({
    ...App
})
// http拦截器,此为需要加入的内容,如果不是写在common目录,请自行修改引入路径
import httpInterceptor from '@/common/http.interceptor.js'
// è¿™é‡Œéœ€è¦å†™åœ¨æœ€åŽï¼Œæ˜¯ä¸ºäº†ç­‰Vue创建对象完成,引入"app"对象(也即页面的"this"实例)
Vue.use(httpInterceptor, app)
app.$mount()
´úÂë¹ÜÀí/PDA/pages/AGV/AGV.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,370 @@
<template>
    <view class="login__info tn-flex tn-flex-direction-column tn-flex-col-center tn-flex-row-center">
        <view
            class="login__info__item__input tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left">
            <view class="login__info__item__input__content">
                <input maxlength="500" v-model="cacheName" placeholder-class="input-placeholder" @click="cacheshow=true"
                    placeholder="请选择缓存架编号" />
                <u-picker v-model="cacheshow" mode="selector" :range="selectorCache" range-key="cateName"
                    @confirm="cacheConfirm"></u-picker>
            </view>
        </view>
        <view
            class="login__info__item__input tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left">
            <view class="login__info__item__input__content">
                <input maxlength="500" :disabled="isOut" v-model="number" type="number" placeholder-class="input-placeholder" focus="true"
                    @input="numberInput" placeholder="请输入数量" />
            </view>
        </view>
        <view class="">
            <u-radio-group v-model="value">
                <u-radio @change="radioChange" v-for="(item, index) in list" :key="index" :name="item.name" :disabled="item.disabled">
                    {{item.name}}
                </u-radio>
            </u-radio-group>
        </view>
        <view class="login__info__item__button tn-cool-bg-color-7--reverse" @tap="submit()" hover-class="tn-hover"
            :hover-stay-time="150">呼叫AGV
        </view>
        <u-toast ref="uToast"></u-toast>
        <u-modal v-model="show" :title-style="{color: 'red'}">
            <view class="slot-content">
                <rich-text :nodes="content"></rich-text>
            </view>
        </u-modal>
    </view>
</template>
<script>
    export default {
        data() {
            return {
                number: '',
                isOut:false,
                show: false,
                content: '',
                list: [{
                        name: '空托盘入库',
                        disabled: false
                    },
                    {
                        name: '空托盘出库',
                        disabled: false
                    }
                ],
                // u-radio-group的v-model绑定的值如果设置为某个radio的name,就会被默认选中
                value: '空托盘入库',
                show1: false,
                cacheNo: '',
                cacheName: '',
                cacheshow: false,
                selectorCache: [{
                        cateName: '1号缓存架',
                        id: 1
                    },
                    {
                        cateName: '2号缓存架',
                        id: 2
                    },
                    {
                        cateName: '3号缓存架',
                        id: 3
                    },
                    {
                        cateName: '4号缓存架',
                        id: 4
                    },
                    {
                        cateName: '5号缓存架',
                        id: 5
                    },
                    {
                        cateName: '6号缓存架',
                        id: 6
                    }
                ]
            }
        },
        methods: {
            numberInput: function(e) {
                this.number = e.detail.value;
            },
            //选中某个单选框时,由radio时触发
            radioChange(e) {
                console.log(e);
                if(e =="空托盘出库"){
                    this.number = 1,
                    this.isOut = true
                }
                else
                {
                    this.number = "",
                    this.isOut = false
                }
            },
            cacheConfirm(e) {
                let x = this.selectorCache[e];
                this.cacheName = x.cateName
                this.cacheNo = x.id
            },
            submit() {
                let radio = ''
                if (this.value === "空托盘入库") {
                    radio = 1;
                } else {
                    radio = 2;
                }
                if(this.number<=0 || this.number>5)
                {
                    this.$t.message.toast('入库数量范围为1-5')
                    return;
                }
                this.$u.post("/api/Towms/SendEpmtyTask", {
                    MainData: {
                        cacheNo: this.cacheNo,
                        creator: uni.getStorageSync('jo_user').userName,
                        radio: radio
                    }
                }).then(res => {
                    this.barcode = ''
                    this.$t.message.toast(res.message);
                }).catch(err => {
                })
            }
        }
    }
</script>
<style lang="scss" scoped>
    // @import '@/static/css/templatePage/custom_nav_bar.scss';
    /* æ‚¬æµ® */
    .rocket-sussuspension {
        animation: suspension 3s ease-in-out infinite;
    }
    @keyframes suspension {
        0%,
        100% {
            transform: translate(0, 0);
        }
        50% {
            transform: translate(-0.8rem, 1rem);
        }
    }
    .login {
        position: relative;
        height: 100%;
        z-index: 1;
        /* èƒŒæ™¯å›¾ç‰‡ start */
        &__bg {
            z-index: -1;
            position: fixed;
            &--top {
                top: 0;
                left: 0;
                right: 0;
                width: 100%;
                .bg {
                    width: 750rpx;
                    will-change: transform;
                }
                .rocket {
                    margin: 50rpx 28%;
                    width: 400rpx;
                    will-change: transform;
                }
            }
            &--bottom {
                bottom: -10rpx;
                left: 0;
                right: 0;
                width: 100%;
                // height: 144px;
                margin-bottom: env(safe-area-inset-bottom);
                image {
                    width: 750rpx;
                    will-change: transform;
                }
            }
        }
        /* èƒŒæ™¯å›¾ç‰‡ end */
        /* å†…容 start */
        &__wrapper {
            margin-top: 403rpx;
            width: 100%;
        }
        /* åˆ‡æ¢ start */
        &__mode {
            position: relative;
            margin: 0 auto;
            width: 476rpx;
            height: 77rpx;
            background-color: #FFFFFF;
            box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
            border-radius: 39rpx;
            &__item {
                height: 77rpx;
                width: 100%;
                line-height: 77rpx;
                text-align: center;
                font-size: 31rpx;
                color: #908f8f;
                letter-spacing: 1em;
                text-indent: 1em;
                z-index: 2;
                transition: all 0.4s;
                &--active {
                    font-weight: bold;
                    color: #FFFFFF;
                }
            }
            &__slider {
                position: absolute;
                height: inherit;
                width: calc(476rpx);
                border-radius: inherit;
                box-shadow: 0rpx 18rpx 72rpx 18rpx rgba(0, 195, 255, 0.1);
                z-index: 1;
                transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
            }
        }
        /* åˆ‡æ¢ end */
        /* ç™»å½•注册信息 start */
        &__info {
            margin: 0 30rpx;
            margin-top: 105rpx;
            padding: 30rpx 51rpx;
            padding-bottom: 0;
            border-radius: 20rpx;
            background-color: #ffff;
            box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
            &__item {
                &__input {
                    margin-top: 59rpx;
                    width: 100%;
                    height: 100rpx;
                    border: 1rpx solid #E6E6E6;
                    border-radius: 39rpx;
                    &__left-icon {
                        width: 10%;
                        font-size: 44rpx;
                        margin-left: 20rpx;
                        color: #AAAAAA;
                    }
                    &__content {
                        width: 80%;
                        padding-left: 10rpx;
                        &--verify-code {
                            width: 56%;
                        }
                        input {
                            font-size: 48rpx;
                            // letter-spacing: 0.1em;
                        }
                    }
                    &__right-icon {
                        width: 10%;
                        font-size: 44rpx;
                        margin-right: 20rpx;
                        color: #AAAAAA;
                    }
                    &__right-verify-code {
                        width: 34%;
                        margin-right: 20rpx;
                    }
                }
                &__button {
                    margin-top: 75rpx;
                    margin-bottom: 39rpx;
                    width: 100%;
                    height: 77rpx;
                    text-align: center;
                    font-size: 38rpx;
                    font-weight: bold;
                    line-height: 77rpx;
                    letter-spacing: 1em;
                    text-indent: 1em;
                    border-radius: 39rpx;
                    box-shadow: 1rpx 10rpx 24rpx 0rpx rgba(60, 129, 254, 0.35);
                }
                &__tips {
                    margin: 30rpx 0;
                    color: #AAAAAA;
                }
            }
        }
        /* ç™»å½•注册信息 end */
        /* ç™»å½•方式切换 start */
        &__way {
            margin: 0 auto;
            margin-top: 110rpx;
            &__item {
                &--icon {
                    width: 77rpx;
                    height: 77rpx;
                    font-size: 50rpx;
                    border-radius: 100rpx;
                    margin-bottom: 18rpx;
                    position: relative;
                    z-index: 1;
                    &::after {
                        content: " ";
                        position: absolute;
                        z-index: -1;
                        width: 100%;
                        height: 100%;
                        left: 0;
                        bottom: 0;
                        border-radius: inherit;
                        opacity: 1;
                        transform: scale(1, 1);
                        background-size: 100% 100%;
                        background-image: url(https://tnuiimage.tnkjapp.com/cool_bg_image/icon_bg5.png);
                    }
                }
            }
        }
        /* ç™»å½•方式切换 end */
        /* å†…容 end */
    }
    /deep/.input-placeholder {
        font-size: 24rpx;
        color: #E6E6E6;
    }
</style>
´úÂë¹ÜÀí/PDA/pages/feeding/feeding.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,347 @@
<template>
    <view class="login__info tn-flex tn-flex-direction-column tn-flex-col-center tn-flex-row-center">
        <view
            class="login__info__item__input tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left">
            <view class="login__info__item__input__content">
                <input maxlength="500" :disabled="isfill" v-model="barcode" placeholder-class="input-placeholder"
                    focus="true" @input="barcodeInput" placeholder="请扫描条码"   />
            </view>
            <button class="" size="mini" @click="clearSN" > æ¸…空</button>
        </view>
        <!-- <view class="login__info__item__button tn-cool-bg-color-7--reverse" @tap="resrt()" hover-class="tn-hover"
            :hover-stay-time="150">重置条码</view> -->
        <view
            class="login__info__item__input tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left">
            <view class="login__info__item__input__content">
                <input  maxlength="500" v-model="station" placeholder-class="input-placeholder" @click="show = true"
                    placeholder="请选择下料口" />
                <u-picker v-model="show" mode="selector" :range="selectorObj" range-key="cateName" @confirm="confirm">
                </u-picker>
            </view>
        </view>
        <view class="login__info__item__button tn-cool-bg-color-7--reverse" @tap="submit()" hover-class="tn-hover"
            :hover-stay-time="150">通知AGV</view>
        <u-toast ref="uToast"></u-toast>
    </view>
</template>
<script>
    export default {
        data() {
            return {
                barcode: '',
                isfill: false,
                show: false,
                station: '',
                stationNo: '',
                selectorObj: [{
                        cateName: '1单元1号下料口',
                        id: "1"
                    },
                    {
                        cateName: '1单元2号下料口',
                        id: "2"
                    },
                    {
                        cateName: '1单元3号下料口',
                        id: "3"
                    },
                    {
                        cateName: '2单元2号下料口',
                        id: "5"
                    },
                    {
                        cateName: '2单元3号下料口',
                        id: "6"
                    }
                ]
            }
        },
        methods: {
            barcodeInput: function(e) {
                if (e.detail.value == "") {
                    this.barcode = e.detail.value;
                } else {
                    var len = this.barcode.split(',').length;
                    if (len <= 4) {
                        this.barcode = this.barcode + ',';
                    } else {
                        this.$t.message.toast('数量最多为5个');
                        this.isfill=true;
                        return;
                    }
                }
            },
            confirm(e) {
                console.log(e)
                let x = this.selectorObj[e];
                this.station = x.cateName
                this.stationNo = x.id
            },
            clearSN:function(e){
                this.barcode="";
                this.isfill=false;
            },
            submit() {
                if (this.barcode == '') {
                    this.$t.message.toast('条码不能为空')
                    return;
                }
                this.$u.post("/api/ToWms/OutsourceInbound", {
                    MainData: {
                        dataSN: this.barcode,
                        stationNo: this.stationNo,
                        creator: uni.getStorageSync('jo_user').userName,
                    }
                }).then(res => {
                    if (res.status) {
                        this.barcode = '',
                            this.station = '',
                            this.stationNo = '',
                            this.$t.message.toast('通知AGV成功,请等待AGV取货');
                    } else {
                        this.$t.message.toast(res.message);
                    }
                    console.log(res);
                }).catch(err => {
                })
            }
        }
    }
</script>
<style lang="scss" scoped>
    // @import '@/static/css/templatePage/custom_nav_bar.scss';
    /* æ‚¬æµ® */
    .rocket-sussuspension {
        animation: suspension 3s ease-in-out infinite;
    }
    @keyframes suspension {
        0%,
        100% {
            transform: translate(0, 0);
        }
        50% {
            transform: translate(-0.8rem, 1rem);
        }
    }
    .login {
        position: relative;
        height: 100%;
        z-index: 1;
        /* èƒŒæ™¯å›¾ç‰‡ start */
        &__bg {
            z-index: -1;
            position: fixed;
            &--top {
                top: 0;
                left: 0;
                right: 0;
                width: 100%;
                .bg {
                    width: 750rpx;
                    will-change: transform;
                }
                .rocket {
                    margin: 50rpx 28%;
                    width: 400rpx;
                    will-change: transform;
                }
            }
            &--bottom {
                bottom: -10rpx;
                left: 0;
                right: 0;
                width: 100%;
                // height: 144px;
                margin-bottom: env(safe-area-inset-bottom);
                image {
                    width: 750rpx;
                    will-change: transform;
                }
            }
        }
        /* èƒŒæ™¯å›¾ç‰‡ end */
        /* å†…容 start */
        &__wrapper {
            margin-top: 403rpx;
            width: 100%;
        }
        /* åˆ‡æ¢ start */
        &__mode {
            position: relative;
            margin: 0 auto;
            width: 476rpx;
            height: 77rpx;
            background-color: #FFFFFF;
            box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
            border-radius: 39rpx;
            &__item {
                height: 77rpx;
                width: 100%;
                line-height: 77rpx;
                text-align: center;
                font-size: 31rpx;
                color: #908f8f;
                letter-spacing: 1em;
                text-indent: 1em;
                z-index: 2;
                transition: all 0.4s;
                &--active {
                    font-weight: bold;
                    color: #FFFFFF;
                }
            }
            &__slider {
                position: absolute;
                height: inherit;
                width: calc(476rpx);
                border-radius: inherit;
                box-shadow: 0rpx 18rpx 72rpx 18rpx rgba(0, 195, 255, 0.1);
                z-index: 1;
                transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
            }
        }
        /* åˆ‡æ¢ end */
        /* ç™»å½•注册信息 start */
        &__info {
            margin: 0 30rpx;
            margin-top: 105rpx;
            padding: 30rpx 51rpx;
            padding-bottom: 0;
            border-radius: 20rpx;
            background-color: #ffff;
            box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
            &__item {
                &__input {
                    margin-top: 59rpx;
                    width: 100%;
                    height: 100rpx;
                    border: 1rpx solid #E6E6E6;
                    border-radius: 39rpx;
                    &__left-icon {
                        width: 10%;
                        font-size: 44rpx;
                        margin-left: 20rpx;
                        color: #AAAAAA;
                    }
                    &__content {
                        width: 80%;
                        padding-left: 10rpx;
                        &--verify-code {
                            width: 56%;
                        }
                        input {
                            font-size: 48rpx;
                            // letter-spacing: 0.1em;
                        }
                    }
                    &__right-icon {
                        width: 10%;
                        font-size: 44rpx;
                        margin-right: 20rpx;
                        color: #AAAAAA;
                    }
                    &__right-verify-code {
                        width: 34%;
                        margin-right: 20rpx;
                    }
                }
                &__button {
                    margin-top: 75rpx;
                    margin-bottom: 39rpx;
                    width: 100%;
                    height: 77rpx;
                    text-align: center;
                    font-size: 48rpx;
                    font-weight: bold;
                    line-height: 77rpx;
                    letter-spacing: 1em;
                    text-indent: 1em;
                    border-radius: 39rpx;
                    box-shadow: 1rpx 10rpx 24rpx 0rpx rgba(60, 129, 254, 0.35);
                }
                &__tips {
                    margin: 30rpx 0;
                    color: #AAAAAA;
                }
            }
        }
        /* ç™»å½•注册信息 end */
        /* ç™»å½•方式切换 start */
        &__way {
            margin: 0 auto;
            margin-top: 110rpx;
            &__item {
                &--icon {
                    width: 77rpx;
                    height: 77rpx;
                    font-size: 50rpx;
                    border-radius: 100rpx;
                    margin-bottom: 18rpx;
                    position: relative;
                    z-index: 1;
                    &::after {
                        content: " ";
                        position: absolute;
                        z-index: -1;
                        width: 100%;
                        height: 100%;
                        left: 0;
                        bottom: 0;
                        border-radius: inherit;
                        opacity: 1;
                        transform: scale(1, 1);
                        background-size: 100% 100%;
                        background-image: url(https://tnuiimage.tnkjapp.com/cool_bg_image/icon_bg5.png);
                    }
                }
            }
        }
        /* ç™»å½•方式切换 end */
        /* å†…容 end */
    }
    /deep/.input-placeholder {
        font-size: 24rpx;
        color: #E6E6E6;
    }
</style>
´úÂë¹ÜÀí/PDA/pages/home/home.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,269 @@
<template>
    <view class="page">
        <view class="top">
            <view class="background"></view>
        </view>
        <view class="user-card">
            <view class="card">
                <view class="top">
                    <view class="userImage">
                        <!-- <open-data type="userAvatarUrl"></open-data> -->
                        <u-avatar :src="src" size="146"></u-avatar>
                    </view>
                </view>
                <view class="bottom" @click="Login">
                    <view class="left">
                        <view class="user-text">
                            <!-- <open-data type="userNickName"></open-data> -->
                            <text style="text-align: center;">{{userNickName}}</text>
                        </view>
                        <!-- <view class="user-phone"> 171****4133 </view> -->
                    </view>
                    <view class="right flex-center">
                        <u-icon class="icon" name="arrow-right"></u-icon>
                    </view>
                </view>
            </view>
        </view>
        <view class="list-card">
            <!-- <view class="card">
        <view class="item item-bottom-solid">
          <view class="left flex-center">
            <image src="../../static/myIcon/qiu.png" mode="aspectFit"></image>
          </view>
          <view class="center">
            <text>参加的活动</text>
          </view>
          <view class="right flex-center">
            <u-icon class="icon" name="arrow-right"></u-icon>
          </view>
        </view>
      </view>
      <view class="card">
        <view class="item item-bottom-solid">
          <view class="left flex-center">
            <image src="../../static/myIcon/1.png" mode="aspectFit"></image>
          </view>
          <view class="center">
            <text>参加的活动</text>
          </view>
          <view class="right flex-center">
            <u-icon class="icon" name="arrow-right"></u-icon>
          </view>
        </view>
      </view>
      <view class="card">
        <view class="item">
          <view class="left flex-center">
            <image src="../../static/myIcon/2.png" mode="aspectFit"></image>
          </view>
          <view class="center">
            <text>参加的活动</text>
          </view>
          <view class="right flex-center">
            <u-icon class="icon" name="arrow-right"></u-icon>
          </view>
        </view>
      </view> -->
        </view>
        <view class="quit flex-center">
            <view class="btn flex-center" @click="LastLogin">
                é€€å‡ºç™»å½•
            </view>
        </view>
    </view>
</template>
<style lang="scss" scoped>
    .top {
        height: 250rpx;
        position: relative;
        .background {
            background-color: #5199ff;
            border-bottom-left-radius: 22px;
            border-bottom-right-radius: 22px;
            position: absolute;
            height: 180rpx;
            width: 100%;
        }
    }
    .icon {
        color: #96a1ae;
        font-size: 40rpx;
    }
    .user-card {
        height: 170rpx;
        padding: 0 15px;
        .card {
            position: relative;
            bottom: 62px;
            height: 250rpx;
            background-color: white;
            border-radius: 5px;
            .top {
                height: 30%;
                position: relative;
                .userImage {
                    position: absolute;
                    bottom: 24%;
                    left: 10%;
                    width: 150rpx;
                    height: 150rpx;
                    overflow: hidden;
                    border-radius: 50%;
                    border: 2px solid white;
                }
            }
            .bottom {
                display: flex;
                height: 70%;
                .left {
                    width: 80%;
                    height: 100%;
                    position: relative;
                    .user-text {
                        width: 100%;
                        font-size: 1.6em;
                        padding-left: 80rpx;
                        height: 50%;
                    }
                    .user-phone {
                        color: #96a1ae;
                        padding-left: 80rpx;
                        height: 50%;
                        width: 100%;
                        font-size: 0.9em;
                    }
                }
                .right {
                    width: 20%;
                    height: 50%;
                }
            }
        }
    }
    .list-card {
        padding: 0 15px;
        .card {
            border-radius: 5px;
            position: relative;
            background-color: white;
            border-radius: 5px;
            padding: 5px 30px;
            .item {
                display: flex;
                height: 120rpx;
                .left {
                    width: 15%;
                    image {
                        width: 70rpx;
                        height: 70rpx;
                    }
                }
                .center {
                    width: 65%;
                    display: flex;
                    justify-content: start;
                    align-items: center;
                    font-size: 1.1em;
                }
                .right {
                    width: 20%;
                    justify-content: flex-end;
                }
            }
        }
    }
    .item-bottom-solid {
        border-bottom: 1px solid #d4d6da;
    }
    .quit {
        height: 100rpx;
        margin-top: 50px;
        .btn {
            background-color: #4f99ff;
            border-radius: 30px;
            width: 80%;
            color: white;
            font-size: 1.2em;
            height: 100%;
        }
    }
    .flex-center {
        display: flex;
        justify-content: center;
        align-items: center;
    }
</style>
<script>
    //import {  } from "@/common/api/{$}.js";
    import httpInterceptor from '@/common/http.interceptor.js'
    export default {
        data() {
            return {
                src: "",
                userNickName: '请登录',
            };
        },
        //监听页面初始化,其参数同 onLoad å‚数,为上个页面传递的数据,参数类型为 Object(用于页面传参),触发时机早于 onLoad
        onInit() {},
        //监听页面加载,其参数为上个页面传递的数据,参数类型为 Object(用于页面传参)
        onLoad() {
            let isLogin = this.hasLogin();
            if (isLogin) {
                let haslogin = uni.getStorageSync('jo_user')
                this.userNickName = haslogin.userName;
                this.src = httpInterceptor.baseUrl + "/" + haslogin.img;
            }
        },
        //监听页面初次渲染完成。注意如果渲染速度快,会在页面进入动画完成前触发
        onReady() {},
        //监听页面显示。页面每次出现在屏幕上都触发,包括从下级页面点返回露出当前页面
        beforeDestroy() {},
        //页面滚动到底部的事件(不是scroll-view滚到底),常用于下拉下一页数据。
        onReachBottom() {},
        onShareAppMessage(res) {},
        created() {},
        methods: {
            hasLogin() {
                let haslogin = uni.getStorageSync('jo_user')
                if (haslogin == null || haslogin == "") {
                    return false
                } else {
                    return true
                }
            },
            LastLogin() {
                uni.clearStorage();
                this.$router.go(0)
            },
            Login(){
                this.$u.route('/pages/login/login');
            }
        },
    };
</script>
´úÂë¹ÜÀí/PDA/pages/index/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
<template>
    <u-card :title="title" >
        <view class="" slot="body">
            <u-grid :col="3">
                <u-grid-item @tap="clickCoupon">
                    <u-icon name="order" :size="46"></u-icon>
                    <view class="grid-text">外协移库</view>
                </u-grid-item>
                <u-grid-item @tap="lock">
                    <u-icon name="car-fill" :size="46"></u-icon>
                    <view class="grid-text">外协入库</view>
                </u-grid-item>
                <u-grid-item @tap="AGV">
                    <u-icon name="car" :size="46"></u-icon>
                    <view class="grid-text">空盘</view>
                </u-grid-item>
            </u-grid>
        </view>
    </u-card>
</template>
<script>
    export default {
        data() {
            return {
                title: '操作功能'
            }
        },
        onLoad() {
        },
        methods: {
            clickCoupon(){
                if(this.hasLogin()){
                    this.$u.route("pages/unpacking/unpacking")
                }else{
                    this.$t.message.loading('登录失效请重新登录')
                    setTimeout(()=>{
                        this.$t.message.closeLoading()
                        this.$u.route({
                            type:'reLaunch',
                            url:'pages/login/login'
                        })
                        // this.$Router.replace({name:"tabbar"})
                    },1300)
                }
            },
            lock(){
                console.log("lock")
                if(this.hasLogin()){
                    this.$u.route("pages/feeding/feeding")
                }else{
                    this.$t.message.loading('登录失效请重新登录')
                    setTimeout(()=>{
                        this.$t.message.closeLoading()
                        this.$u.route({
                            type:'reLaunch',
                            url:'pages/login/login'
                        })
                        // this.$Router.replace({name:"tabbar"})
                    },1300)
                }
            },
            AGV(){
                console.log("AGV")
                if(this.hasLogin()){
                    this.$u.route("pages/AGV/AGV")
                }else{
                    this.$t.message.loading('登录失效请重新登录')
                    setTimeout(()=>{
                        this.$t.message.closeLoading()
                        this.$u.route({
                            type:'reLaunch',
                            url:'pages/login/login'
                        })
                        // this.$Router.replace({name:"tabbar"})
                    },1300)
                }
            },
            //判断是否登录
            hasLogin(){
                let haslogin= uni.getStorageSync('jo_user')
                if(haslogin==null||haslogin==""){
                    return false
                }
                else{
                    return true
                }
            }
        }
    }
</script>
<style lang="scss" scoped>
    .grid-text {
        font-size: 28rpx;
        margin-top: 4rpx;
        color: $u-type-info;
    }
</style>
´úÂë¹ÜÀí/PDA/pages/login/login.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,439 @@
<template>
  <view class="template-login">
    <!-- é¡¶éƒ¨è‡ªå®šä¹‰å¯¼èˆª -->
    <!-- <tn-nav-bar fixed alpha customBack>
      <view slot="back" class='tn-custom-nav-bar__back'
        @click="goBack">
        <text class='icon tn-icon-left'></text>
        <text class='icon tn-icon-home-capsule-fill'></text>
      </view>
    </tn-nav-bar> -->
    <view class="login">
      <!-- é¡¶éƒ¨èƒŒæ™¯å›¾ç‰‡-->
      <view class="login__bg login__bg--top">
        <image class="bg" src="/static/login_top2.jpg" mode="widthFix"></image>
      </view>
      <view class="login__bg login__bg--top">
        <image class="rocket rocket-sussuspension" src="/static/login_top3.png" mode="widthFix"></image>
      </view>
      <view class="login__wrapper">
        <!-- ç™»å½•/注册切换 -->
        <!-- <view class="login__mode tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-center">
          <view class="login__mode__item tn-flex-1" :class="[{'login__mode__item--active': currentModeIndex === 0}]" @tap.stop="modeSwitch(0)">
            ç™»å½•
          </view>
          <view class="login__mode__item tn-flex-1" :class="[{'login__mode__item--active': currentModeIndex === 1}]" @tap.stop="modeSwitch(1)">
            æ³¨å†Œ
          </view>
          <view class="login__mode__slider tn-cool-bg-color-15--reverse" :style="[modeSliderStyle]"></view>
        </view> -->
        <!-- è¾“入框内容-->
        <view class="login__info tn-flex tn-flex-direction-column tn-flex-col-center tn-flex-row-center">
          <!-- ç™»å½• -->
          <block v-if="currentModeIndex === 0">
            <view class="login__info__item__input tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left">
              <view class="login__info__item__input__left-icon">
                <view class="tn-icon-my"></view>
              </view>
              <view class="login__info__item__input__content">
                <input maxlength="45" placeholder-class="input-placeholder" @input="userInput" placeholder="请输入登录用户名称" />
              </view>
            </view>
            <view class="login__info__item__input tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left">
              <view class="login__info__item__input__left-icon">
                <view class="tn-icon-lock"></view>
              </view>
              <view class="login__info__item__input__content">
                <input :password="!showPassword" placeholder-class="input-placeholder" @input="passInput" placeholder="请输入登录密码" />
              </view>
              <view class="login__info__item__input__right-icon" @click="showPassword = !showPassword">
                <view :class="[showPassword ? 'tn-icon-eye' : 'tn-icon-eye-hide']"></view>
              </view>
            </view>
            <view class="login__info__item__input tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left">
              <view class="login__info__item__input__left-icon">
                <view class="tn-icon-my"></view>
              </view>
              <view class="login__info__item__input__content">
                <input maxlength="45" placeholder-class="input-placeholder" @input="codeInput" placeholder="请输入验证码" />
              </view>
              <view class="login__info__item__input__right-icon u-border" style="width: 30%;" @click="getVierificationCode">
                <img v-show="codeImgSrc != ''" :src="codeImgSrc" />
              </view>
            </view>
          </block>
          <!-- æ³¨å†Œ -->
          <view class="login__info__item__button tn-cool-bg-color-7--reverse" @click="currentModeIndex === 0 ? login() : registra()" hover-class="tn-hover" :hover-stay-time="150">{{ currentModeIndex === 0 ? '登录' : '注册'}}</view>
          <!-- <view v-if="currentModeIndex === 0" class="login__info__item__tips">忘记密码?</view> -->
        </view>
      </view>
      <tn-tips ref="tips" position="top"></tn-tips>
      <!-- åº•部背景图片-->
      <view class="login__bg login__bg--bottom">
        <image src="/static/login_bottom_bg.jpg" mode="widthFix"></image>
      </view>
    </view>
    <!-- éªŒè¯ç å€’计时 -->
    <!-- <tn-verification-code
      ref="code"
      uniqueKey="login-demo-1"
      :seconds="60"
      @change="codeChange">
    </tn-verification-code> -->
  </view>
</template>
<script>
  var app = getApp();
  export default {
    name: 'login-demo-1',
    // mixins: [template_page_mixin],
    data() {
      return {
        // å½“前选中的模式
        currentModeIndex: 0,
        // æ¨¡å¼é€‰ä¸­æ»‘块
        modeSliderStyle: {
          left: 0
        },
        // æ˜¯å¦æ˜¾ç¤ºå¯†ç 
        showPassword: false,
        // å€’计时提示文字
        tips: '获取验证码',
        email: '',
        codeImgSrc: '',
        pass: '',
        code: '',
        user: '',
        isDetail: false
      }
    },
    watch: {
      currentModeIndex(value) {
        const sliderWidth = uni.upx2px(476 / 2)
        this.modeSliderStyle.left = `${sliderWidth * value}px`
      }
    },
    onLoad(options) {
        this.getVierificationCode()
        if (options.id) {
            this.isDetail = true
        }
    },
    methods: {
        ///获取验证码
        getVierificationCode() {
            this.$u.get('/api/User/getVierificationCode', {}).then(res=>{
                if (res.img != null) {
                    this.codeImgSrc = "data:image/png;base64," + res.img;
                    this.email = res.uuid;
                } else {
                    this.$refs.uToast.show({
                        title: '获取验证码失败请重新获取',
                        type: 'error',
                    })
                }
            })
            // uni.request({
            //     url:"http://192.168.0.101:8098/api/User/getVierificationCode",
            //     success: (res) => {
            //     }
            // })
        },
        login() {
            if (this.pass == '') {
                this.$t.message.toast('请输入密码')
                return;
            } else if (this.user == '') {
                this.$t.message.toast('请输入用户名')
                return;
            } else if (this.pass.length < 6) {
                this.$t.message.toast('密码应大于6位')
                return;
            } else {
                this.$t.message.loading('正在登录')
                let userInfo = this.userInfo;
                let userifno = {
                    UUID: this.email,
                    passWord: this.pass,
                    userName: this.user,
                    verificationCode: this.code
                }
                this.$u.post('/api/User/login', {
                    UUID: this.email,
                    passWord: this.pass,
                    userName: this.user,
                    verificationCode: this.code
                }).then(res => {
                    // this.$u.toast(res.message);
                    this.$t.message.toast(res.message)
                    this.$t.message.closeLoading()
                    uni.setStorage({
                        key: 'jo_id_token',
                        data: res.data.token,
                    });
                    uni.setStorage({
                        key: 'jo_user',
                        data: res.data,
                    });
                    uni.setStorage({
                        key: 'jo_userImg',
                        data: res.data.img,
                    });
                    setTimeout(()=>{
                        this.$u.route({
                            type:'reLaunch',
                            url:'pages/index/index'
                        })
                        // this.$Router.replace({name:"tabbar"})
                    },1300)
                });
            }
        },
        codeInput: function(e) {
            this.code = e.detail.value;
        },
        passInput: function(e) {
            this.pass = e.detail.value;
        },
        userInput: function(e) {
            this.user = e.detail.value;
        },
    }
  }
</script>
<style lang="scss" scoped>
  // @import '@/static/css/templatePage/custom_nav_bar.scss';
  /* æ‚¬æµ® */
  .rocket-sussuspension{
    animation: suspension 3s ease-in-out infinite;
  }
  @keyframes suspension {
    0%, 100% {
      transform: translate(0 , 0);
    }
    50% {
      transform: translate(-0.8rem , 1rem);
    }
  }
  .login {
    position: relative;
    height: 100%;
    z-index: 1;
    /* èƒŒæ™¯å›¾ç‰‡ start */
    &__bg {
      z-index: -1;
      position: fixed;
      &--top {
        top: 0;
        left: 0;
        right: 0;
        width: 100%;
        .bg {
          width: 750rpx;
          will-change: transform;
        }
        .rocket {
          margin: 50rpx 28%;
          width: 400rpx;
          will-change: transform;
        }
      }
      &--bottom {
        bottom: -10rpx;
        left: 0;
        right: 0;
        width: 100%;
        // height: 144px;
        margin-bottom: env(safe-area-inset-bottom);
        image {
          width: 750rpx;
          will-change: transform;
        }
      }
    }
    /* èƒŒæ™¯å›¾ç‰‡ end */
    /* å†…容 start */
    &__wrapper {
      margin-top: 250rpx;
      width: 100%;
    }
    /* åˆ‡æ¢ start */
    &__mode {
      position: relative;
      margin: 0 auto;
      width: 476rpx;
      height: 77rpx;
      background-color: #FFFFFF;
      box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
      border-radius: 39rpx;
      &__item {
        height: 77rpx;
        width: 100%;
        line-height: 77rpx;
        text-align: center;
        font-size: 31rpx;
        color: #908f8f;
        letter-spacing: 1em;
        text-indent: 1em;
        z-index: 2;
        transition: all 0.4s;
        &--active {
          font-weight: bold;
          color: #FFFFFF;
        }
      }
      &__slider {
        position: absolute;
        height: inherit;
        width: calc(476rpx);
        border-radius: inherit;
        box-shadow: 0rpx 18rpx 72rpx 18rpx rgba(0, 195, 255, 0.1);
        z-index: 1;
        transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
      }
    }
    /* åˆ‡æ¢ end */
    /* ç™»å½•注册信息 start */
    &__info {
      margin: 0 30rpx;
      margin-top: 105rpx;
      padding: 30rpx 51rpx;
      padding-bottom: 0;
      border-radius: 20rpx;
      background-color: #ffff;
      box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
      &__item {
        &__input {
          margin-top: 59rpx;
          width: 100%;
          height: 77rpx;
          border: 1rpx solid #E6E6E6;
          border-radius: 39rpx;
          &__left-icon {
            width: 10%;
            font-size: 44rpx;
            margin-left: 20rpx;
            color: #AAAAAA;
          }
          &__content {
            width: 80%;
            padding-left: 10rpx;
            &--verify-code {
              width: 56%;
            }
            input {
              font-size: 24rpx;
              // letter-spacing: 0.1em;
            }
          }
          &__right-icon {
            width: 10%;
            font-size: 44rpx;
            margin-right: 20rpx;
            color: #AAAAAA;
          }
          &__right-verify-code {
            width: 34%;
            margin-right: 20rpx;
          }
        }
        &__button {
          margin-top: 75rpx;
          margin-bottom: 39rpx;
          width: 100%;
          height: 77rpx;
          text-align: center;
          font-size: 31rpx;
          font-weight: bold;
          line-height: 77rpx;
          letter-spacing: 1em;
          text-indent: 1em;
          border-radius: 39rpx;
          box-shadow: 1rpx 10rpx 24rpx 0rpx rgba(60, 129, 254, 0.35);
        }
        &__tips {
          margin: 30rpx 0;
          color: #AAAAAA;
        }
      }
    }
    /* ç™»å½•注册信息 end */
    /* ç™»å½•方式切换 start */
    &__way {
      margin: 0 auto;
      margin-top: 110rpx;
      &__item {
        &--icon {
          width: 77rpx;
          height: 77rpx;
          font-size: 50rpx;
          border-radius: 100rpx;
          margin-bottom: 18rpx;
          position: relative;
          z-index: 1;
          &::after {
            content: " ";
            position: absolute;
            z-index: -1;
            width: 100%;
            height: 100%;
            left: 0;
            bottom: 0;
            border-radius: inherit;
            opacity: 1;
            transform: scale(1, 1);
            background-size: 100% 100%;
            background-image: url(https://tnuiimage.tnkjapp.com/cool_bg_image/icon_bg5.png);
          }
        }
      }
    }
    /* ç™»å½•方式切换 end */
    /* å†…容 end */
  }
  /deep/.input-placeholder {
    font-size: 24rpx;
    color: #E6E6E6;
  }
</style>
´úÂë¹ÜÀí/PDA/pages/unpacking/unpacking.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,439 @@
<template>
    <view>
        <view class="login__info tn-flex tn-flex-direction-column tn-flex-col-center tn-flex-row-center">
            <!-- <view
                class="login__info__item__input tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left">
                <view class="login__info__item__input__content">
                    <input maxlength="500" v-model="jobID" focus="true" placeholder-class="input-placeholder"
                        @input="jobIDInput" placeholder="请输入交接工单" />
                </view>
            </view> -->
            <view
                class="login__info__item__input tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left">
                <view class="login__info__item__input__content">
                    <input maxlength="500" v-model="barcode" focus="true" placeholder-class="input-placeholder"
                        @input="barcodeInput" placeholder="请扫描最底下的车轮号" />
                </view>
            </view>
            <!-- <view
                class="login__info__item__input tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left">
                <view class="login__info__item__input__content">
                    <input  maxlength="500" v-model="cacheName"  placeholder-class="input-placeholder"
                        @click="cacheshow=true" placeholder="请选择缓存架编号" />
                    <u-picker v-model="cacheshow" mode="selector" :range="selectorCache" range-key="cateName"
                        @confirm="cacheConfirm"></u-picker>
                </view>
            </view> -->
            <!-- <view
                class="login__info__item__input tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left">
                <view class="login__info__item__input__content">
                    <input  maxlength="500" v-model="stationName"  placeholder-class="input-placeholder"
                        @click="stationshow=true" placeholder="请选择下料口" />
                    <u-picker v-model="stationshow" mode="selector" :range="selectorStation" range-key="cateName"
                        @confirm="stationConfirm"></u-picker>
                </view>
            </view> -->
            <view class="login__info__item__button tn-cool-bg-color-7--reverse" @tap="submitCheek()"
                hover-class="tn-hover" :hover-stay-time="150">信息核对</view>
            <view :class="istrue?'isnone':'isnonone'">
                <view class="login__info__item__button tn-cool-bg-color-7--reverse" @tap="submitTransfer()"
                    hover-class="tn-hover" :hover-stay-time="150" style="margin-top: 0px;">入缓存架</view>
            </view>
        </view>
    </view>
</template>
<script>
    export default {
        data() {
            return {
                jobID: '',
                barcode: '',
                cacheNo: '',
                cacheName: '',
                stationNo: '',
                stationName: '',
                istrue: true,
                cacheshow: false,
                stationshow: false,
                selectorCache: [{
                        cateName: '1号缓存架',
                        id: 1
                    },
                    {
                        cateName: '2号缓存架',
                        id: 2
                    },
                    {
                        cateName: '3号缓存架',
                        id: 3
                    },
                    {
                        cateName: '4号缓存架',
                        id: 4
                    },
                    {
                        cateName: '5号缓存架',
                        id: 5
                    },
                    {
                        cateName: '6号缓存架',
                        id: 6
                    }
                ],
                selectorStation: [{
                        cateName: 'B区正极投料口',
                        id: "DepThreeForZJYJ"
                    },
                    {
                        cateName: 'B区负极投料口',
                        id: "DepThreeForFJYJ"
                    }
                ]
            }
        },
        methods: {
            ///工单号
            jobIDInput: function(e) {
                this.jobID = e.detail.value;
                console.log(this.jobID);
            },
            ///条码
            barcodeInput: function(e) {
                this.barcode = e.detail.value;
                console.log(this.barcode);
                console.log(uni.getStorageSync('jo_user'));
            },
            //缓存架编号
            cacheConfirm(e) {
                let x = this.selectorCache[e];
                this.cacheName = x.cateName
                this.cacheNo = x.id
            },
            //下料口
            stationConfirm(e) {
                let x = this.selectorStation[e];
                this.stationName = x.cateName
                this.stationNo = x.id
            },
            //MES信息校验
            submitCheek() {
                if (this.barcode == '') {
                    this.$t.message.toast('条码不能为空');
                    return
                }
                // else if (this.stationNo == '') {
                //     this.$t.message.toast('下料口不能为空');
                //     return
                // }
                // console.log(ip);
                this.$u.post("api/ToWms/agvTransferList", {
                    MainData: {
                        sn: this.barcode,
                        jobID: this.jobID,
                        // cacheNo: this.cacheNo,
                        // address: this.stationNo,
                        creator: uni.getStorageSync('jo_user').userName,
                        //targetWorkshop:"1"
                    }
                }).then(res => {
                    if (res.status) {
                        this.istrue = false;
                        this.$t.message.toast('校验成功');
                    } else {
                        this.istrue = true;
                        this.$t.message.toast(res.message);
                    }
                }).catch(err => {
                })
            },
            //送至一楼
            submitTransfer() {
                if (this.barcode == '') {
                    this.$t.message.toast('条码不能为空');
                    return
                } else if (this.cacheNo == '') {
                    this.$t.message.toast('缓存架编号不能为空');
                    return
                }
                this.stationNo = "DepThreeForZJYJ";
                this.$u.post("/api/ToWCS/SendFirstFloor", {
                    MainData: {
                        dataSN: this.barcode,
                        cacheNo: this.cacheNo,
                        address: this.stationNo,
                        creator: uni.getStorageSync('jo_user').userName
                    }
                }).then(res => {
                    if (res.status) {
                        this.barcode = '',
                            this.cacheNo = '',
                            this.cacheName = '',
                            this.stationNo = '',
                            this.stationName = '',
                            this.istrue = true;
                        this.$t.message.toast('送至一楼成功,请按提升机下发按钮');
                    } else {
                        this.$t.message.toast(res.message);
                    }
                }).catch(err => {
                })
            },
            stop() {
                // setInterval(function() {
                //     uni.hideKeyboard(); //隐藏软键盘
                // },5);
            },
        },
        onLoad() {
            let _self = this;
            _self.stop()
        }
    }
</script>
<style lang="scss" scoped>
    .isnone {
        display: none;
        width: 100%;
    }
    .isnonone {
        width: 100%;
    }
    // @import '@/static/css/templatePage/custom_nav_bar.scss';
    /* æ‚¬æµ® */
    .rocket-sussuspension {
        animation: suspension 3s ease-in-out infinite;
    }
    @keyframes suspension {
        0%,
        100% {
            transform: translate(0, 0);
        }
        50% {
            transform: translate(-0.8rem, 1rem);
        }
    }
    .login {
        position: relative;
        height: 100%;
        z-index: 1;
        /* èƒŒæ™¯å›¾ç‰‡ start */
        &__bg {
            z-index: -1;
            position: fixed;
            &--top {
                top: 0;
                left: 0;
                right: 0;
                width: 100%;
                .bg {
                    width: 750rpx;
                    will-change: transform;
                }
                .rocket {
                    margin: 50rpx 28%;
                    width: 400rpx;
                    will-change: transform;
                }
            }
            &--bottom {
                bottom: -10rpx;
                left: 0;
                right: 0;
                width: 100%;
                // height: 144px;
                margin-bottom: env(safe-area-inset-bottom);
                image {
                    width: 750rpx;
                    will-change: transform;
                }
            }
        }
        /* èƒŒæ™¯å›¾ç‰‡ end */
        /* å†…容 start */
        &__wrapper {
            margin-top: 403rpx;
            width: 100%;
        }
        /* åˆ‡æ¢ start */
        &__mode {
            position: relative;
            margin: 0 auto;
            width: 476rpx;
            height: 77rpx;
            background-color: #FFFFFF;
            box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
            border-radius: 39rpx;
            &__item {
                height: 77rpx;
                width: 100%;
                line-height: 77rpx;
                text-align: center;
                font-size: 31rpx;
                color: #908f8f;
                letter-spacing: 1em;
                text-indent: 1em;
                z-index: 2;
                transition: all 0.4s;
                &--active {
                    font-weight: bold;
                    color: #FFFFFF;
                }
            }
            &__slider {
                position: absolute;
                height: inherit;
                width: calc(476rpx);
                border-radius: inherit;
                box-shadow: 0rpx 18rpx 72rpx 18rpx rgba(0, 195, 255, 0.1);
                z-index: 1;
                transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
            }
        }
        /* åˆ‡æ¢ end */
        /* ç™»å½•注册信息 start */
        &__info {
            margin: 0 30rpx;
            margin-top: 105rpx;
            padding: 30rpx 51rpx;
            padding-bottom: 0;
            border-radius: 20rpx;
            background-color: #ffff;
            box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
            &__item {
                &__input {
                    margin-top: 59rpx;
                    width: 100%;
                    height: 100rpx;
                    border: 1rpx solid #E6E6E6;
                    border-radius: 39rpx;
                    &__left-icon {
                        width: 10%;
                        font-size: 44rpx;
                        margin-left: 20rpx;
                        color: #AAAAAA;
                    }
                    &__content {
                        width: 80%;
                        padding-left: 10rpx;
                        &--verify-code {
                            width: 56%;
                        }
                        input {
                            font-size: 48rpx;
                            // letter-spacing: 0.1em;
                        }
                    }
                    &__right-icon {
                        width: 10%;
                        font-size: 44rpx;
                        margin-right: 20rpx;
                        color: #AAAAAA;
                    }
                    &__right-verify-code {
                        width: 34%;
                        margin-right: 20rpx;
                    }
                }
                &__button {
                    margin-top: 75rpx;
                    margin-bottom: 39rpx;
                    width: 100%;
                    height: 77rpx;
                    text-align: center;
                    font-size: 48rpx;
                    font-weight: bold;
                    line-height: 77rpx;
                    letter-spacing: 1em;
                    text-indent: 1em;
                    border-radius: 39rpx;
                    box-shadow: 1rpx 10rpx 24rpx 0rpx rgba(60, 129, 254, 0.35);
                }
                &__tips {
                    margin: 30rpx 0;
                    color: #AAAAAA;
                }
            }
        }
        /* ç™»å½•注册信息 end */
        /* ç™»å½•方式切换 start */
        &__way {
            margin: 0 auto;
            margin-top: 110rpx;
            &__item {
                &--icon {
                    width: 77rpx;
                    height: 77rpx;
                    font-size: 50rpx;
                    border-radius: 100rpx;
                    margin-bottom: 18rpx;
                    position: relative;
                    z-index: 1;
                    &::after {
                        content: " ";
                        position: absolute;
                        z-index: -1;
                        width: 100%;
                        height: 100%;
                        left: 0;
                        bottom: 0;
                        border-radius: inherit;
                        opacity: 1;
                        transform: scale(1, 1);
                        background-size: 100% 100%;
                        background-image: url(https://tnuiimage.tnkjapp.com/cool_bg_image/icon_bg5.png);
                    }
                }
            }
        }
        /* ç™»å½•方式切换 end */
        /* å†…容 end */
    }
    /deep/.input-placeholder {
        font-size: 24rpx;
        color: #E6E6E6;
    }
</style>
´úÂë¹ÜÀí/PDA/static/center-selected.png
´úÂë¹ÜÀí/PDA/static/center.png
´úÂë¹ÜÀí/PDA/static/favicon.ico
´úÂë¹ÜÀí/PDA/static/index-selected.png
´úÂë¹ÜÀí/PDA/static/index.png
´úÂë¹ÜÀí/PDA/static/login_bottom_bg.jpg
´úÂë¹ÜÀí/PDA/static/login_top2.jpg
´úÂë¹ÜÀí/PDA/static/login_top3.png
´úÂë¹ÜÀí/PDA/static/logo.png
´úÂë¹ÜÀí/PDA/static/myIcon/1.png
´úÂë¹ÜÀí/PDA/static/myIcon/2.png
´úÂë¹ÜÀí/PDA/static/myIcon/qiu.png
´úÂë¹ÜÀí/PDA/template.h5.html
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="zh-CN">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico">
        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <title>
            <%= htmlWebpackPlugin.options.title %>
        </title>
        <script>
            document.addEventListener('DOMContentLoaded', function() {
                document.documentElement.style.fontSize = document.documentElement.clientWidth / 20 + 'px'
            })
        </script>
        <link rel="stylesheet" href="<%= BASE_URL %>static/index.css" />
    </head>
    <body>
        <noscript>
            <strong>本站点必须要开启JavaScript才能运行</strong>
        </noscript>
        <div id="app"></div>
    </body>
</html>
´úÂë¹ÜÀí/PDA/tuniao-ui/README.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,4 @@
TuniaoUi for uniApp v1.0.0 | by å›¾é¸Ÿ 2021-09-01
仅供开发,如作它用所承受的法律责任一概与作者无关
*使用TuniaoUi开发扩展与插件时,请注明基于tuniao字眼
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-action-sheet/tn-action-sheet.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,202 @@
<template>
  <view v-if="value" class="tn-action-sheet-class tn-action-sheet">
    <tn-popup
      v-model="value"
      mode="bottom"
      length="auto"
      :popup="false"
      :borderRadius="borderRadius"
      :maskCloseable="maskCloseable"
      :safeAreaInsetBottom="safeAreaInsetBottom"
      :zIndex="elZIndex"
      @close="close"
    >
      <!-- æç¤ºä¿¡æ¯ -->
      <view
        v-if="tips.text"
        class="tn-action-sheet__tips tn-border-solid-bottom"
        :style="[tipsStyle]"
      >
        {{tips.text}}
      </view>
      <!-- æŒ‰é’®åˆ—表 -->
      <block v-for="(item, index) in list" :key="index">
        <view
          class="tn-action-sheet__item tn-text-ellipsis"
          :class="[ index < list.length - 1 ? 'tn-border-solid-bottom' : '']"
          :style="[itemStyle(index)]"
          hover-class="tn-hover-class"
          :hover-stay-time="150"
          @tap="itemClick(index)"
          @touchmove.stop.prevent
        >
          <text>{{item.text}}</text>
          <text v-if="item.subText" class="tn-action-sheet__item__subtext tn-text-ellipsis">{{item.subText}}</text>
        </view>
      </block>
      <!-- å–消按钮 -->
      <block v-if="cancelBtn">
        <view class="tn-action-sheet__cancel--gab"></view>
        <view
          class="tn-action-sheet__cancel tn-action-sheet__item"
          hover-class="tn-hover-class"
          :hover-stay-time="150"
          @tap="close"
        >{{cancelText}}</view>
      </block>
    </tn-popup>
  </view>
</template>
<script>
  export default {
    name: 'tn-action-sheet',
    props: {
      // é€šè¿‡v-model控制弹出和收起
      value: {
        type: Boolean,
        default: false
      },
      // æŒ‰é’®æ–‡å­—数组,可以自定义颜色和字体大小
      // return [{
      //     text: '确定',
      //  subText: '这是一个确定按钮',
      //     color: '',
      //     fontSize: '',
      //  disabled: true
      // }]
      list: {
        type: Array,
        default() {
          return []
        }
      },
      // é¡¶éƒ¨æç¤ºæ–‡å­—
      tips: {
        type: Object,
        default() {
          return {
            text: '',
            color: '',
            fontSize: 26
          }
        }
      },
      // å¼¹å‡ºçš„顶部圆角值
      borderRadius: {
        type: Number,
        default: 0
      },
      // ç‚¹å‡»é®ç½©å¯ä»¥å…³é—­
      maskCloseable: {
        type: Boolean,
        default: true
      },
      // åº•部取消按钮
      cancelBtn: {
        type: Boolean,
        default: true
      },
      // åº•部取消按钮的文字
      cancelText: {
        type: String,
        default: '取消'
      },
      // å¼€å¯åº•部安全区域
      // åœ¨iPhoneX机型底部添加一定的内边距
      safeAreaInsetBottom: {
        type: Boolean,
        default: false
      },
      // z-index值
      zIndex: {
        type: Number,
        default: 0
      }
    },
    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 = '#AAAAAA'
          return style
        }
      },
      elZIndex() {
        return this.zIndex ? this.zIndex : this.$t.zIndex.popup
      }
    },
    methods: {
      // ç‚¹å‡»å–消按钮
      close() {
        // å‘送input事件,并不会作用于父组件,而是要设置组件内部通过props传递的value参数
        this.popupClose();
        this.$emit('close');
      },
      // å…³é—­å¼¹çª—
      popupClose() {
        this.$emit('input', false)
      },
      // ç‚¹å‡»å¯¹åº”çš„item
      itemClick(index) {
        // å¦‚果是禁用项则不进行操作
        if (this.list[index].disabled) return
        this.$emit('click', index)
        this.popupClose()
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-action-sheet {
    &__tips {
      font-size: 26rpx;
      text-align: center;
      padding: 34rpx 0;
      line-height: 1;
      color: $tn-content-color;
    }
    &__item {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-size: 32rpx;
      padding: 34rpx 0;
      &__subtext {
        font-size: 24rpx;
        color: $tn-content-color;
        margin-top: 20rpx;
      }
    }
    &__cancel {
      color: $tn-font-color;
      &--gab {
        height: 12rpx;
        background-color: #eaeaec;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-avatar-group/tn-avatar-group.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
<template>
  <view class="tn-avatar-group-class tn-avatar-group">
    <view v-for="(item, index) in lists" :key="index" class="tn-avatar-group__item" :style="[itemStyle(index)]">
      <tn-avatar
        :src="item.src || ''"
        :text="item.text || ''"
        :icon="item.icon || ''"
        :size="size"
        :shape="shape"
        :imgMode="imgMode"
        :border="true"
        backgroundColor="rgba(255, 255, 255, 0.4)"
        :borderSize="4"
      ></tn-avatar>
    </view>
  </view>
</template>
<script>
  export default {
    name: 'tn-avatar-group',
    props: {
      // å¤´åƒåˆ—表
      lists: {
        type: Array,
        default() {
          return []
        }
      },
      // å¤´åƒç±»åž‹
      // square å¸¦åœ†è§’正方形 circle åœ†å½¢
      shape: {
        type: String,
        default: 'circle'
      },
      // å¤§å°
      // sm å°å¤´åƒ lg å¤§å¤´åƒ xl åŠ å¤§å¤´åƒ
      // å¦‚果为其他则认为是直接设置大小
      size: {
        type: [Number, String],
        default: ''
      },
      // å½“设置为显示头像信息时,
      // å›¾ç‰‡çš„裁剪模式
      imgMode: {
        type: String,
        default: 'aspectFill'
      },
      // å¤´åƒä¹‹é—´çš„遮挡比例
      // 0.4 ä»£è¡¨ 40%
      gap: {
        type: Number,
        default: 0.4
      }
    },
    computed: {
      itemStyle() {
        return (index) => {
          let style = {}
          if (this._checkSizeIsInline()) {
            switch(this.size) {
              case 'sm':
                style.marginLeft = index != 0 ? `${-48 * this.gap}rpx` : ''
                break
              case 'lg':
                style.marginLeft = index != 0 ? `${-96 * this.gap}rpx` : ''
                break
              case 'xl':
                style.marginLeft = index != 0 ? `${-128 * this.gap}rpx` : ''
                break
            }
          } else {
            const size = Number(this.size.replace(/(px|rpx)/g, '')) || 64
            style.marginLeft = index != 0 ? `-${size * this.gap}rpx` : ''
          }
          return style
        }
      }
    },
    data() {
      return {
      }
    },
    methods: {
      // æ£€æŸ¥æ˜¯å¦ä½¿ç”¨å†…置的大小进行设置
      _checkSizeIsInline() {
        if (/(xs|sm|md|lg|xl|xxl)/.test(this.size)) return true
        else return false
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-avatar-group {
    display: flex;
    flex-direction: row;
    &__item {
      position: relative;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-avatar/tn-avatar.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,298 @@
<template>
  <view
    class="tn-avatar-class tn-avatar"
    :class="[backgroundColorClass,avatarClass]"
    :style="[avatarStyle]"
    @tap="click"
  >
    <image
      v-if="showImg"
      class="tn-avatar__img"
      :class="[imgClass]"
      :src="src"
      :mode="imgMode || 'aspectFill'"
      @error="loadImageError"
    ></image>
    <view v-else class="tn-avatar__text" >
      <view v-if="text">{{ text }}</view>
      <view v-else :class="[`tn-icon-${icon}`]"></view>
    </view>
    <!-- è§’æ ‡ -->
    <tn-badge
      v-if="badge && (badgeIcon || badgeText)"
      :radius="badgeSize"
      :backgroundColor="badgeBgColor"
      :fontColor="badgeColor"
      :fontSize="badgeSize - 8"
      :absolute="true"
      :top="badgePosition[0]"
      :right="badgePosition[1]"
    >
      <view v-if="badgeIcon && badgeText === ''">
        <view :class="[`tn-icon-${badgeIcon}`]"></view>
      </view>
      <view v-else>
        {{ badgeText }}
      </view>
    </tn-badge>
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    mixins: [componentsColorMixin],
    name: 'tn-avatar',
    props: {
      // åºå·
      index: {
        type: [Number, String],
        default: 0
      },
      // å¤´åƒç±»åž‹
      // square å¸¦åœ†è§’正方形 circle åœ†å½¢
      shape: {
        type: String,
        default: 'circle'
      },
      // å¤§å°
      // sm å°å¤´åƒ lg å¤§å¤´åƒ xl åŠ å¤§å¤´åƒ
      // å¦‚果为其他则认为是直接设置大小
      size: {
        type: [Number, String],
        default: ''
      },
      // æ˜¯å¦æ˜¾ç¤ºé˜´å½±
      shadow: {
        type: Boolean,
        default: false
      },
      // æ˜¯å¦æ˜¾ç¤ºè¾¹æ¡†
      border: {
        type: Boolean,
        default: false
      },
      // è¾¹æ¡†é¢œè‰²
      borderColor: {
        type: String,
        default: 'rgba(0, 0, 0, 0.1)'
      },
      // è¾¹æ¡†å¤§å°, rpx
      borderSize: {
        type: Number,
        default: 2
      },
      // å¤´åƒè·¯å¾„
      src: {
        type: String,
        default: ''
      },
      // æ–‡å­—
      text: {
        type: String,
        default: ''
      },
      // å›¾æ ‡
      icon: {
        type: String,
        default: ''
      },
      // å½“设置为显示头像信息时,
      // å›¾ç‰‡çš„裁剪模式
      imgMode: {
        type: String,
        default: 'aspectFill'
      },
      // æ˜¯å¦æ˜¾ç¤ºè§’æ ‡
      badge: {
        type: Boolean,
        default: false
      },
      // è®¾ç½®æ˜¾ç¤ºè§’标后,角标大小
      badgeSize: {
        type: Number,
        default: 0
      },
      // è§’标背景颜色
      badgeBgColor: {
        type: String,
        default: '#AAAAAA'
      },
      // è§’标字体颜色
      badgeColor: {
        type: String,
        default: '#FFFFFF'
      },
      // è§’标图标
      badgeIcon: {
        type: String,
        default: ''
      },
      // è§’标文字,优先级比icon高
      badgeText: {
        type: String,
        default: ''
      },
      // è§’标坐标
      // [top, right]
      badgePosition: {
        type: Array,
        default() {
          return [0, 0]
        }
      }
    },
    data() {
      return {
        // å›¾ç‰‡æ˜¾ç¤ºæ˜¯å¦å‘生错误
        imgLoadError: false
      }
    },
    computed: {
      showImg() {
        // å¦‚果设置了图片地址,则为显示图片,否则为显示文本
        return this.text === '' && this.icon === ''
      },
      avatarClass() {
        let clazz = ''
        clazz += ` tn-avatar--${this.shape}`
        if (this._checkSizeIsInline()) {
          clazz += ` tn-avatar--${this.size}`
        }
        if (this.shadow) {
          clazz += ' tn-avatar--shadow'
        }
        return clazz
      },
      avatarStyle() {
        let style = {}
        if (this.backgroundColorStyle) {
          style.background = this.backgroundColorStyle
        } else if (this.shadow && this.showImg) {
          style.backgroundImage = `url(${this.src})`
        }
        if (this.border) {
          style.border = `${this.borderSize}rpx solid ${this.borderColor}`
        }
        if (!this._checkSizeIsInline()) {
          style.width = this.size
          style.height = this.size
        }
        return style
      },
      imgClass() {
        let clazz = ''
        clazz += ` tn-avatar__img--${this.shape}`
        return clazz
      }
    },
    methods: {
      // åŠ è½½å›¾ç‰‡å¤±è´¥
      loadImageError() {
        this.imgLoadError = true
      },
      // ç‚¹å‡»äº‹ä»¶
      click() {
        this.$emit("click", this.index)
      },
      // æ£€æŸ¥æ˜¯å¦ä½¿ç”¨å†…置的大小进行设置
      _checkSizeIsInline() {
        if (/^(xs|sm|md|lg|xl|xxl)$/.test(this.size)) return true
        else return false
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-avatar {
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
    margin: 0;
    padding: 0;
    text-align: center;
    align-items: center;
    justify-content: center;
    background-color: $tn-font-holder-color;
    // color: #FFFFFF;
    white-space: nowrap;
    position: relative;
    width: 64rpx;
    height: 64rpx;
    z-index: 1;
    &--sm {
      width: 48rpx;
      height: 48rpx;
    }
    &--lg {
      width: 96rpx;
      height: 96rpx;
    }
    &--xl {
      width: 128rpx;
      height: 128rpx;
    }
    &--square {
      border-radius: 10rpx;
    }
    &--circle {
      border-radius: 5000rpx;
    }
    &--shadow {
      position: relative;
      &::after {
        content: " ";
        display: block;
        background: inherit;
        filter: blur(10rpx);
        position: absolute;
        width: 100%;
        height: 100%;
        top: 10rpx;
        left: 10rpx;
        z-index: -1;
        opacity: 0.4;
        transform-origin: 0 0;
        border-radius: inherit;
        transform: scale(1, 1);
      }
    }
    &__img {
      width: 100%;
      height: 100%;
      &--square {
        border-radius: 10rpx;
      }
      &--circle {
        border-radius: 5000rpx;
      }
    }
    &__text {
      display: flex;
      flex-direction: row;
      align-items: center;
      justify-content: center;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-badge/tn-badge.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,173 @@
<template>
  <view
    class="tn-badge-class tn-badge"
    :class="[
      backgroundColorClass,
      fontColorClass,
      badgeClass
    ]"
    :style="[badgeStyle]"
    @click="handleClick"
  >
    <slot v-if="!dot"></slot>
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    mixins: [componentsColorMixin],
    name: 'tn-badge',
    props: {
      // åºå·
      index: {
        type: [Number, String],
        default: '0'
      },
      // å¾½ç« çš„大小 rpx
      radius: {
        type: Number,
        default: 0
      },
      // å†…边距
      padding: {
        type: String,
        default: ''
      },
      // å¤–边距
      margin: {
        type: String,
        default: ''
      },
      // æ˜¯å¦ä¸ºä¸€ä¸ªç‚¹
      dot: {
        type: Boolean,
        default: false
      },
      // æ˜¯å¦ä½¿ç”¨ç»å¯¹å®šä½
      absolute: {
        type: Boolean,
        default: false
      },
      // top
      top: {
        type: [String, Number],
        default: ''
      },
      // right
      right: {
        type: [String, Number],
        default: ''
      },
      // å±…中 å¯¹é½å³ä¸Šè§’
      translateCenter: {
        type: Boolean,
        default: true
      }
    },
    computed: {
      badgeClass() {
        let clazz = ''
        if (this.dot) {
          clazz += ' tn-badge--dot'
        }
        if (this.absolute) {
          clazz += ' tn-badge--absolute'
          if (this.translateCenter) {
            clazz += ' tn-badge--center-position'
          }
        }
        return clazz
      },
      badgeStyle() {
        let style = {}
        if (this.radius !== 0) {
          style.width = this.radius + 'rpx'
          style.height = this.radius + 'rpx'
          style.lineHeight = this.radius + 'rpx'
          // style.borderRadius = (this.radius * 8) + 'rpx'
        }
        if (this.padding) {
          style.padding = this.padding
        }
        if (this.margin) {
          style.margin = this.margin
        }
        if (this.fontColorStyle) {
          style.color = this.fontColorStyle
        }
        if (this.fontSize) {
          style.fontSize = this.fontSize + this.fontUnit
        }
        if (this.backgroundColorStyle) {
          style.backgroundColor = this.backgroundColorStyle
        }
        if (this.top) {
          style.top = this.$t.string.getLengthUnitValue(this.top)
        }
        if (this.right) {
          style.right = this.$t.string.getLengthUnitValue(this.right)
        }
        return style
      },
    },
    data() {
      return {
      }
    },
    methods: {
      // å¤„理点击事件
      handleClick() {
        this.$emit('click', {
          index: Number(this.index)
        })
        this.$emit('tap', {
          index: Number(this.index)
        })
      },
    }
  }
</script>
<style lang="scss" scoped>
  .tn-badge {
    width: auto;
    height: auto;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 10;
    font-size: 20rpx;
    background-color: #FFFFFF;
    // color: #FFFFFF;
    border-radius: 100rpx;
    padding: 4rpx 8rpx;
    line-height: initial;
    &--dot {
      width: 8rpx;
      height: 8rpx;
      border-radius: 50%;
      padding: 0;
    }
    &--absolute {
      position: absolute;
      top: 0;
      right: 0;
    }
    &--center-position {
      transform: translate(50%, -50%);
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-button/tn-button.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,302 @@
<template>
  <button
    class="tn-btn-class tn-btn"
    :class="[
      buttonClass,
      backgroundColorClass,
      fontColorClass
    ]"
    :style="[buttonStyle]"
    hover-class="tn-hover"
    :loading="loading"
    :disabled="disabled"
    :form-type="formType"
    :open-type="openType"
    @getuserinfo="handleGetUserInfo"
    @getphonenumber="handleGetPhoneNumber"
    @contact="handleContact"
    @error="handleError"
    @tap="handleClick"
  >
    <slot></slot>
  </button>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    mixins: [componentsColorMixin],
    name: "tn-button",
    // è§£å†³å†å¾®ä¿¡å°ç¨‹åºç§ï¼Œè‡ªå®šä¹‰æŒ‰é’®æ— æ³•触发bindsubmit
    behaviors: ['wx://form-field-button'],
    props: {
      // æŒ‰é’®ç´¢å¼•,用于区分多个按钮
      index: {
        type: [Number, String],
        default: 0
      },
      // æŒ‰é’®å½¢çж default é»˜è®¤ round åœ†è§’ icon å›¾æ ‡æŒ‰é’®
      shape: {
        type: String,
        default: 'default'
      },
      // æ˜¯å¦åŠ é˜´å½±
      shadow: {
        type: Boolean,
        default: false
      },
      // å®½åº¦ rpx或%
      width: {
        type: String,
        default: 'auto'
      },
      // é«˜åº¦ rpx或%
      height: {
        type: String,
        default: ''
      },
      // æŒ‰é’®çš„尺寸 sm lg
      size: {
        type: String,
        default: ''
      },
      // å­—体是否加粗
      fontBold: {
        type: Boolean,
        default: false
      },
      padding: {
        type: String,
        default: '0 30rpx'
      },
      // å¤–边距 ä¸Žcss的margin参数用法相同
      margin: {
        type: String,
        default: ''
      },
      // æ˜¯å¦é•‚空
      plain: {
        type: Boolean,
        default: false
      },
      // å½“plain=true时,是否显示边框
      border: {
        type: Boolean,
        default: true
      },
      // å½“plain=true时,是否加粗显示边框
      borderBold: {
        type: Boolean,
        default: false
      },
      // æ˜¯å¦ç¦ç”¨
      disabled: {
        type: Boolean,
        default: false
      },
      // æ˜¯å¦æ˜¾ç¤ºåŠ è½½å›¾æ ‡
      loading: {
        type: Boolean,
        default: false
      },
      // è§¦å‘form表单的事件类型
      formType: {
        type: String,
        default: ''
      },
      // å¼€æ”¾èƒ½åŠ›
      openType: {
        type: String,
        default: ''
      },
      // æ˜¯å¦é˜»æ­¢é‡å¤ç‚¹å‡»(默认间隔是200ms)
      blockRepeatClick: {
        type: Boolean,
        default: false
      }
    },
    computed: {
      // æ ¹æ®ä¸åŒçš„参数动态生成class
      buttonClass() {
        let clazz = ''
        // æŒ‰é’®å½¢çж
        switch (this.shape) {
          case 'icon':
          case 'round':
            clazz += ' tn-round'
            break
        }
        // é˜´å½±
        if (this.shadow) {
          if (this.backgroundColorClass !== '' && this.backgroundColorClass.indexOf('tn-bg') != -1) {
            const color = this.backgroundColor.slice(this.backgroundColor.lastIndexOf('-') + 1)
            clazz += ` tn-shadow-${color}`
          } else {
            clazz += ' tn-shadow-blur'
          }
        }
        // å­—体加粗
        if (this.fontBold) {
          clazz += ' tn-text-bold'
        }
        // è®¾ç½®ä¸ºé•‚空并且设置镂空便可才进行设置
        if (this.plain) {
          clazz += ' tn-btn--plain'
          if (this.border) {
            clazz += ' tn-border-solid'
            if (this.borderBold) {
              clazz += ' tn-bold-border'
            }
            if (this.backgroundColor !== '' && this.backgroundColor.includes('tn-bg')) {
              const color = this.backgroundColor.slice(this.backgroundColor.lastIndexOf('-') + 1)
              clazz += ` tn-border-${color}`
            }
          }
        }
        return clazz
      },
      // æŒ‰é’®çš„æ ·å¼
      buttonStyle() {
        let style = {}
        switch(this.size) {
          case 'sm':
            style.padding = '0 20rpx'
            style.fontSize = '22rpx'
            style.height = this.height || '48rpx'
            break
          case 'lg':
            style.padding = '0 40rpx'
            style.fontSize = '32rpx'
            style.height = this.height || '80rpx'
            break
          default :
            style.padding = '0 30rpx'
            style.fontSize = '28rpx'
            style.height = this.height || '64rpx'
        }
        // æ˜¯å¦æ‰‹åŠ¨è®¾ç½®äº†å†…è¾¹è·
        if (this.padding) {
          style.padding = this.padding
        }
        // æ˜¯å¦æ‰‹åŠ¨è®¾ç½®å¤–è¾¹è·
        if (this.margin) {
          style.margin = this.margin
        }
        // æ˜¯å¦æ‰‹åŠ¨è®¾ç½®äº†å­—ä½“å¤§å°
        if (this.fontSize) {
          style.fontSize = this.fontSize + this.fontUnit
        }
        style.width = this.shape === 'icon' ? style.height : this.width
        style.padding = this.shape === 'icon' ? '0' : style.padding
        if (this.fontColorStyle) {
          style.color = this.fontColorStyle
        }
        if (!this.backgroundColorClass) {
          if (this.plain) {
            style.borderColor = this.backgroundColorStyle || '#080808'
          } else {
            style.backgroundColor = this.backgroundColorStyle || '#FFFFFF'
          }
        }
        // è®¾ç½®é˜´å½±
        if (this.shadow && !this.backgroundColorClass) {
          if (this.backgroundColorStyle.indexOf('#') != -1) {
            style.boxShadow = `6rpx 6rpx 8rpx ${(this.backgroundColorStyle || '#000000')}10`
          } else if (this.backgroundColorStyle.indexOf('rgb') != -1 || this.backgroundColorStyle.indexOf('rgba') != -1 || !this.backgroundColorStyle) {
            style.boxShadow = `6rpx 6rpx 8rpx ${(this.backgroundColorStyle || 'rgba(0, 0, 0, 0.1)')}`
          }
        }
        return style
      },
    },
    data() {
      return {
        // ä¸Šæ¬¡ç‚¹å‡»çš„æ—¶é—´
        clickTime: 0,
        // ä¸¤æ¬¡ç‚¹å‡»é˜²æŠ–的间隔时间
        clickIntervalTime: 200
      }
    },
    methods: {
      // æŒ‰é’®ç‚¹å‡»äº‹ä»¶
      handleClick() {
        if (this.disabled) {
          return
        }
        if (this.blockRepeatClick) {
          const nowTime = new Date().getTime()
          if (nowTime - this.clickTime <= this.clickIntervalTime) {
            return
          }
          this.clickTime = nowTime
          setTimeout(() => {
            this.clickTime = 0
          }, this.clickIntervalTime)
        }
        this.$emit('click', {
          index: Number(this.index)
        })
        // å…¼å®¹tap事件
        this.$emit('tap', {
          index: Number(this.index)
        })
      },
      handleGetUserInfo({ detail = {} } = {}) {
          this.$emit('getuserinfo', detail);
      },
      handleContact({ detail = {} } = {}) {
          this.$emit('contact', detail);
      },
      handleGetPhoneNumber({ detail = {} } = {}) {
          this.$emit('getphonenumber', detail);
      },
      handleError({ detail = {} } = {}) {
          this.$emit('error', detail);
      },
    }
  }
</script>
<style lang="scss" scoped>
  .tn-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    line-height: 1;
    text-align: center;
    text-decoration: none;
    overflow: visible;
    transform: translate(0rpx, 0rpx);
    // background-color: $tn-mai
    border-radius: 12rpx;
    // color: $tn-font-color;
    margin: 0;
    &--plain {
      background-color: transparent !important;
      background-image: none;
      &.tn-round {
        border-radius: 1000rpx !important;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-calendar/tn-calendar.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,707 @@
<template>
  <tn-popup
    v-model="value"
    mode="bottom"
    :popup="false"
    length="auto"
    :borderRadius="borderRadius"
    :safeAreaInsetBottom="safeAreaInsetBottom"
    :maskCloseable="maskCloseable"
    :closeBtn="closeBtn"
    :zIndex="elIndex"
    @close="close"
  >
    <view class="tn-calendar-class tn-calendar">
      <!-- å¤´éƒ¨ -->
      <view class="tn-calendar__header">
        <view v-if="!$slots.tooltip || !$slots.$tooltip" class="tn-calendar__header__text">
          {{ toolTips }}
        </view>
        <view v-else>
          <slot name="tooltip"></slot>
        </view>
      </view>
      <!-- æ“ä½œæç¤ºä¿¡æ¯ -->
      <view class="tn-calendar__action">
        <view v-if="changeYear" class="tn-calendar__action__icon" :style="{backgroundColor: yearArrowColor}" @tap.stop="changeYearHandler(false)">
          <view><text class="tn-icon-left"></text></view>
        </view>
        <view v-if="changeMonth" class="tn-calendar__action__icon" :style="{backgroundColor: monthArrowColor}" @tap.stop="changeMonthHandler(false)">
          <view><text class="tn-icon-left"></text></view>
        </view>
        <view class="tn-calendar__action__text">{{ dateTitle }}</view>
        <view v-if="changeMonth" class="tn-calendar__action__icon" :style="{backgroundColor: monthArrowColor}" @tap.stop="changeMonthHandler(true)">
          <view><text class="tn-icon-right"></text></view>
        </view>
        <view v-if="changeYear" class="tn-calendar__action__icon" :style="{backgroundColor: yearArrowColor}" @tap.stop="changeYearHandler(true)">
          <view><text class="tn-icon-right"></text></view>
        </view>
      </view>
      <!-- æ˜ŸæœŸä¸­æ–‡æ ‡è¯† -->
      <view class="tn-calendar__week-day-zh">
        <view v-for="(item,index) in weekDayZh" :key="index" class="tn-calendar__week-day-zh__text">{{ item }}</view>
      </view>
      <!-- æ—¥åކ䏻体 -->
      <view class="tn-calendar__content">
        <!-- å‰ç½®ç©ºç™½éƒ¨åˆ† -->
        <block v-for="(item, index) in weekdayArr" :key="index">
          <view class="tn-calendar__content__item"></view>
        </block>
        <view
          v-for="(item, index) in daysArr"
          :key="index"
          class="tn-calendar__content__item"
          :class="{
            'tn-hover': disabledChoose(year, month, index + 1),
            'tn-calendar__content--start-date': (mode === 'range' && startDate == `${year}-${month}-${index+1}`) || mode === 'date',
            'tn-calendar__content--end-date': (mode === 'range' && endDate == `${year}-${month}-${index+1}`) || mode === 'date'
          }"
          :style="{
            backgroundColor: colorValue(index, 'bg')
          }"
          @tap.stop="dateClick(index)"
        >
          <view class="tn-calendar__content__item__text" :style="{color: colorValue(index, 'text')}">
            <view>{{ item.day }}</view>
          </view>
          <view class="tn-calendar__content__item__tips" :style="{color: item.color}">
            {{ item.bottomInfo }}
          </view>
        </view>
        <view class="tn-calendar__content__month--bg">{{ month }}</view>
      </view>
      <!-- åº•部 -->
      <view class="tn-calendar__bottom">
        <view class="tn-calendar__bottom__choose">
          <text>{{ mode === 'date' ? activeDate : startDate }}</text>
          <text v-if="endDate">至{{ endDate }}</text>
        </view>
        <view class="tn-calendar__bottom__btn" :style="{backgroundColor: btnColor}" @click="handleBtnClick(false)">
          <view class="tn-calendar__bottom__btn--text">确定</view>
        </view>
      </view>
    </view>
  </tn-popup>
</template>
<script>
  import Calendar from '../../libs/utils/calendar.js'
  export default {
    name: 'tn-calendar',
    props: {
      // åŒå‘绑定控制组件弹出与收起
      value: {
        type: Boolean,
        default: false
      },
      // æ¨¡å¼
      // date -> å•日期 range -> æ—¥æœŸèŒƒå›´
      mode: {
        type: String,
        default: 'date'
      },
      // æ˜¯å¦å…è®¸åˆ‡æ¢å¹´ä»½
      changeYear: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦å…è®¸åˆ‡æ¢æœˆä»½
      changeMonth: {
        type: Boolean,
        default: true
      },
      // å¯åˆ‡æ¢çš„æœ€å¤§å¹´ä»½
      maxYear: {
        type: [Number, String],
        default: 2100
      },
      // å¯åˆ‡æ¢çš„æœ€å°å¹´ä»½
      minYear: {
        type: [Number, String],
        default: 1970
      },
      // æœ€å°æ—¥æœŸ(不在范围被不允许选择)
      minDate: {
        type: String,
        default: '1970-01-01'
      },
      // æœ€å¤§æ—¥æœŸï¼Œå¦‚果为空则默认为今天
      maxDate: {
        type: String,
        default: ''
      },
      // åˆ‡æ¢æœˆä»½æŒ‰é’®çš„颜色
      monthArrowColor: {
        type: String,
        default: '#AAAAAA'
      },
      // åˆ‡æ¢å¹´ä»½æŒ‰é’®çš„颜色
      yearArrowColor: {
        type: String,
        default: '#C8C8C8'
      },
      // é»˜è®¤å­—体颜色
      color: {
        type: String,
        default: '#080808'
      },
      // é€‰ä¸­|起始结束日期背景颜色
      activeBgColor: {
        type: String,
        default: '#01BEFF'
      },
      // é€‰ä¸­|起始结束日期文字颜色
      activeColor: {
        type: String,
        default: '#FFFFFF'
      },
      // èŒƒå›´æ—¥æœŸå†…的背景颜色
      rangeBgColor: {
        type: String,
        default: '#E6E6E655'
      },
      // èŒƒå›´æ—¥æœŸå†…的文字颜色
      rangeColor: {
        type: String,
        default: '#01BEFF'
      },
      // èµ·å§‹æ—¥æœŸæ˜¾ç¤ºçš„æ–‡å­—,mode=range时生效
      startText: {
        type: String,
        default: '开始'
      },
      // ç»“束日期显示的文字,mode=range时生效
      endText: {
        type: String,
        default: '结束'
      },
      // æŒ‰é’®èƒŒæ™¯é¢œè‰²
      btnColor: {
        type: String,
        default: '#01BEFF'
      },
      // å†œåŽ†æ–‡å­—çš„é¢œè‰²
      lunarColor: {
        type: String,
        default: '#AAAAAA'
      },
      // é€‰ä¸­æ—¥æœŸæ˜¯å¦æœ‰é€‰ä¸­æ•ˆæžœ
      isActiveCurrent: {
        type: Boolean,
        default: true
      },
      // åˆ‡æ¢å¹´æœˆæ˜¯å¦è§¦å‘事件,mode=date时生效
      isChange: {
        type: Boolean,
        default: false
      },
      // æ˜¯å¦æ˜¾ç¤ºå†œåކ
      showLunar: {
        type: Boolean,
        default: true
      },
      // é¡¶éƒ¨æç¤ºæ–‡å­—
      toolTips: {
        type: String,
        default: '请选择日期'
      },
      // æ˜¾ç¤ºåœ†è§’的大小
      borderRadius: {
        type: Number,
        default: 8
      },
      // æ˜¯å¦å¼€å¯åº•部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
      safeAreaInsetBottom: {
          type: Boolean,
          default: false
      },
      // æ˜¯å¦å¯ä»¥é€šè¿‡ç‚¹å‡»é®ç½©è¿›è¡Œå…³é—­
      maskCloseable: {
          type: Boolean,
          default: true
      },
      // zIndex
      zIndex: {
        type: Number,
        default: 0
      },
      // æ˜¯å¦æ˜¾ç¤ºå…³é—­æŒ‰é’®
      closeBtn: {
        type: Boolean,
        default: false
      },
    },
    computed: {
      dateChange() {
        return `${this.mode}-${this.minDate}-${this.maxDate}`
      },
      elIndex() {
        return this.zIndex ? this.zIndex : this.$t.zIndex.popup
      },
      colorValue() {
        return (index, type) => {
          let color = type === 'bg' ? '' : 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.mode === 'date' && this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) {
            color = type === 'bg' ? this.activeBgColor : this.activeColor
          } else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) {
            color = type === 'bg' ? this.rangeBgColor : this.rangeColor
          }
          return color
        }
      }
    },
    data() {
      return {
        // æ˜ŸæœŸå‡ ï¼Œ1-7
        weekday: 1,
        weekdayArr: [],
        // æ˜ŸæœŸå¯¹åº”的中文
        weekDayZh: ['日','一','二','三','四','五','六'],
        // å½“前月有多少天
        days: 0,
        daysArr: [],
        year: 2021,
        month: 0,
        day: 0,
        startYear: 0,
        startMonth: 0,
        startDay: 0,
        endYear: 0,
        endMonth: 0,
        endDay: 0,
        today: '',
        activeDate: '',
        startDate: '',
        endDate: '',
        min: null,
        max: null,
        // æ—¥æœŸæ ‡é¢˜
        dateTitle: '',
        // æ ‡è®°æ˜¯å¦å·²ç»é€‰æ‹©äº†å¼€å§‹æ—¥æœŸ
        chooseStart: false
      }
    },
    watch: {
      dateChange() {
        this.init()
      }
    },
    created() {
      this.init()
    },
    methods: {
      // åˆå§‹åŒ–
      init() {
        let now = new Date()
        this.year = now.getFullYear()
        this.month = now.getMonth() + 1
        this.day = now.getDate()
        this.today = `${this.year}-${this.month}-${this.day}`
        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.endDate = ''
        this.endYear = 0
        this.endMonth = 0
        this.endDay = 0
        this.chooseStart = false
        this.changeData()
      },
      // åˆ‡æ¢æœˆä»½
      changeMonthHandler(add) {
        if (add) {
          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(add) {
        let year = add ? this.year + 1 : this.year - 1
        if (!this.checkRange(year)) {
          this.year = year
          this.changeData()
        }
      },
      // æ—¥æœŸç‚¹å‡»äº‹ä»¶
      dateClick(day) {
        day += 1
        if (!this.disabledChoose(this.year, this.month, day)) {
          this.day = day
          let date = `${this.year}-${this.month}-${day}`
          if (this.mode === 'date') {
            this.activeDate = date
          } else {
            let startTimeCompare = new Date(date.replace(/\-/g,'/')).getTime() < new Date(this.startDate.replace(/\-/g,'/')).getTime()
            if (!this.chooseStart || startTimeCompare) {
              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.chooseStart = true
            } else {
              this.endDate = date
              this.endYear = this.year
              this.endMonth = this.month
              this.endDay = this.day
              this.chooseStart = false
            }
          }
          this.daysArr = this.handleDaysArr()
        }
      },
      // ä¿®æ”¹æ—¥æœŸæ•°æ®
      changeData() {
        this.days = this.getMonthDay(this.year, this.month)
        this.daysArr = this.handleDaysArr()
        this.weekday = this.getMonthFirstWeekDay(this.year, this.month)
        this.weekdayArr = this.generateArray(1, this.weekday)
        this.dateTitle = `${this.year}å¹´${this.month}月`
        if (this.isChange && this.mode === 'date') {
          this.handleBtnClick(true)
        }
      },
      // å¤„理按钮点击
      handleBtnClick(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.formatNumber(month)}-${this.formatNumber(day)}`
          let weekText = this.getWeekText(result)
          let isToday = false
          if (`${year}-${month}-${day}` === this.today) {
            isToday = true
          }
          this.$emit('change', {
            year,
            month,
            day,
            days,
            week: weekText,
            isToday,
            date: result,
            // æ˜¯å¦ä¸ºåˆ‡æ¢å¹´æœˆæ“ä½œ
            switch: show
          })
        } else {
          if (!this.startDate || !this.endDate) return
          let startMonth = this.formatNumber(this.startMonth)
          let startDay = this.formatNumber(this.startDay)
          let startDate = `${this.startYear}-${startMonth}-${startDay}`
          let startWeek = this.getWeekText(startDate)
          let endMonth = this.formatNumber(this.endMonth)
          let endDay = this.formatNumber(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,
            startWeek,
            endYear: this.endYear,
            endMonth: this.endMonth,
            endDay: this.endDay,
            endDate,
            endWeek
          })
        }
      },
      // åˆ¤æ–­æ˜¯å¦å…è®¸é€‰æ‹©
      disabledChoose(year, month, day) {
        let flag = true
        let date = `${year}/${month}/${day}`
        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()) {
          flag = false
        }
        return flag
      },
      // æ£€æŸ¥æ˜¯å¦åœ¨æ—¥æœŸèŒƒå›´å†…
      checkRange(year) {
        let overstep = false
        if (year < this.minYear || year > this.maxYear) {
          uni.showToast({
            title: '所选日期超出范围',
            icon: 'none'
          })
          overstep = true
        }
        return overstep
      },
      // å¤„理日期
      initDate(date) {
        let fdate = date.split('-')
        return {
          year: Number(fdate[0] || 1970),
          month: Number(fdate[1] || 1),
          day: Number(fdate[2] || 1)
        }
      },
      // å¤„理日期数组
      handleDaysArr() {
        let days = this.generateArray(1, this.days)
        let daysArr = days.map((item) => {
          let bottomInfo = this.showLunar ? Calendar.solar2lunar(this.year, this.month, item).IDayCn : ''
          let color = this.showLunar ? this.lunarColor : this.activeColor
          if (
            (this.mode === 'date' && this.day == item) ||
            (this.mode === 'range' && (this.startDay == item || this.endDay == item))
          ) {
            color = this.activeColor
          }
          if (this.mode === 'range') {
            if (this.startDay == item && this.startDay != this.endDay) {
              bottomInfo = this.startText
            }
            if (this.endDay == item) {
              bottomInfo = this.endText
            }
          }
          return {
            day: item,
            color: color,
            bottomInfo: bottomInfo
          }
        })
        return daysArr
      },
      // èŽ·å–å¯¹åº”æœˆæœ‰å¤šå°‘å¤©
      getMonthDay(year, month) {
        return new Date(year, month, 0).getDate()
      },
      // èŽ·å–å¯¹åº”æœˆçš„ç¬¬ä¸€å¤©æ—¶æ˜ŸæœŸå‡ 
      getMonthFirstWeekDay(year, month) {
        return new Date(`${year}/${month}/01 00:00:00`).getDay()
      },
      // èŽ·å–å¯¹åº”æ˜ŸæœŸçš„æ–‡æœ¬
      getWeekText(date) {
        date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`)
        let week = date.getDay()
        return '星期' + this.weekDayZh[week]
      },
      // ç”Ÿæˆæ—¥æœŸå¤©æ•°æ•°ç»„
      generateArray(start, end) {
        return Array.from(new Array(end + 1).keys()).slice(start)
      },
      // æ ¼å¼åŒ–æ•°å­—
      formatNumber(num) {
        return num < 10 ? '0' + num : num + ''
      },
      // å…³é—­çª—口
      close() {
        this.$emit('input', false)
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-calendar {
    color: $tn-font-color;
    &__header {
      width: 100%;
      box-sizing: border-box;
      font-size: 30rpx;
      background-color: #FFFFFF;
      color: $tn-main-color;
      &__text {
        display: flex;
        flex-direction: row;
        align-items: center;
        justify-content: center;
        margin-top: 30rpx;
        padding: 0 60rpx;
      }
    }
    &__action {
      display: flex;
      flex-direction: row;
      justify-content: center;
      align-items: center;
      padding: 40rpx 0 40rpx 0;
      &__icon {
        display: flex;
        align-items: center;
        justify-content: center;
        margin: 0 16rpx;
        width: 32rpx;
        height: 32rpx;
        font-size: 20rpx;
        // line-height: 32rpx;
        border-radius: 50%;
        color: #FFFFFF;
      }
      &__text {
        padding: 0 16rpx;
        color: $tn-font-color;
        font-size: 32rpx;
        font-weight: bold;
      }
    }
    &__week-day-zh {
      display: flex;
      flex-direction: row;
      align-items: center;
      justify-content: center;
      padding: 12rpx 0;
      overflow: hidden;
      box-shadow: 16rpx 6rpx 8rpx 0 #E6E6E6;
      margin-bottom: 2rpx;
      &__text {
        flex: 1;
        text-align: center;
      }
    }
    &__content {
      display: flex;
      flex-direction: row;
      flex-wrap: wrap;
      width: 100%;
      padding: 12rpx 0;
      box-sizing: border-box;
      background-color: #F7F7F7;
      position: relative;
      &__item {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        width: 14.2857%;
        padding: 12rpx 0;
        margin: 6rpx 0;
        overflow: hidden;
        position: relative;
        z-index: 2;
        // box-shadow: inset 0rpx 0rpx 22rpx 4rpx rgba(255,255,255, 0.52);
        &__text {
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          height: 80rpx;
          font-size: 32rpx;
          position: relative;
        }
        &__tips {
          position: absolute;
          width: 100%;
          line-height: 24rpx;
          left: 0;
          bottom: 8rpx;
          text-align: center;
          z-index: 2;
          transform-origin: center center;
          transform: scale(0.8);
        }
      }
      &--start-date {
        border-top-left-radius: 8rpx;
        border-bottom-left-radius: 8rpx;
      }
      &--end-date {
        border-top-right-radius: 8rpx;
        border-bottom-right-radius: 8rpx;
      }
      &__month {
        &--bg {
          position: absolute;
          font-size: 200rpx;
          line-height: 200rpx;
          left: 50%;
          top: 50%;
          transform: translate(-50%, -50%);
          color: $tn-font-holder-color;
          z-index: 1;
        }
      }
    }
    &__bottom {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      width: 100%;
      background-color: #F7F7F7;
      padding: 0 40rpx 30rpx;
      box-sizing: border-box;
      font-size: 24rpx;
      color: $tn-font-sub-color;
      &__choose {
        height: 50rpx;
      }
      &__btn {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 100%;
        height: 60rpx;
        border-radius: 40rpx;
        color: #FFFFFF;
        font-size: 28rpx;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-car-keyboard/tn-car-keyboard.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,320 @@
<template>
  <view class="tn-car-keyboard-class tn-car-keyboard" @touchmove.stop.prevent="() => {}">
    <view class="tn-car-keyboard__grids">
      <view
        v-for="(data, index) in inputCarNumber ? endKeyBoardList : areaList"
        :key="index"
        class="tn-car-keyboard__grids__item"
      >
        <view
          v-for="(sub_data, sub_index) in data"
          :key="sub_index"
          class="tn-car-keyboard__grids__btn"
          :class="{'tn-car-keyboard__grids__btn--disabled': sub_data === 'I'}"
          :hover-class="sub_data !== 'I' ? 'tn-car-keyboard--hover' : ''"
          :hover-stay-time="100"
          @tap="click(index, sub_index)"
        >
          {{ sub_data }}
        </view>
      </view>
      <view
        class="tn-car-keyboard__back"
        hover-class="tn-hover-class"
        :hover-stay-time="150"
        @touchstart.stop="backspaceClick"
        @touchend="clearTimer"
      >
        <view class="tn-icon-left-arrow tn-car-keyboard__back__icon"></view>
      </view>
      <view
        class="tn-car-keyboard__change"
        hover-class="tn-car-keyboard--hover"
        :hover-stay-time="150"
        @tap="changeMode"
      >
        <text class="tn-car-keyboard__mode--zh" :class="[`tn-car-keyboard__mode--${!inputCarNumber ? 'active' : 'inactive'}`]">中</text>
        /
        <text class="tn-car-keyboard__mode--en" :class="[`tn-car-keyboard__mode--${inputCarNumber ? 'active' : 'inactive'}`]">英</text>
      </view>
    </view>
  </view>
</template>
<script>
  export default {
    name: 'tn-car-keyboard',
    props: {
      // æ˜¯å¦æ‰“乱键盘顺序
      randomEnabled: {
        type: Boolean,
        default: false
      },
      // åˆ‡æ¢ä¸­è‹±æ–‡è¾“å…¥
      switchEnMode: {
        type: Boolean,
        default: false
      }
    },
    computed: {
      areaList() {
        let data = [
          '京',
          '沪',
          '粤',
          'æ´¥',
          '冀',
          '豫',
          '云',
          'è¾½',
          '黑',
          '湘',
          '皖',
          '鲁',
          '苏',
          '浙',
          'èµ£',
          '鄂',
          '桂',
          '甘',
          '晋',
          '陕',
          '蒙',
          '吉',
          '闽',
          'è´µ',
          '渝',
          '川',
          '青',
          '琼',
          '宁',
          '藏',
          '港',
          'æ¾³',
          '新',
          '使',
          'å­¦',
          '临',
          'è­¦'
        ]
        // æ‰“乱顺序
        if (this.randomEnabled) data = this.$t.array.random(data)
        // åˆ‡å‰²äºŒç»´æ•°ç»„
        let showData = []
        showData[0] = data.slice(0, 10)
        showData[1] = data.slice(10, 20)
        showData[2] = data.slice(20, 30)
        showData[3] = data.slice(30, 37)
        return showData
      },
      endKeyBoardList() {
        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'
        ]
        // æ‰“乱顺序
        if (this.randomEnabled) data = this.$t.array.random(data)
        // åˆ‡å‰²äºŒç»´æ•°ç»„
        let showData = []
        showData[0] = data.slice(0, 10)
        showData[1] = data.slice(10, 20)
        showData[2] = data.slice(20, 29)
        showData[3] = data.slice(29, 36)
        return showData
      }
    },
    data() {
      return {
        // æ ‡è®°æ˜¯å¦è¾“入车牌号码
        inputCarNumber: false,
        // é•¿æŒ‰å¤šæ¬¡åˆ é™¤äº‹ä»¶ç›‘听
        longPressDeleteTimer: null
      }
    },
    watch:{
      switchEnMode: {
        handler(value) {
          if (value) {
            this.inputCarNumber = true
          } else {
            this.inputCarNumber = false
          }
        },
        immediate: true
      }
    },
    methods: {
      // ç‚¹å‡»é”®ç›˜æŒ‰é’®
      click(i, j) {
        let value = ''
        // æ ¹æ®ä¸åŒæ¨¡å¼èŽ·å–ä¸åŒæ•°ç»„çš„å€¼
        if (this.inputCarNumber) value = this.endKeyBoardList[i][j]
        else value = this.areaList[i][j]
        // è½¦ç‰Œé‡Œä¸åŒ…含I
        if (value === 'I') return
        this.$emit('change', value)
      },
      // ä¿®æ”¹è¾“入模式
      // ä¸­æ–‡/英文
      changeMode() {
        this.inputCarNumber = !this.inputCarNumber
      },
      // ç‚¹å‡»é€€æ ¼
      backspaceClick() {
        this.$emit('backspace')
        this.clearTimer()
        this.longPressDeleteTimer = setInterval(() => {
          this.$emit('backspace')
        }, 250)
      },
      // æ¸…空定时器
      clearTimer() {
        if (this.longPressDeleteTimer) {
          clearInterval(this.longPressDeleteTimer)
          this.longPressDeleteTimer = null
        }
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-car-keyboard {
    position: relative;
    padding: 24rpx 0;
    background-color: #E6E6E6;
    &__grids {
      &__item {
        display: flex;
        flex-direction: row;
        align-items: center;
        justify-content: center;
      }
      &__btn {
        display: inline-flex;
        justify-content: center;
        flex: 0 0 64rpx;
        width: 62rpx;
        height: 80rpx;
        font-size: 38rpx;
        line-height: 80rpx;
        font-weight: 500;
        text-decoration: none;
        text-align: center;
        background-color: #FFFFFF;
        margin: 8rpx 5rpx;
        border-radius: 8rpx;
        box-shadow: 0 2rpx 0rpx $tn-box-shadow-color;
        &--disabled {
          opacity: 0.6;
        }
      }
    }
    &__back {
      display: flex;
      flex-direction: row;
      align-items: center;
      justify-content: center;
      position: absolute;
      width: 96rpx;
      height: 80rpx;
      right: 22rpx;
      bottom: 32rpx;
      background-color: #E6E6E6;
      border-radius: 8rpx;
      box-shadow: 0 2rpx 0rpx $tn-box-shadow-color;
    }
    &__change {
      display: flex;
      flex-direction: row;
      align-items: center;
      justify-content: center;
      position: absolute;
      width: 96rpx;
      height: 80rpx;
      left: 22rpx;
      bottom: 32rpx;
      line-height: 1;
      background-color: #FFFFFF;
      border-radius: 8rpx;
      box-shadow: 0 2rpx 0rpx $tn-box-shadow-color;
    }
    &__mode {
      &--zh {
        transform: translateY(-10rpx);
      }
      &--en {
        transform: translateY(10rpx);
      }
      &--active {
        color: $tn-main-color;
        font-size: 30rpx;
      }
      &--inactive {
        &.tn-car-keyboard__mode--zh {
          transform: scale(0.85) translateY(-10rpx);
        }
      }
      &--inactive {
        &.tn-car-keyboard__mode--en {
          transform: scale(0.85) translateY(10rpx);
        }
      }
    }
    &--hover {
      background-color: #E6E6E6 !important;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-cascade-selection/tn-cascade-selection.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,654 @@
<template>
  <view class="tn-cascade-selection tn-cascade-selection-class">
    <scroll-view
      class="selection__scroll-view"
      :class="[{'tn-border-solid-bottom': headerLine}]"
      :style="[scrollViewStyle]"
      scroll-x
      scroll-with-animation
      :scroll-into-view="scrollViewId"
    >
      <view class="selection__header" :class="[backgroundColorClass]" :style="[headerStyle]">
        <view
          v-for="(item, index) in selectedArr"
          :key="index"
          :id="`select__${index}`"
          class="selection__header__item"
          :class="[headerItemClass(index)]"
          :style="[headerItemStyle(index)]"
          @tap.stop="clickNav(index)"
        >
          {{ item.text }}
          <view
            v-if="index===currentTab && showActiveLine"
            class="selection__header__line"
            :style="{backgroundColor: activeLineColor}"
          ></view>
        </view>
      </view>
    </scroll-view>
    <swiper
      class="selection__list"
      :class="[backgroundColorClass]"
      :style="[listStyle]"
      :current="currentTab"
      :duration="300"
      @change="switchTab"
    >
      <swiper-item
        v-for="(item, index) in selectedArr"
        :key="index"
      >
        <scroll-view
          class="selection__list__item"
          :style="{height: selectionContainerHeight + 'rpx'}"
          scroll-y
          :scroll-into-view="item.scrollViewId"
        >
          <view class="selection__list__item--first"></view>
          <view
            v-for="(subItem, subIndex) in item.list"
            :key="subIndex"
            :id="`select__${subIndex}`"
            class="selection__list__item__cell"
            :style="[itemStyle]"
            @tap="change(index, subIndex, subItem)"
          >
            <view
              v-if="item.index === subIndex"
              class="selection__list__item__icon tn-icon-success"
              :style="[itemIconStyle]"
            ></view>
            <image
              v-if="subItem.src"
              class="selection__list__item__image"
              :style="[itemImageStyle]"
              :src="subItem.src"
            ></image>
            <view
              class="selection__list__item__title"
              :class="[{'tn-text-bold': item.index === subIndex && itemActiveBold}]"
              :style="[itemTitleStyle(index, subIndex)]"
            >
              {{ subItem.text }}
            </view>
            <view
              v-if="subItem.subText"
              class="selection__list__item__title--sub"
              :style="[itemSubTitleStyle]"
            >
              {{ subItem.subText }}
            </view>
          </view>
        </scroll-view>
      </swiper-item>
    </swiper>
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    name: 'tn-cascade-selection',
    mixins: [ componentsColorMixin ],
    props: {
      // å¦‚果下一级是请求返回,则为第一级数据,否则为所有数据
      /* {
        text: '', // æ ‡é¢˜
        subText: '', // å­æ ‡é¢˜
        src: '', // å›¾ç‰‡åœ°å€
        value: 0, // é€‰ä¸­çš„值
        children: [
          {
            text: '',
            subText: '',
            value: 0,
            children: []
          }
        ]
      } */
      list: {
        type: Array,
        default() {
          return []
        }
      },
      // é»˜è®¤é€‰ä¸­å€¼
      // ['value1','value2','value3']
      defaultValue: {
        type: Array,
        default() {
          return []
        }
      },
      // å­é›†æ•°æ®é€šè¿‡è¯·æ±‚来获取
      request: {
        type: Boolean,
        default: false
      },
      // request为true时生效, èŽ·å–åˆ°çš„å­é›†æ•°æ®
      receiveData: {
        type: Array,
        default() {
          return []
        }
      },
      // æ˜¾ç¤ºheader底部细线
      headerLine: {
        type: Boolean,
        default: true
      },
      // header背景颜色
      headerBgColor: {
        type: String,
        default: ''
      },
      // é¡¶éƒ¨æ ‡ç­¾æ é«˜åº¦,单位rpx
      tabsHeight: {
        type: Number,
        default: 88
      },
      // é»˜è®¤æ˜¾ç¤ºæ–‡å­—
      text: {
        type: String,
        default: '请选择'
      },
      // é€‰ä¸­çš„颜色
      activeColor: {
        type: String,
        default: '#01BEFF'
      },
      // é€‰ä¸­åŽåŠ ç²—
      activeBold: {
        type: Boolean,
        default: true
      },
      // é€‰ä¸­æ˜¾ç¤ºåº•部线条
      showActiveLine: {
        type: Boolean,
        default: true
      },
      // çº¿æ¡é¢œè‰²
      activeLineColor: {
        type: String,
        default: '#01BEFF'
      },
      // icon大小,单位rpx
      activeIconSize: {
        type: Number,
        default: 0
      },
      // icon颜色
      activeIconColor: {
        type: String,
        default: '#01BEFF'
      },
      // item图片宽度, å•位rpx
      itemImgWidth: {
        type: Number,
        default: 0
      },
      // item图片高度, å•位rpx
      itemImgHeight: {
        type: Number,
        default: 0
      },
      // item图片圆角
      itemImgRadius: {
        type: String,
        default: '50%'
      },
      // item text颜色
      itemTextColor: {
        type: String,
        default: ''
      },
      // item text选中颜色
      itemActiveTextColor: {
        type: String,
        default: ''
      },
      // item text选中加粗
      itemActiveBold: {
        type: Boolean,
        default: true
      },
      // item text文字大小, å•位rpx
      itemTextSize: {
        type: Number,
        default: 0
      },
      // item subText颜色
      itemSubTextColor: {
        type: String,
        default: ''
      },
      // item subText字体大小, å•位rpx
      itemSubTextSize: {
        type: Number,
        default: 0
      },
      // item样式
      itemStyle: {
        type: Object,
        default() {
          return {}
        }
      },
      // selection选项容器高度, å•位rpx
      selectionContainerHeight: {
        type: Number,
        default: 300
      }
    },
    computed: {
      scrollViewStyle() {
        let style = {}
        if (this.headerBgColor) {
          style.backgroundColor = this.headerBgColor
        }
        return style
      },
      headerStyle() {
        let style = {}
        style.height = `${this.tabsHeight}rpx`
        if (this.backgroundColorStyle) {
          style.backgroundColor = this.backgroundColorStyle
        }
        return style
      },
      headerItemClass() {
        return (index) => {
          let clazz = ''
          if (index !== this.currentTab) {
            clazz += ` ${this.fontColorClass}`
          } else {
            if (this.activeBold) {
              clazz += ' tn-text-bold'
            }
          }
          return clazz
        }
      },
      headerItemStyle() {
        return (index) => {
          let style = {}
          style.color = index === this.currentTab ? this.activeColor : (this.fontColorStyle ? this.fontColorStyle : '')
          if (this.fontSizeStyle) {
            style.fontSize = this.fontSizeStyle
          }
          return style
        }
      },
      listStyle() {
        let style = {}
        style.height = `${this.selectionContainerHeight}rpx`
        if (this.backgroundColorStyle) {
          style.color = this.backgroundColorStyle
        }
        return style
      },
      itemIconStyle() {
        let style = {}
        if (this.activeIconColor) {
          style.color = this.activeIconColor
        }
        if (this.activeIconSize) {
          style.fontSize = this.activeIconSize + 'rpx'
        }
        return style
      },
      itemImageStyle() {
        let style = {}
        if (this.itemImgWidth) {
          style.width = this.itemImgWidth + 'rpx'
        }
        if (this.itemImgHeight) {
          style.height = this.itemImgHeight + 'rpx'
        }
        if (this.itemImgRadius) {
          style.borderRadius = this.itemImgRadius
        }
        return style
      },
      itemTitleStyle() {
        return (index, subIndex) => {
          let style = {}
          if (index === subIndex) {
            if (this.itemActiveTextColor) {
              style.color = this.itemActiveTextColor
            }
          } else {
            if (this.itemTextColor) {
              style.color = this.itemTextColor
            }
          }
          if (this.itemTextSize) {
            style.fontSize = this.itemTextSize + 'rpx'
          }
          return style
        }
      },
      itemSubTitleStyle() {
        let style = {}
        if (this.itemSubTextColor) {
          style.color = this.itemSubTextColor
        }
        if (this.itemSubTextSize) {
          style.fontSize = this.itemSubTextSize + 'rpx'
        }
        return {}
      }
    },
    watch: {
      list(val) {
        this.initData(val, -1)
      },
      defaultValue(val) {
        this.setDefaultValue(val)
      },
      receiveData(val) {
        this.addSubData(val, this.currentTab)
      },
    },
    data() {
      return {
        // å½“前选中的子集
        currentTab: 0,
        // tabs栏scrollView滚动的位置
        scrollViewId: 'select__0',
        // é€‰é¡¹æ•°ç»„
        selectedArr: []
      }
    },
    created() {
      this.setDefaultValue(this.defaultValue)
    },
    methods: {
      // åˆå§‹åŒ–数据
      initData(data, index) {
        if (!data || data.length === 0) return
        if (this.request) {
          // ç¬¬ä¸€çº§æ•°æ®
          this.addSubData(data, index)
        } else {
          this.addSubData(this.getItemList(index, -1), index)
        }
      },
      // é‡ç½®æ•°æ®
      reset() {
        this.initData(this.list, -1)
      },
      // æ»šåŠ¨åˆ‡æ¢
      switchTab(e) {
        this.currentTab = e.detail.current
        this.checkSelectPosition()
      },
      // ç‚¹å‡»æ ‡é¢˜åˆ‡æ¢
      clickNav(index) {
        if (this.currentTab !== index) {
          this.currentTab = index
        }
      },
      // åˆ—表数据发生改变
      change(index, subIndex, subItem) {
        let item = this.selectedArr[index]
        if (item.index === subIndex) return
        item.index = subIndex
        item.text = subItem.text
        item.subText = subItem.subText || ''
        item.value = subItem.value
        item.src = subItem.src || ''
        this.$emit('change', {
          index: index,
          subIndex: subIndex,
          ...subItem
        })
        // å¦‚果不是异步加载,则取出对应的数据
        if (!this.request) {
          let data = this.getItemList(index, subIndex)
          this.addSubData(data, index)
        }
      },
      // è®¾ç½®é»˜è®¤çš„æ•°æ®
      setDefaultValue(val) {
        let defaultValues = val || []
        if (defaultValues.length > 0) {
          this.selectedArr = this.getItemListWithValues(JSON.parse(JSON.stringify(this.list)), defaultValues)
          if (!this.selectedArr) return
          this.currentTab = this.selectedArr.length - 1
          this.$nextTick(() => {
            this.checkSelectPosition()
          })
          // defaultItemList.map((item) => {
          //   item.scrollViewId = `select__${item.index}`
          // })
          // this.selectedArr = defaultItemList
          // this.currentTab = defaultItemList.length - 1
          // this.$nextTick(() => {
          //   this.checkSelectPosition()
          // })
        } else {
          this.initData(this.list, -1)
        }
      },
      // èŽ·å–å¯¹åº”é€‰é¡¹çš„item数据
      getItemList(index, subIndex) {
        let list = []
        let arr = JSON.parse(JSON.stringify(this.list))
        // åˆå§‹åŒ–数据
        if (index === -1) {
          list = this.removeChildren(arr)
        } else {
          // åˆ¤æ–­ç¬¬ä¸€é¡¹æ˜¯å¦å·²ç»é€‰æ‹©
          let value = this.selectedArr[0].index
          value = value === -1 ? subIndex : value
          list = arr[value].children || []
          if (index > 0) {
            for (let i = 1; i < index + 1; i++) {
              // èŽ·å–å½“å‰æ•°æ®é€‰ä¸­çš„åºå·
              let val = index === i ? subIndex : this.selectedArr[i].index
              list = list[val].children || []
              if (list.length === 0) break
            }
          }
          list = this.removeChildren(list)
        }
        return list
      },
      // æ ¹æ®æ•°ç»„中的值获取对应的item数据
      getItemListWithValues(data, values) {
        const defaultValues = JSON.parse(JSON.stringify(values))
        if (!defaultValues || defaultValues.length === 0) return
        // å–出第一个值所对应的item
        const itemIndex = data.findIndex((item) => {
          return item.value === defaultValues[0]
        })
        if (itemIndex === -1) return
        const item = data[itemIndex]
        item.index = itemIndex
        item.scrollViewId = `select__${itemIndex}`
        item.list = this.removeChildren(JSON.parse(JSON.stringify(data)))
        // åˆ¤æ–­æ˜¯å¦åªæœ‰1个值
        if (defaultValues.length === 1 || (!item.hasOwnProperty('children') || item.children.length === 0)) {
          return this.removeChildren([item])
        } else {
          let selectItemList = []
          const children = item.children
          selectItemList.push(item)
          // ç§»é™¤å·²ç»èŽ·å–çš„å€¼
          defaultValues.splice(0, 1)
          const childrenValue = this.getItemListWithValues(children, defaultValues)
          selectItemList = selectItemList.concat(childrenValue)
          return this.removeChildren(selectItemList)
        }
      },
      // åˆ é™¤å­å…ƒç´ 
      removeChildren(data) {
        let list = data.map((item) => {
          if (item.hasOwnProperty('children')) {
            delete item['children']
          }
          return item
        })
        return list
      },
      // æ–°å¢žå­é›†æ•°æ®æ—¶å¤„理
      addSubData(data, index) {
        // åˆ¤æ–­æ˜¯å¦å·²ç»å®Œæˆé€‰æ‹©æ•°æ®æˆ–者为初始化数据
        if (!data || data.length === 0) {
          if (index == -1) return
          // å®Œæˆé€‰æ‹©
          let arr = this.selectedArr
          // å¦‚果当前选中项的序号比已选数据的长度小,则表示当前重新选择了数据
          if (index < arr.length - 1) {
            let newArr = arr.slice(0, index + 1)
            this.selectedArr = newArr
          }
          let result = JSON.parse(JSON.stringify(this.selectedArr))
          let lastItem = result[result.length - 1] || {}
          let text = ''
          result.map(item => {
            text += item.text
            delete item['list']
            delete item['scrollViewId']
            return item
          })
          this.$emit('complete', {
            result: result,
            value: lastItem.value,
            text: text,
            subText: lastItem.subText,
            src: lastItem.src
          })
        } else {
          // é‡ç½®æ•°æ®
          let item = [{
            text: this.text,
            subText: '',
            value: '',
            src: '',
            index: -1,
            scrollViewId: 'select__0',
            list: data
          }]
          // åˆå§‹åŒ–数据
          if (index === -1) {
            this.selectedArr = item
          } else {
            // æ‹¼æŽ¥æ–°æ—§æ•°æ®å¹¶ä¸”判断是否为重新选择了数据(如果为重新选择了数据则重置之后的选项数据)
            let retainArr = this.selectedArr.slice(0, index + 1)
            this.selectedArr = retainArr.concat(item)
          }
          this.$nextTick(() => {
            this.currentTab = this.selectedArr.length - 1
          })
        }
      },
      // æ£€æŸ¥å½“前选中项,并将选项设置位置信息
      checkSelectPosition() {
        let item = this.selectedArr[this.currentTab]
        item.scrollViewId = 'select__0'
        this.$nextTick(() => {
          setTimeout(() => {
            // è®¾ç½®å½“前数据滚动到的位置
            let val = item.index < 2 ? 0 : Number(item.index - 2)
            item.scrollViewId = `select__${val}`
          }, 10)
        })
        // è®¾ç½®é€‰é¡¹æ»šåŠ¨åˆ°æ‰€åœ¨çš„ä½ç½®
        if (this.currentTab > 1) {
          this.scrollViewId = `select__${this.currentTab - 1}`
        } else {
          this.scrollViewId = `select__0`
        }
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-cascade-selection {
    width: 100%;
  }
  .selection {
    &__scroll-view {
      background-color: #FFFFFF;
    }
    &__header {
      width: 100%;
      display: flex;
      align-items: center;
      position: relative;
      &__item {
        max-width: 240rpx;
        padding: 15rpx 30rpx;
        flex-shrink: 0;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
        position: relative;
      }
      &__line {
        width: 60rpx;
        height: 6rpx;
        border-radius: 4rpx;
        position: absolute;
        bottom: 0;
        right: 0;
        left: 50%;
        transform: translateX(-50%);
      }
    }
    &__list {
      background-color: #FFFFFF;
      &__item {
        &--first {
          width: 100%;
          height: 20rpx;
        }
        &__cell {
          width: 100%;
          display: flex;
          align-items: center;
          padding: 20rpx 30rpx;
        }
        &__icon {
          margin-right: 12rpx;
          font-size: 24rpx;
        }
        &__image {
          width: 40rpx;
          height: 40rpx;
          margin-right: 12rpx;
          flex-shrink: 0;
        }
        &__title {
          word-break: break-all;
          color: #333333;
          font-size: 28rpx;
          &--sub {
            margin-left: 20rpx;
            word-break: break-all;
            color: $tn-font-sub-color;
            font-size: 24rpx;
          }
        }
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-checkbox-group/tn-checkbox-group.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,134 @@
<template>
  <view class="tn-checkbox-group-class tn-checkbox-group">
    <slot></slot>
  </view>
</template>
<script>
  import Emitter from '../../libs/utils/emitter.js'
  export default {
    mixins: [ Emitter ],
    name: 'tn-checkbox-group',
    props: {
      value: {
        type: Array,
        default() {
          return []
        }
      },
      // å¯ä»¥é€‰ä¸­å¤šå°‘个checkbox
      max: {
        type: Number,
        default: 999
      },
      // è¡¨å•提交时的标识符
      name: {
        type: String,
        default: ''
      },
      // ç¦ç”¨é€‰æ‹©
      disabled: {
        type: Boolean,
        default: false
      },
      // ç¦ç”¨ç‚¹å‡»æ ‡ç­¾è¿›è¡Œé€‰æ‹©
      disabledLabel: {
        type: Boolean,
        default: false
      },
      // é€‰æ‹©æ¡†çš„形状 square æ–¹å½¢ circle åœ†å½¢
      shape: {
        type: String,
        default: 'square'
      },
      // é€‰ä¸­æ—¶çš„颜色
      activeColor: {
        type: String,
        default: '#01BEFF'
      },
      // ç»„件大小
      size: {
        type: Number,
        default: 34
      },
      // æ¯ä¸ªcheckbox占的宽度
      width: {
        type: String,
        default: 'auto'
      },
      // æ˜¯å¦æ¢è¡Œ
      wrap: {
        type: Boolean,
        default: false
      },
      // å›¾æ ‡å¤§å°
      iconSize: {
        type: Number,
        default: 20
      }
    },
    computed: {
      // è¿™é‡Œcomputed的变量,都是子组件tn-checkbox需要用到的,由于头条小程序的兼容性差异,子组件无法实时监听父组件参数的变化
      // æ‰€ä»¥éœ€è¦æ‰‹åŠ¨é€šçŸ¥å­ç»„ä»¶ï¼Œè¿™é‡Œè¿”å›žä¸€ä¸ªparentData变量,供watch监听,在其中去通知每一个子组件重新从父组件(tn-checkbox-group)
      // æ‹‰å–父组件新的变化后的参数
      parentData() {
        return [this.value, this.disabled, this.disabledLabel, this.shape, this.activeColor, this.size, this.width, this.wrap, this.iconSize]
      }
    },
    data() {
      return {
      }
    },
    watch: {
      // å½“父组件中的子组件需要共享的参数发生了变化,手动通知子组件
      parentData() {
        if (this.children.length) {
          this.children.map(child => {
            // åˆ¤æ–­å­ç»„ä»¶(tn-checkbox)如果有updateParentData方法的话,子组件重新从父组件拉取了最新的值
            typeof(child.updateParentData) === 'function' && child.updateParentData()
          })
        }
      }
    },
    created() {
      this.children = []
    },
    methods: {
      initValue(values) {
        this.$emit('input', values)
      },
      // è§¦å‘事件
      emitEvent() {
        let values = []
        this.children.map(child => {
          if (child.checkValue) values.push(child.name)
        })
        this.$emit('change', values)
        this.$emit('input', values)
        // å‘出事件,用于在表单组件中嵌入checkbox的情况,进行验证
        // ç”±äºŽå¤´æ¡å°ç¨‹åºæ‰§è¡Œè¿Ÿé’ï¼Œæ•…需要用几十毫秒的延时
        setTimeout(() => {
            // å°†å½“前的值发送到 tn-form-item è¿›è¡Œæ ¡éªŒ
            this.dispatch('tn-form-item', 'on-form-change', values)
        }, 60)
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-checkbox-group {
    /* #ifndef MP || APP-NVUE */
    display: inline-flex;
    flex-wrap: wrap;
    /* #endif */
    &::after {
      content: " ";
      display: table;
      clear: both;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-checkbox/tn-checkbox.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,328 @@
<template>
  <view class="tn-checkbox-class tn-checkbox" :style="[checkboxStyle]">
    <view
      class="tn-checkbox__icon-wrap"
      :class="[iconClass]"
      :style="[iconStyle]"
      @tap="toggle"
    >
      <view class="tn-checkbox__icon-wrap__icon" :class="[`tn-icon-${iconName}`]"></view>
    </view>
    <view
      class="tn-checkbox__label"
      :class="[labelClass]"
      :style="{
        fontSize: labelSize ? labelSize + 'rpx' : ''
      }"
      @tap="onClickLabel"
    >
      <slot></slot>
    </view>
  </view>
</template>
<script>
  export default {
    name: 'tn-checkbox',
    props: {
      // checkbox名称
      name: {
        type: [String, Number],
        default: ''
      },
      // æ˜¯å¦ä¸ºé€‰ä¸­çŠ¶æ€
      value: {
        type: Boolean,
        default: false
      },
      // ç¦ç”¨é€‰æ‹©
      disabled: {
        type: Boolean,
        default: false
      },
      // ç¦ç”¨ç‚¹å‡»æ ‡ç­¾è¿›è¡Œé€‰æ‹©
      disabledLabel: {
        type: Boolean,
        default: false
      },
      // é€‰æ‹©æ¡†çš„形状 square æ–¹å½¢ circle åœ†å½¢
      shape: {
        type: String,
        default: ''
      },
      // é€‰ä¸­æ—¶çš„颜色
      activeColor: {
        type: String,
        default: ''
      },
      // ç»„件大小
      size: {
        type: Number,
        default: 0
      },
      // å›¾æ ‡åç§°
      iconName: {
        type: String,
        default: 'success'
      },
      // å›¾æ ‡å¤§å°
      iconSize: {
        type: Number,
        default: 0
      },
      // label的字体大小
      labelSize: {
        type: Number,
        default: 0
      }
    },
    computed: {
      // æ˜¯å¦ç¦ç”¨é€‰ä¸­ï¼Œçˆ¶ç»„件的禁用会覆盖当前的禁用状态
      isDisabled() {
        return this.disabled ? this.disabled : (this.parent ? this.parentData.disabled : false)
      },
      // æ˜¯å¦ç¦ç”¨ç‚¹å‡»label选中,父组件的禁用会覆盖当前的禁用状态
      isDisabledLabel() {
        return this.disabledLabel ? this.disabledLabel : (this.parent ? this.parentData.disabledLabel : false)
      },
      // å°ºå¯¸
      checkboxSize() {
        return this.size ? this.size : (this.parent ? this.parentData.size : 34)
      },
      // æ¿€æ´»æ—¶çš„颜色
      elAvtiveColor() {
        return this.activeColor ? this.activeColor : (this.parent ? this.parentData.activeColor : '#01BEFF')
      },
      // å½¢çж
      elShape() {
        return this.shape ? this.shape : (this.parent ? this.parentData.shape : 'square')
      },
      iconClass() {
        let clazz = ''
        clazz += (' tn-checkbox__icon-wrap--' + this.elShape)
        if (this.checkValue) clazz += ' tn-checkbox__icon-wrap--checked'
        if (this.isDisabled) clazz += ' tn-checkbox__icon-wrap--disabled'
        if (this.value && this.isDisabled) clazz += ' tn-checkbox__icon-wrap--disabled--checked'
        return clazz
      },
      iconStyle() {
        let style = {}
        // åˆ¤æ–­æ˜¯å¦ç”¨æˆ·æ‰‹åŠ¨ç¦ç”¨å’Œä¼ é€’çš„å€¼
        if (this.elAvtiveColor && this.checkValue && !this.isDisabled) {
          style.borderColor = this.elAvtiveColor
          style.backgroundColor = this.elAvtiveColor
        }
        // checkbox内部的勾选图标,如果选中状态,为白色,否则为透明色即可
        style.color = this.checkValue ? '#FFFFFF' : 'transparent'
        style.width = this.checkboxSize + 'rpx'
        style.height = style.width
        style.fontSize = (this.iconSize ? this.iconSize : (this.parent ? this.parentData.iconSize : 20)) + 'rpx'
        return style
      },
      checkboxStyle() {
        let style = {}
        if (this.parent && this.parentData.width) {
          // #ifdef MP
          // å„家小程序因为它们特殊的编译结构,使用float布局
          style.float = 'left';
          // #endif
          // #ifndef MP
          // H5和APP使用flex布局
          style.flex = `0 0 ${this.parentData.width}`;
          // #endif
        }
        if(this.parent && this.parentData.wrap) {
            style.width = '100%';
            // #ifndef MP
            // H5和APP使用flex布局,将宽度设置100%,即可自动换行
            style.flex = '0 0 100%';
            // #endif
        }
        return style
      },
      labelClass() {
        let clazz = ''
        if (this.isDisabled) {
          clazz += ' tn-checkbox__label--disabled'
        }
        return clazz
      }
    },
    data() {
      return {
        // å½“前checkbox的value值
        checkValue: false,
        parentData: {
          value: null,
          max: null,
          disabled: null,
          disabledLabel: null,
          shape: null,
          activeColor: null,
          size: null,
          width: null,
          wrap: null,
          iconSize: null
        }
      }
    },
    watch: {
      value(val) {
        this.checkValue = val
      }
    },
    created() {
      // æ”¯ä»˜å®å°ç¨‹åºä¸æ”¯æŒprovide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
      // this.parent = this.$t.$parent.call(this, 'tn-checkbox-group')
      // // å¦‚果存在u-checkbox-group,将本组件的this塞进父组件的children中
      // this.parent && this.parent.children.push(this)
      // // åˆå§‹åŒ–父组件的value值
      // this.parent && this.parent.emitEvent()
      this.updateParentData()
      this.parent && this.parent.children.push(this)
    },
    methods: {
      updateCheckValue() {
        // æ›´æ–°å½“前checkbox的选中状态
        this.checkValue = (this.parent && this.parentData.value.includes(this.name)) || this.value === true
        if (this.parent) {
          if (this.value && !this.parentData.value.includes(this.name)) {
            this.parentData.value.push(this.name)
            this.parent.initValue(this.parentData.value)
          }
        }
      },
      updateParentData() {
        this.getParentData('tn-checkbox-group')
        this.updateCheckValue()
      },
      onClickLabel() {
        if (!this.isDisabled && !this.isDisabledLabel) {
          this.setValue()
        }
      },
      toggle() {
        if (!this.isDisabled) {
          this.setValue()
        }
      },
      emitEvent() {
        this.$emit('change', {
          name: this.name,
          value: !this.checkValue
        })
        if (this.parent) {
          this.checkValue = !this.checkValue
          // æ‰§è¡Œçˆ¶ç»„ä»¶tn-checkbox-group的事件方法
          // ç­‰å¾…下一个周期再执行,因为this.$emit('input')作用于父组件,再反馈到子组件内部,需要时间
          setTimeout(() => {
            if(this.parent.emitEvent) this.parent.emitEvent();
          }, 80)
        }
      },
      // è®¾ç½®input的值,通过v-modal绑定组件的值
      setValue() {
        // åˆ¤æ–­æ˜¯å¦ä¸ºå¯é€‰é¡¹ç»„
        if (this.parent) {
          // åè½¬çŠ¶æ€
          if (this.checkValue === true) {
            this.emitEvent()
            // this.$emit('input', !this.checkValue)
          } else {
            // è¶…出最大可选项,弹出提示
            if (this.parentData.value.length >= this.parentData.max) {
              return this.$t.message.toast(`最多可选${this.parent.max}项`)
            }
            // å¦‚果原来为未选中状态,需要选中的数量少于父组件中设置的max值,才可以选中
            this.emitEvent();
            // this.$emit('input', !this.checkValue);
          }
        } else {
          // åªæœ‰ä¸€ä¸ªå¯é€‰é¡¹
          this.emitEvent()
          this.$emit('input', !this.checkValue)
        }
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-checkbox {
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
    align-items: center;
    overflow: hidden;
    user-select: none;
    line-height: 1.8;
    &__icon-wrap {
      color: $tn-font-color;
      flex: none;
      display: flex;
      flex-direction: row;
      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;
      border: 1px solid $tn-font-sub-color;
      transition-duration: 0.2s;
      /* #ifdef MP-TOUTIAO */
      // å¤´æ¡å°ç¨‹åºå…¼å®¹æ€§é—®é¢˜ï¼Œéœ€è¦è®¾ç½®è¡Œé«˜ä¸º0,否则图标偏下
      &__icon {
          line-height: 0;
      }
      /* #endif */
      &--circle {
        border-radius: 100%;
      }
      &--square {
        border-radius: 6rpx;
      }
      &--checked {
        color: #FFFFFF;
        background-color: $tn-main-color;
        border-color: $tn-main-color;
      }
      &--disabled {
        background-color: $tn-font-holder-color;
        border-color: $tn-font-sub-color;
      }
      &--disabled--checked {
        color: $tn-font-sub-color !important;
      }
    }
    &__label {
      word-wrap: break-word;
      margin-left: 10rpx;
      margin-right: 24rpx;
      color: $tn-font-color;
      font-size: 30rpx;
      &--disabled {
        color: $tn-font-sub-color;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-circle-progress/tn-circle-progress.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,223 @@
<template>
  <view
    class="tn-circle-progress-class tn-circle-progress"
    :style="{
      width: widthPx + 'px',
      height: widthPx + 'px'
    }"
  >
    <!-- æ”¯ä»˜å®å°ç¨‹åºä¸æ”¯æŒcanvas-id属性,必须用id属性 -->
    <!-- é»˜è®¤åœ†çޝ -->
    <canvas
      class="tn-circle-progress__canvas-bg"
      :canvas-id="elBgId"
      :id="elBgId"
      :style="{
        width: widthPx + 'px',
        height: widthPx + 'px'
      }"
    ></canvas>
    <!-- è¿›åº¦åœ†çޝ -->
    <canvas
      class="tn-circle-progress__canvas"
      :canvas-id="elId"
      :id="elId"
      :style="{
        width: widthPx + 'px',
        height: widthPx + 'px'
      }"
    ></canvas>
    <view class="tn-circle-progress__content">
      <slot v-if="$slots.default || $slots.$default"></slot>
      <view v-else-if="showPercent" class="tn-circle-progress__content__percent">{{ percent + '%' }}</view>
    </view>
  </view>
</template>
<script>
  export default {
    name: 'tn-circle-progress',
    props: {
      // è¿›åº¦ï¼ˆç™¾åˆ†æ¯”)
      percent: {
        type: Number,
        default: 0,
        validator: val => {
          return val >= 0 && val <= 100
        }
      },
      // åœ†çŽ¯çº¿å®½
      borderWidth: {
        type: Number,
        default: 14
      },
      // æ•´ä½“圆的宽度
      width: {
        type: Number,
        default: 200
      },
      // æ˜¯å¦æ˜¾ç¤ºæ¡çº¹
      striped: {
        type: Boolean,
        default: false
      },
      // æ¡çº¹æ˜¯å¦è¿åЍ
      stripedActive: {
        type: Boolean,
        default: true
      },
      // æ¿€æ´»éƒ¨åˆ†é¢œè‰²
      activeColor: {
        type: String,
        default: '#01BEFF'
      },
      // éžæ¿€æ´»éƒ¨åˆ†é¢œè‰²
      inactiveColor: {
        type: String,
        default: '#f0f0f0'
      },
      // æ˜¯å¦æ˜¾ç¤ºè¿›åº¦æ¡å†…部百分比值
      showPercent: {
        type: Boolean,
        default: false
      },
      // åœ†çŽ¯æ‰§è¡ŒåŠ¨ç”»çš„æ—¶é—´ï¼Œms
      duration: {
        type: Number,
        default: 1500
      }
    },
    data() {
      return {
        // å¾®ä¿¡å°ç¨‹åºä¸­ä¸èƒ½ä½¿ç”¨this.$t.uuid()形式动态生成id值,否则会报错
        // #ifdef MP-WEIXIN
        elBgId: 'tCircleProgressBgId',
        elId: 'tCircleProgressElId',
        // #endif
        // #ifndef MP-WEIXIN
        elBgId: this.$t.uuid(),
        elId: this.$t.uuid(),
        // #endif
        // æ´»åŠ¨åœ†ä¸Šä¸‹æ–‡
        progressContext: null,
        // è½¬æ¢æˆpx为单位的背景宽度
        widthPx: uni.upx2px(this.width || 200),
        // è½¬æ¢æˆpx为单位的圆环宽度
        borderWidthPx: uni.upx2px(this.borderWidth || 14),
        // canvas画圆的起始角度,默认为-90度,顺时针
        startAngle: -90 * Math.PI / 180,
        // åŠ¨æ€ä¿®æ”¹è¿›åº¦å€¼çš„æ—¶å€™ï¼Œä¿å­˜è¿›åº¦å€¼çš„å˜åŒ–å‰åŽå€¼
        newPercent: 0,
        oldPercent: 0
      }
    },
    watch: {
      percent(newVal, oldVal = 0) {
        if (newVal > 100) newVal = 100
        if (oldVal < 0) oldVal = 0
        this.newPercent = newVal
        this.oldPercent = oldVal
        setTimeout(() => {
            // æ— è®ºæ˜¯ç™¾åˆ†æ¯”值增加还是减少,需要操作还是原来的旧的百分比值
            // å°†æ­¤å€¼å‡å°‘或者新增到新的百分比值
            this.drawCircleByProgress(oldVal)
        }, 50)
      }
    },
    created() {
      // èµ‹å€¼ï¼Œç”¨äºŽåŠ è½½åŽç¬¬ä¸€ä¸ªç”»åœ†ä½¿ç”¨
      this.newPercent = this.percent;
      this.oldPercent = 0;
    },
    mounted() {
      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()
        let radius = this.widthPx / 2
        ctx.arc(radius, radius, radius - this.borderWidthPx, 0, 360 * Math.PI / 180, false)
        ctx.stroke()
        ctx.draw()
      },
      // ç»˜åˆ¶åœ†å¼§çš„进度
      drawCircleByProgress(progress) {
        // å¦‚果已经存在则拿来使用
        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.activeColor)
        // å°†æ€»è¿‡æ¸¡æ—¶é—´é™¤ä»¥100,得出每修改百分之一进度所需的时间
        let preSecondTime = Math.floor(this.duration / 100)
        // ç»“束角的计算依据为:将2π分为100份,乘以当前的进度值,得出终止点的弧度值,加起始角,为整个圆从默认的
        let endAngle = ((360 * Math.PI / 180) / 100) * progress + this.startAngle
        let radius = this.widthPx / 2
        ctx.beginPath()
        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)
        }, preSecondTime)
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-circle-progress {
    position: relative;
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
    align-items: center;
    justify-content: center;
    background-color: transparent;
    &__canvas {
      position: absolute;
      &-bg {
        position: absolute;
      }
    }
    &__content {
      display: flex;
      align-items: center;
      justify-content: center;
      &__percent {
        font-size: 28rpx;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-collapse-item/tn-collapse-item.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,236 @@
<template>
  <view class="tn-collapse-item-class tn-collapse-item" :style="[itemStyle]">
    <!-- å¤´éƒ¨ -->
    <view
      class="tn-collapse-item__head"
      :style="[headStyle]"
      :hover-stay-time="200"
      :hover-class="hoverClass"
      @tap.stop="headClick"
    >
      <block v-if="!$slots['title-all'] || !$slots['$title-all']">
        <view
          v-if="!$slots.title || !$slots.$title"
          class="tn-collapse-item__head__title tn-text-ellipsis"
          :style="[
            { textAlign: align ? align : 'left'},
            isShow && activeStyle && !arrow ? activeStyle : ''
          ]"
        >{{ title }}</view>
        <view v-else>
          <slot name="title"></slot>
        </view>
        <view class="tn-collapse-item__head__icon__wrap">
          <view
            v-if="arrow"
            class="tn-icon-down tn-collapse-item__head__icon__arrow"
            :class="{'tn-collapse-item__head__icon__arrow--active': isShow}"
            :style="[arrowIconStyle]"
          ></view>
        </view>
      </block>
      <view v-else>
        <slot name="title-all"></slot>
      </view>
    </view>
    <!-- å†…容 -->
    <view
      class="tn-collapse-item__body"
      :style="[{
        height: isShow ? height + 'px' : '0'
      }]"
    >
      <view class="tn-collapse-item__body__content" :id="elId" :style="[bodyStyle]">
        <slot></slot>
      </view>
    </view>
  </view>
</template>
<script>
  export default {
    name: 'tn-collapse-item',
    props: {
      // å±•å¼€
      open: {
        type: Boolean,
        default: false
      },
      // å”¯ä¸€æ ‡è¯†
      name: {
        type: String,
        default: ''
      },
      // æ ‡é¢˜
      title: {
        type: String,
        default: ''
      },
      // æ ‡é¢˜å¯¹é½æ–¹å¼
      align: {
        type: String,
        default: 'left'
      },
      // ç‚¹å‡»ä¸æ”¶èµ·
      disabled: {
        type: Boolean,
        default: false
      },
      // æ´»åŠ¨æ—¶æ ·å¼
      activeStyle: {
        type: Object,
        default() {
          return {}
        }
      },
      // æ ‡è¯†
      index: {
        type: [Number, String],
        default: ''
      }
    },
    computed: {
      arrowIconStyle() {
        let style = {}
        if (this.arrowColor) {
          style.color = this.arrowColor
        }
        return style
      }
    },
    data() {
      return {
        isShow: false,
        elId: this.$t.uuid(),
        // body高度
        height: 0,
        // å¤´éƒ¨æ ·å¼
        headStyle: {},
        // ä¸»ä½“样式
        bodyStyle: {},
        // item样式
        itemStyle: {},
        // æ˜¾ç¤ºå³è¾¹ç®­å¤´
        arrow: true,
        // ç®­å¤´é¢œè‰²
        arrowColor: '',
        // ç‚¹å‡»å¤´éƒ¨æ—¶çš„æ•ˆæžœæ ·å¼
        hoverClass: ''
      }
    },
    watch: {
      open(value) {
        this.isShow = value
      }
    },
    created() {
      this.parent = false
      this.isShow = this.open
    },
    mounted() {
      this.init()
    },
    methods: {
      // å¼‚步获取内容或者修改了内容时重新获取内容的信息
      init() {
        this.parent = this.$t.$parent.call(this, 'tn-collapse')
        if (this.parent) {
          this.nameSync = this.name ? this.name : this.parent.childrens.length
          // ä¸å­˜åœ¨æ‰æ·»åŠ å¯¹åº”å®žä¾‹
          !this.parent.childrens.includes(this) && this.parent.childrens.push(this)
          this.headStyle = this.parent.headStyle
          this.bodyStyle = this.parent.bodyStyle
          this.itemStyle = this.parent.itemStyle
          this.arrow = this.parent.arrow
          this.arrowColor = this.parent.arrowColor
          this.hoverClass = this.parent.hoverClass
        }
        this.$nextTick(() => {
          this.queryRect()
        })
      },
      // ç‚¹å‡»å¤´éƒ¨
      headClick() {
        if (this.disabled) return
        if (this.parent && this.parent.accordion) {
          this.parent.childrens.map(child => {
            // å¦‚果是手风琴模式,将其他的item关闭
            if (this !== child) {
              child.isShow = false
            }
          })
        }
        this.isShow = !this.isShow
        // è§¦å‘修改事件
        this.$emit('change', {
          index: this.index,
          show: this.isShow
        })
        // åªæœ‰åœ¨æ‰“开时才触发父元素的change
        if (this.isShow) this.parent && this.parent.onChange()
        this.$forceUpdate()
      },
      // æŸ¥è¯¢å†…容高度
      queryRect() {
        this._tGetRect('#'+this.elId).then(res => {
          this.height = res.height
        })
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-collapse-item {
    &__head {
      position: relative;
      display: flex;
      flex-direction: row;
      justify-content: space-around;
      align-items: center;
      color: $tn-font-color;
      font-size: 30rpx;
      line-height: 1;
      padding: 24rpx 0;
      padding-left: 24rpx;
      text-align: left;
      background-color: #FFFFFF;
      &__title {
        flex: 1;
        overflow: hidden;
      }
      &__icon {
        &__arrow {
          transition: all 0.3s;
          margin-right: 20rpx;
          margin-left: 14rpx;
          font-size: inherit;
          &--active {
            transform: rotate(180deg);
            transform-origin: center center;
          }
        }
      }
    }
    &__body {
      transition: all 0.3s;
      overflow: hidden;
      &__content {
        overflow: hidden;
        font-size: 28rpx;
        color: $tn-font-color;
        text-align: left;
        background-color: #FFFFFF;
        padding-left: 24rpx;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-collapse/tn-collapse.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,98 @@
<template>
  <view class="tn-collapse-class tn-collapse">
    <slot></slot>
  </view>
</template>
<script>
  export default {
    name: 'tn-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: '#AAAAAA'
      },
      // ç‚¹å‡»æ ‡é¢˜æ æ—¶çš„æŒ‰åŽ‹æ ·å¼
      hoverClass: {
        type: String,
        default: 'tn-hover'
      }
    },
    computed: {
      parentData() {
        return [this.headStyle, this.bodyStyle, this.itemStyle, this.arrow, this.arrowColor, this.hoverClass]
      }
    },
    data() {
      return {
      }
    },
    watch: {
      parentData() {
        // å¦‚果父组件的参数发生变化重新初始化子组件的信息
        if (this.childrens.length > 0) {
          this.init()
        }
      }
    },
    created() {
      this.childrens = []
    },
    methods: {
      // é‡æ–°åˆå§‹åŒ–内部所有子元素计算高度,异步获取数据时重新渲染
      init() {
        this.childrens.forEach((child, index) => {
          child.init()
        })
      },
      // collapseItem被点击时由collapseItem调用父组件
      onChange() {
        let activeItem = []
        this.childrens.forEach((child, index) => {
          if (child.isShow) {
            activeItem.push(child.nameSync)
          }
        })
        // å¦‚果时手风琴模式,只有一个匹配结果,即activeItem长度为1
        if (this.accordion) activeItem = activeItem.join(',')
        this.$emit('change', activeItem)
      }
    }
  }
</script>
<style lang="scss" scoped>
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-color-icon/tn-color-icon.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,318 @@
<template>
  <text
    class="tn-color-icon-class tn-color-icon"
    :class="[
      'tn-color-icon-' + name
    ]"
    :style="{
      fontSize: size + unit,
      margin: margin
    }"
    @tap="handleClick"
  ></text>
</template>
<script>
  export default {
    name: 'tn-color-icon',
    props: {
      // ç´¢å¼•
      index: {
        type: [Number, String],
        default: '0'
      },
      // å›¾æ ‡åç§°
      name: {
        type: String,
        default: ''
      },
      // å›¾æ ‡å¤§å°
      size: {
        type: Number,
        default:32
      },
      // å¤§å°å•位
      unit: {
        type: String,
        default: 'px'
      },
      // å¤–边距
      margin: {
        type: String,
        default: '0'
      }
    },
    computed: {
    },
    data() {
      return {
      }
    },
    methods: {
      // å¤„理点击事件
      handleClick() {
        this.$emit("click", {
          index: Number(this.index)
        })
        this.$emit("tap", {
          index: Number(this.index)
        })
      }
    }
  }
</script>
<style scoped>
  @charset "UTF-8";
  @font-face {
    font-family: "tuniaoColorFont"; /* Project id 2445412 */
    /* Color fonts */
    src: url('iconfont.woff2?t=1632654518618') format('woff2');
  }
  .tn-color-icon {
    font-family: "tuniaoColorFont" !important;
    font-size: 16px;
    font-style: normal;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    text-decoration: none;
  }
  .tn-color-icon-logo-github:before {
    content: "\e601";
  }
  .tn-color-icon-logo-qq:before {
    content: "\e602";
  }
  .tn-color-icon-logo-weixin:before {
    content: "\e603";
  }
  .tn-color-icon-logo-alipay:before {
    content: "\e604";
  }
  .tn-color-icon-logo-weibo:before {
    content: "\e605";
  }
  .tn-color-icon-logo-dingtalk:before {
    content: "\e606";
  }
  .tn-color-icon-safe:before {
    content: "\e607";
  }
  .tn-color-icon-wifi:before {
    content: "\e608";
  }
  .tn-color-icon-help:before {
    content: "\e609";
  }
  .tn-color-icon-tag:before {
    content: "\e60a";
  }
  .tn-color-icon-play:before {
    content: "\e60b";
  }
  .tn-color-icon-stopwatch:before {
    content: "\e60c";
  }
  .tn-color-icon-home:before {
    content: "\e60d";
  }
  .tn-color-icon-map:before {
    content: "\e60e";
  }
  .tn-color-icon-book:before {
    content: "\e60f";
  }
  .tn-color-icon-qrcode:before {
    content: "\e610";
  }
  .tn-color-icon-discover:before {
    content: "\e611";
  }
  .tn-color-icon-visitor:before {
    content: "\e612";
  }
  .tn-color-icon-menu:before {
    content: "\e613";
  }
  .tn-color-icon-renew:before {
    content: "\e614";
  }
  .tn-color-icon-business:before {
    content: "\e615";
  }
  .tn-color-icon-telephone:before {
    content: "\e616";
  }
  .tn-color-icon-medicine:before {
    content: "\e617";
  }
  .tn-color-icon-chicken:before {
    content: "\e618";
  }
  .tn-color-icon-clock:before {
    content: "\e619";
  }
  .tn-color-icon-download:before {
    content: "\e61a";
  }
  .tn-color-icon-lamp:before {
    content: "\e61b";
  }
  .tn-color-icon-hourglass:before {
    content: "\e61c";
  }
  .tn-color-icon-calendar:before {
    content: "\e61d";
  }
  .tn-color-icon-bluetooth:before {
    content: "\e61e";
  }
  .tn-color-icon-fish:before {
    content: "\e61f";
  }
  .tn-color-icon-seal:before {
    content: "\e620";
  }
  .tn-color-icon-remind:before {
    content: "\e621";
  }
  .tn-color-icon-music:before {
    content: "\e622";
  }
  .tn-color-icon-email:before {
    content: "\e623";
  }
  .tn-color-icon-medal:before {
    content: "\e624";
  }
  .tn-color-icon-image:before {
    content: "\e625";
  }
  .tn-color-icon-network:before {
    content: "\e626";
  }
  .tn-color-icon-wallet:before {
    content: "\e627";
  }
  .tn-color-icon-program:before {
    content: "\e628";
  }
  .tn-color-icon-shrimp:before {
    content: "\e629";
  }
  .tn-color-icon-collect:before {
    content: "\e62a";
  }
  .tn-color-icon-screw:before {
    content: "\e62b";
  }
  .tn-color-icon-set:before {
    content: "\e62c";
  }
  .tn-color-icon-userfavorite:before {
    content: "\e62d";
  }
  .tn-color-icon-useradd:before {
    content: "\e62e";
  }
  .tn-color-icon-honor:before {
    content: "\e62f";
  }
  .tn-color-icon-shop:before {
    content: "\e630";
  }
  .tn-color-icon-usercard:before {
    content: "\e631";
  }
  .tn-color-icon-school:before {
    content: "\e632";
  }
  .tn-color-icon-user:before {
    content: "\e633";
  }
  .tn-color-icon-internet:before {
    content: "\e634";
  }
  .tn-color-icon-time:before {
    content: "\e635";
  }
  .tn-color-icon-topic:before {
    content: "\e636";
  }
  .tn-color-icon-phone:before {
    content: "\e637";
  }
  .tn-color-icon-usertable:before {
    content: "\e638";
  }
  .tn-color-icon-userset:before {
    content: "\e639";
  }
  .tn-color-icon-game:before {
    content: "\e63a";
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-column-notice/tn-column-notice.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,251 @@
<template>
  <view
    class="tn-column-notice-class tn-column-notice"
    :class="[backgroundColorClass]"
    :style="[noticeStyle]"
  >
    <!-- å·¦å›¾æ ‡ -->
    <view class="tn-column-notice__icon">
      <view
        v-if="leftIcon"
        class="tn-column-notice__icon--left"
        :class="[`tn-icon-${leftIconName}`,fontColorClass]"
        :style="[fontStyle('leftIcon')]"
        @tap="clickLeftIcon"></view>
    </view>
    <!-- æ»šåŠ¨æ˜¾ç¤ºå†…å®¹ -->
    <swiper class="tn-column-notice__swiper" :style="[swiperStyle]" :vertical="vertical" circular :autoplay="autoplay && playStatus === 'play'" :interval="duration" @change="change">
      <swiper-item v-for="(item, index) in list" :key="index" class="tn-column-notice__swiper--item">
        <view
          class="tn-column-notice__swiper--content tn-text-ellipsis"
          :class="[fontColorClass]"
          :style="[fontStyle()]"
          @tap="click(index)"
        >{{ item }}</view>
      </swiper-item>
    </swiper>
    <!-- å³å›¾æ ‡ -->
    <view class="tn-column-notice__icon">
      <view
        v-if="rightIcon"
        class="tn-column-notice__icon--right"
        :class="[`tn-icon-${rightIconName}`,fontColorClass]"
        :style="[fontStyle('rightIcon')]"
        @tap="clickRightIcon"></view>
      <view
        v-if="closeBtn"
        class="tn-column-notice__icon--right"
        :class="[`tn-icon-close`,fontColorClass]"
        :style="[fontStyle('close')]"
        @tap="close"></view>
    </view>
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    name: 'tn-column-notice',
    mixins: [componentsColorMixin],
    props: {
      // æ˜¾ç¤ºçš„内容
      list: {
        type: Array,
        default() {
          return []
        }
      },
      // æ˜¯å¦æ˜¾ç¤º
      show: {
        type: Boolean,
        default: true
      },
      // æ’­æ”¾çŠ¶æ€
      // play -> æ’­æ”¾ paused -> æš‚停
      playStatus: {
        type: String,
        default: 'play'
      },
      // æ»šåŠ¨æ–¹å‘
      // horizontal -> æ°´å¹³æ»šåЍ vertical -> åž‚直滚动
      mode: {
        type: String,
        default: 'horizontal'
      },
      // æ˜¯å¦æ˜¾ç¤ºå·¦è¾¹å›¾æ ‡
      leftIcon: {
        type: Boolean,
        default: true
      },
      // å·¦è¾¹å›¾æ ‡çš„名称
      leftIconName: {
        type: String,
        default: 'sound'
      },
      // å·¦è¾¹å›¾æ ‡çš„大小
      leftIconSize: {
        type: Number,
        default: 34
      },
      // æ˜¯å¦æ˜¾ç¤ºå³è¾¹çš„图标
      rightIcon: {
        type: Boolean,
        default: false
      },
      // å³è¾¹å›¾æ ‡çš„名称
      rightIconName: {
        type: String,
        default: 'right'
      },
      // å³è¾¹å›¾æ ‡çš„大小
      rightIconSize: {
        type: Number,
        default: 26
      },
      // æ˜¯å¦æ˜¾ç¤ºå…³é—­æŒ‰é’®
      closeBtn: {
        type: Boolean,
        default: false
      },
      // åœ†è§’
      radius: {
        type: Number,
        default: 0
      },
      // å†…边距
      padding: {
        type: String,
        default: '18rpx 24rpx'
      },
      // è‡ªåŠ¨æ’­æ”¾
      autoplay: {
        type: Boolean,
        default: true
      },
      // æ»šåŠ¨å‘¨æœŸ
      duration: {
        type: Number,
        default: 2000
      }
    },
    computed: {
      fontStyle() {
        return (type) => {
          let style = {}
          style.color = this.fontColorStyle ? this.fontColorStyle : ''
          style.fontSize = this.fontSizeStyle ? this.fontSizeStyle : ''
          if (type === 'leftIcon' && this.leftIconSize) {
            style.fontSize = this.leftIconSize + 'rpx'
          }
          if (type === 'rightIcon' && this.rightIconSize) {
            style.fontSize = this.rightIconSize + 'rpx'
          }
          if (type === 'close') {
            style.fontSize = '24rpx'
          }
          return style
        }
      },
      noticeStyle() {
        let style = {}
        style.backgroundColor = this.backgroundColorStyle ? this.backgroundColorStyle : 'transparent'
        if (this.padding) style.padding = this.padding
        return style
      },
      swiperStyle() {
        let style = {}
        style.height = this.fontSize ? this.fontSize + 6 + this.fontUnit : '32rpx'
        style.lineHeight = style.height
        return style
      },
      // æ ‡è®°æ˜¯å¦ä¸ºåž‚ç›´
      vertical() {
        if (this.mode === 'horizontal') return false
        else return true
      }
    },
    data() {
      return {
      }
    },
    watch: {
    },
    methods: {
      // ç‚¹å‡»äº†é€šçŸ¥æ 
      click(index) {
        this.$emit('click', index)
      },
      // ç‚¹å‡»äº†å…³é—­æŒ‰é’®
      close() {
        this.$emit('close')
      },
      // ç‚¹å‡»äº†å·¦è¾¹å›¾æ ‡
      clickLeftIcon() {
        this.$emit('clickLeft')
      },
      // ç‚¹å‡»äº†å³è¾¹å›¾æ ‡
      clickRightIcon() {
        this.$emit('clickRight')
      },
      // åˆ‡æ¢æ¶ˆæ¯æ—¶é—´
      change(event) {
        let index = event.detail.current
        if (index === this.list.length - 1) {
          this.$emit('end')
        }
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-column-notice {
    width: 100%;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    flex-wrap: nowrap;
    overflow: hidden;
    &__swiper {
      height: auto;
      flex: 1;
      display: flex;
      flex-direction: row;
      align-items: center;
      margin-left: 12rpx;
      &--item {
        display: flex;
        flex-direction: row;
        align-items: center;
        overflow: hidden;
      }
      &--content {
        overflow: hidden;
      }
    }
    &__icon {
      &--left {
        display: inline-flex;
        align-items: center;
      }
      &--right {
        margin-left: 12rpx;
        display: inline-flex;
        align-items: center;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-count-down/tn-count-down.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,314 @@
<template>
  <view class="tn-countdown-class tn-countdown">
    <view
      v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))"
      class="tn-countdown__item"
      :class="[backgroundColorClass]"
      :style="[itemStyle]"
    >
      <view class="tn-countdown__item__time" :class="[fontColorClass]" :style="[letterStyle]">
        {{ d }}
      </view>
    </view>
    <view
      v-if="showHours && (hideZeroDay || (!hideZeroDay && d != '00'))"
      class="tn-countdown__separator"
      :style="{
        fontSize: separatorSize + 'rpx',
        color: separatorColor,
        paddingBottom: separator === 'en' ? '4rpx' : 0
      }"
    >
      {{ separator === 'en' ? ':' : '天'}}
    </view>
    <view
      v-if="showHours"
      class="tn-countdown__item"
      :class="[backgroundColorClass]"
      :style="[itemStyle]"
    >
      <view class="tn-countdown__item__time" :class="[fontColorClass]" :style="[letterStyle]">
        {{ h }}
      </view>
    </view>
    <view
      v-if="showMinutes"
      class="tn-countdown__separator"
      :style="{
        fontSize: separatorSize + 'rpx',
        color: separatorColor,
        paddingBottom: separator === 'en' ? '4rpx' : 0
      }"
    >
      {{ separator === 'en' ? ':' : '时'}}
    </view>
    <view
      v-if="showMinutes"
      class="tn-countdown__item"
      :class="[backgroundColorClass]"
      :style="[itemStyle]"
    >
      <view class="tn-countdown__item__time" :class="[fontColorClass]" :style="[letterStyle]">
        {{ m }}
      </view>
    </view>
    <view
      v-if="showSeconds"
      class="tn-countdown__separator"
      :style="{
        fontSize: separatorSize + 'rpx',
        color: separatorColor,
        paddingBottom: separator === 'en' ? '4rpx' : 0
      }"
    >
      {{ separator === 'en' ? ':' : '分'}}
    </view>
    <view
      v-if="showSeconds"
      class="tn-countdown__item"
      :class="[backgroundColorClass]"
      :style="[itemStyle]"
    >
      <view class="tn-countdown__item__time" :class="[fontColorClass]" :style="[letterStyle]">
        {{ s }}
      </view>
    </view>
    <view
      v-if="showSeconds && separator === 'cn'"
      class="tn-countdown__separator"
      :style="{
        fontSize: separatorSize + 'rpx',
        color: separatorColor,
        paddingBottom: separator === 'en' ? '4rpx' : 0
      }"
    >
      ç§’
    </view>
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    name: 'tn-count-down',
    mixins: [componentsColorMixin],
    props: {
      // å€’计时时间,秒作为单位
      timestamp: {
        type: Number,
        default: 0
      },
      // æ˜¯å¦è‡ªåЍ开始
      autoplay: {
        type: Boolean,
        default: true
      },
      // æ•°å­—框高度
      height: {
        type: [String, Number],
        default: 'auto'
      },
      // åˆ†éš”符类型
      // en -> ä½¿ç”¨è‹±æ–‡çš„冒号 cn -> ä½¿ç”¨ä¸­æ–‡è¿›è¡Œåˆ†å‰²
      separator: {
        type: String,
        default: 'en'
      },
      // åˆ†å‰²ç¬¦å¤§å°
      separatorSize: {
        type: Number,
        default: 30
      },
      // åˆ†éš”符颜色
      separatorColor: {
        type: String,
        default: '#080808'
      },
      // æ˜¯å¦æ˜¾ç¤ºè¾¹æ¡†
      showBorder: {
        type: Boolean,
        default: false
      },
      // è¾¹æ¡†é¢œè‰²
      borderColor: {
        type: String,
        default: '#080808'
      },
      // æ˜¯å¦æ˜¾ç¤ºç§’
      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
      }
    },
    computed: {
      // å€’计时item的样式
      itemStyle() {
        let style = {}
        if (this.height) {
          style.height = this.$t.string.getLengthUnitValue(this.height)
          style.width = style.height
        }
        if (this.showBorder) {
          style.borderStyle = 'solid'
          style.borderColor = this.borderColor
          style.borderWidth = '1rpx'
        }
        style.backgroundColor = this.backgroundColorStyle || '#FFFFFF'
        return style
      },
      // å€’计时数字样式
      letterStyle() {
        let style = {}
        style.fontSize = this.fontSizeStyle || '30rpx'
        style.color = this.fontColorStyle || '#080808'
        return style
      }
    },
    data() {
      return {
        d: '00',
        h: '00',
        m: '00',
        s: '00',
        // å®šæ—¶å™¨
        timer: null,
        // è®°å½•倒计过程中变化的秒数
        seconds: 0
      }
    },
    watch: {
      // ç›‘听时间戳变化
      timestamp(value) {
        this.clearTimer()
        this.start()
      }
    },
    mounted() {
      // å¦‚果时自动倒计时,加载完成开始计时
      this.autoplay && this.timestamp && this.start()
    },
    beforeDestroy() {
      this.clearTimer()
    },
    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)
        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) - (minute * 60) - (hour * 60 * 60) - (day * 24 * 60 * 60)
        // å¦‚果小于0在前面进行补0操作
        showHour = this.$t.number.formatNumberAddZero(showHour)
        minute = this.$t.number.formatNumberAddZero(minute)
        second = this.$t.number.formatNumberAddZero(second)
        day = this.$t.number.formatNumberAddZero(day)
        this.d = day
        this.h = showHour
        this.m = minute
        this.s = second
      },
      // å€’计时结束
      end() {
        this.clearTimer()
        this.$emit('end')
      },
      // æ¸…除倒计时
      clearTimer() {
        if (this.timer !== null) {
          clearInterval(this.timer)
          this.timer = null
        }
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-countdown {
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
    align-items: center;
    &__item {
      box-sizing: content-box;
      display: flex;
      flex-direction: row;
      align-items: center;
      justify-content: center;
      padding: 2rpx;
      border-radius: 6rpx;
      white-space: nowrap;
      transform: translateZ(0);
      &__time {
        margin: 0;
        padding: 0;
        line-height: 1;
      }
    }
    &__separator {
      display: flex;
      flex-direction: row;
      align-items: center;
      justify-content: center;
      padding: 0 5rpx;
      line-height: 1;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-count-scroll/tn-count-scroll.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,171 @@
<template>
  <view class="tn-count-scroll-class tn-count-scroll">
    <view
      v-for="(item, index) in columns"
      :key="index"
      class="tn-count-scroll__box"
      :style="{
        width: $t.string.getLengthUnitValue(width),
        height: heightPxValue + 'px'
      }"
    >
      <view
        class="tn-count-scroll__column"
        :style="{
          transform: `translate3d(0, -${keys[index] * heightPxValue}px, 0)`,
          transitionDuration: `${duration}s`
        }"
      >
        <view
          v-for="(value, value_index) in item"
          :key="value_index"
          class="tn-count-scroll__column__item"
          :class="[fontColorClass]"
          :style="{
            height: heightPxValue + 'px',
            lineHeight: heightPxValue + 'px',
            fontSize: fontSizeStyle || '32rpx',
            fontWeight: bold ? 'bold': 'normal',
            color: fontColorStyle || '#080808'
          }"
        >
          {{ value }}
        </view>
      </view>
    </view>
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    name: 'tn-count-scroll',
    mixins: [componentsColorMixin],
    props: {
      value: {
        type: Number,
        default: 0
      },
      // è¡Œé«˜
      height: {
        type: Number,
        default: 32
      },
      // å•个字的宽度
      width: {
        type: [String, Number],
        default: 'auto'
      },
      // æ˜¯å¦åŠ ç²—
      bold: {
        type: Boolean,
        default: false
      },
      // æŒç»­æ—¶é—´
      duration: {
        type: Number,
        default: 1.2
      },
      // ååˆ†ä½åˆ†å‰²ç¬¦
      decimalSeparator: {
        type: String,
        default: '.'
      },
      // åƒåˆ†ä½åˆ†å‰²ç¬¦
      thousandthsSeparator: {
        type: String,
        default: ''
      }
    },
    computed: {
      heightPxValue() {
        return uni.upx2px(this.height || 0)
      }
    },
    data() {
      return {
        // æ¯åˆ—的数据
        columns: [],
        // æ¯åˆ—对应值所在的滚动位置
        keys: []
      }
    },
    watch: {
      value(val) {
        this.initColumn(val)
      }
    },
    created() {
      // ä¸ºäº†è¾¾åˆ°ä¸€è¿›å…¥å°±æœ‰æ»šåŠ¨æ•ˆæžœï¼Œå»¶è¿Ÿæ‰§è¡Œåˆå§‹åŒ–
      this.initColumn()
      setTimeout(() => {
        this.initColumn(this.value)
      }, 20)
    },
    methods: {
      // åˆå§‹åŒ–每一列的数据
      initColumn(val) {
        val = val + ''
        let digit = val.length,
            columnArray = [],
            rows = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
        for (let i = 0; i < digit; i++) {
          if (val[i] === this.decimalSeparator || val[i] === this.thousandthsSeparator) {
            columnArray.push(val[i])
          } else {
            columnArray.push(rows)
          }
        }
        this.columns = columnArray
        this.roll(val)
      },
      // æ»šåŠ¨å¤„ç†
      roll(value) {
        let valueArray = value.toString().split(''),
            lengths = this.columns.length,
            indexs = [];
        while (valueArray.length) {
          let figure = valueArray.pop()
          if (figure === this.decimalSeparator || figure === this.thousandthsSeparator) {
            indexs.unshift(0)
          } else {
            indexs.unshift(Number(figure))
          }
        }
        while(indexs.length < lengths) {
          indexs.unshift(0)
        }
        this.keys = indexs
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-count-scroll {
    display: inline-flex;
    align-items: center;
    justify-content: space-between;
    &__box {
      overflow: hidden;
    }
    &__column {
      transform: translate3d(0, 0, 0);
      display: flex;
      align-items: center;
      justify-content: center;
      flex-direction: column;
      transition-timing-function: cubic-bezier(0, 1, 0, 1);
      &__item {
        display: flex;
        align-items: center;
        justify-content: center;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-count-to/tn-count-to.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,231 @@
<template>
  <view
    class="tn-count-num-class tn-count-num"
    :class="[fontColorClass]"
    :style="{
      fontSize: fontSizeStyle || '50rpx',
      fontWeight: bold ? 'bold' : 'normal',
      color: fontColorStyle || '#080808'
    }"
  >
    {{ displayValue }}
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    name: 'tn-count-to',
    mixins: [componentsColorMixin],
    props: {
      // å¼€å§‹çš„æ•°å€¼ï¼Œé»˜è®¤ä¸º0
      startVal: {
        type: Number,
        default: 0
      },
      // ç»“束目标数值
      endVal: {
        type: Number,
        default: 0,
        required: true
      },
      // æ˜¯å¦è‡ªåЍ开始
      autoplay: {
        type: Boolean,
        default: true
      },
      // æ»šåŠ¨åˆ°ç›®æ ‡å€¼çš„æŒç»­æ—¶é—´ï¼Œå•ä½ä¸ºæ¯«ç§’
      duration: {
        type: Number,
        default: 2000
      },
      // æ˜¯å¦åœ¨å³å°†ç»“束的时候使用缓慢滚动的效果
      useEasing: {
        type: Boolean,
        default: true
      },
      // æ˜¾ç¤ºçš„小数位数
      decimals: {
        type: Number,
        default: 0
      },
      // åè¿›åˆ¶çš„分割符
      decimalSeparator: {
        type: String,
        default: '.'
      },
      // åƒåˆ†ä½çš„分隔符
      // ç±»ä¼¼é‡‘额的分割(ï¿¥23,321.05中的",")
      thousandthsSeparator: {
        type: String,
        default: ''
      },
      // æ˜¯å¦æ˜¾ç¤ºåŠ ç²—å­—ä½“
      bold: {
        type: Boolean,
        default: false
      }
    },
    computed: {
      countDown() {
        return this.startVal > this.endVal
      }
    },
    data() {
      return {
        localStartVal: this.startVal,
        localDuration: this.duration,
        // æ˜¾ç¤ºçš„æ•°å€¼
        displayValue: this.formatNumber(this.startVal),
        // æ‰“印的数值
        printValue: null,
        // æ˜¯å¦æš‚停
        paused: false,
        // å¼€å§‹æ—¶é—´æˆ³
        startTime: null,
        // åœç•™æ—¶é—´æˆ³
        remainingTime: null,
        // å½“前时间戳
        timestamp: null,
        // ä¸Šä¸€æ¬¡çš„æ—¶é—´æˆ³
        lastTime: 0,
        rAF: null
      }
    },
    watch: {
      startVal() {
        this.autoplay && this.start()
      },
      endVal() {
        this.autoplay && this.start()
      }
    },
    mounted() {
      this.autoplay && this.start()
    },
    methods: {
      // å¼€å§‹æ»šåЍ
      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.remainingTime
        this.localStartVal = this.printValue
        this.requestAnimationFrame(this.count)
      },
      // é‡ç½®
      reset() {
        this.startTime = null
        this.cnacelAnimationFrame(this.rAF)
        this.displayValue = this.formatNumber(this.startVal)
      },
      // é”€æ¯ç»„ä»¶
      destroyed() {
        this.cancelAnimationFrame(this.rAF)
      },
      // ç´¯åŠ æ—¶é—´
      count(timestamp) {
        if (!this.startTime) this.startTime = timestamp
        this.timestamp = timestamp
        const progress = timestamp - this.startTime
        this.remainingTime = this.localDuration - progress
        if (this.useEasing) {
          if (this.countDown) {
            this.printValue = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration)
          } {
            this.printValue = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration)
          }
        } else {
          if (this.countDown) {
            this.printValue = this.localStartVal - (this.localStartVal - this.endVal) * (progress / this.localDuration)
          } else {
            this.printValue = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration)
          }
        }
        if (this.countDown) {
          this.printValue = this.printValue < this.endVal ? this.endVal : this.printValue
        } else {
          this.printValue = this.printValue > this.endVal ? this.endVal : this.printValue
        }
        this.displayValue = this.formatNumber(this.printValue)
        if (progress < this.localDuration) {
          this.rAF = this.requestAnimationFrame(this.count)
        } else {
          this.$emit('end')
        }
      },
      // ç¼“动时间计算
      easingFn(t, b, c, d) {
        return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
      },
      // è¯·æ±‚帧动画
      requestAnimationFrame(cb) {
        const currentTime = new Date().getTime()
        // ä¸ºäº†ä½¿setTimteout的尽可能的接近每秒60帧的效果
        const timeToCall = Math.max(0, 16 - (currentTime - this.lastTime))
        const timerId = setTimeout(() => {
          cb && cb(currentTime + timeToCall)
        }, timeToCall)
        this.lastTime = currentTime + timeToCall
        return timerId
      },
      // æ¸…除帧动画
      clearAnimationFrame(timerId) {
        clearTimeout(timerId)
      },
      // æ ¼å¼åŒ–数值
      formatNumber(number) {
        const reg = /(\d+)(\d{3})/
        number = Number(number)
        number = number.toFixed(Number(this.decimals))
        number += ''
        const numberArray = number.split('.')
        let num1 = numberArray[0]
        const num2 = numberArray.length > 1 ? this.decimalSeparator + numberArray[1] : ''
        if (this.thousandthsSeparator && !this.isNumber(this.thousandthsSeparator)) {
          while(reg.test(num1)) {
            num1 = num1.replace(reg, '$1' + this.thousandthsSeparator + '$2')
          }
        }
        return num1 + num2
      },
      // åˆ¤æ–­æ˜¯å¦ä¸ºæ•°å­—
      isNumber(val) {
        return !isNaN(parseFloat(val))
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-count-num {
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
    text-align: center;
    line-height: 1;
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-cropper/index.wxs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,328 @@
var cropper = {
  // ç”»å¸ƒx轴起点
  cutX: 0,
  // ç”»å¸ƒy轴起点
  cutY: 0,
  // è§¦æ‘¸ç‚¹ä¿¡æ¯(手指与图片中心点的相对位置)
  touchRelactive: [{
    x: 0,
    y: 0
  }],
  // åŒæŒ‡è§¦æ‘¸æ—¶æ–œè¾¹çš„长度
  hypotenuseLength:0,
  // æ˜¯å¦ç»“束触摸
  touchEndFlag: false,
  // ç”»å¸ƒå®½é«˜
  canvasWidth: 0,
  canvasHeight: 0,
  // å›¾ç‰‡å®½é«˜
  imgWidth: 0,
  imgHeight: 0,
  // å›¾ç‰‡ç¼©æ”¾æ¯”例
  scale: 1,
  // å›¾ç‰‡æ—‹è½¬è§’度
  angle: 0,
  // å›¾ç‰‡ä¸Šè¾¹è·
  imgTop: 0,
  // å›¾ç‰‡å·¦è¾¹è·
  imgLeft: 0,
  // çª—口宽高
  windowWidth: 0,
  windowHeight: 0,
  init: true
}
function bool(str) {
  return str === 'true' || str === true
}
function propsChange(prop, oldProp, ownerInstance, instance) {
  if (prop && prop !== 'null') {
    var params = prop.split(',')
    var type = +params[0]
    var dataset = instance.getDataset()
    if (cropper.init || type == 4) {
      cropper.canvasWidth = +dataset.width
      cropper.canvasHeight = +dataset.height
      cropper.imgTop = +dataset.windowheight / 2
      cropper.imgLeft = +dataset.windowwidth / 2
      cropper.imgWidth = +dataset.imgwidth
      cropper.imgHeight = +dataset.imgheight
      cropper.windowHeight = +dataset.windowheight
      cropper.windowWidth = +dataset.windowwidth
      cropper.init = false
    } else if (type == 2 || type == 3) {
      cropper.imgWidth = +dataset.imgwidth
      cropper.imgHeight = +dataset.imgheight
    }
    cropper.angle = +dataset.angle
    if (type == 3) {
      imgTransform(ownerInstance)
    }
    switch(type) {
      case 1:
        setCutCenter(ownerInstance)
        // è®¾ç½®è£å‰ªæ¡†å¤§å°
        computeCutSize(ownerInstance)
        // æ£€æŸ¥è£å‰ªæ¡†æ˜¯å¦åœ¨èŒƒå›´å†…
        cutDetectionPosition(ownerInstance)
        break
      case 2:
        setCutCenter(ownerInstance)
        break
      case 3:
        imgMarginDetectionScale(ownerInstance)
        break
      case 4:
        imageReset(ownerInstance)
        break
      case 5:
        setCutCenter(ownerInstance)
        break
      default:
        break
    }
  }
}
function touchStart(event, ownerInstance) {
  var touch = event.touches || event.changedTouches
  cropper.touchEndFlag = false
  if (touch.length === 1) {
    cropper.touchRelactive[0] = {
      x: touch[0].pageX - cropper.imgLeft,
      y: touch[0].pageY - cropper.imgTop
    }
  } else {
    var width = Math.abs(touch[0].pageX - touch[1].pageX)
    var height = Math.abs(touch[0].pageY - touch[1].pageY)
    cropper.touchRelactive = [{
      x: touch[0].pageX - cropper.imgLeft,
      y: touch[0].pageY - cropper.imgTop
    },{
      x: touch[1].pageX - cropper.imgLeft,
      y: touch[1].pageY - cropper.imgTop
    }]
    cropper.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2))
  }
}
function touchMove(event, ownerInstance) {
  var touch = event.touches || event.changedTouches
  if (cropper.touchEndFlag) return
  moveDuring(ownerInstance)
  if (event.touches.length === 1) {
    var left = touch[0].pageX - cropper.touchRelactive[0].x,
      top = touch[0].pageY - cropper.touchRelactive[0].y;
    cropper.imgLeft = left
    cropper.imgTop = top
    imgTransform(ownerInstance)
    imgMarginDetectionPosition(ownerInstance)
  } else {
    var dataset = event.instance.getDataset()
    var minScale = +dataset.minscale
    var maxScale = +dataset.maxscale
    var width = Math.abs(touch[0].pageX - touch[1].pageX),
      height = Math.abs(touch[0].pageY - touch[1].pageY),
      hypotenuse = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)),
      scale = cropper.scale * (hypotenuse / cropper.hypotenuseLength),
      current_deg = 0;
    scale = scale <= minScale ? minScale : scale
    scale = scale >= maxScale ? maxScale : scale
    cropper.scale = scale
    imgMarginDetectionScale(ownerInstance, true)
    var touchRelative = [{
      x: touch[0].pageX - cropper.imgLeft,
      y: touch[0].pageY - cropper.imgTop
    }, {
      x: touch[1].pageX - cropper.imgLeft,
      y: touch[1].pageY - cropper.imgTop
    }]
    cropper.touchRelactive = touchRelative
    cropper.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2))
    // æ›´æ–°è§†å›¾
    cropper.angle = cropper.angle + current_deg
    imgTransform(ownerInstance)
  }
}
function touchEnd(event, ownerInstance) {
  cropper.touchEndFlag = true
  moveStop(ownerInstance)
  updateData(ownerInstance)
}
function moveDuring(ownerInstance) {
  if (!ownerInstance) return
  ownerInstance.callMethod('moveDuring')
}
function moveStop(ownerInstance) {
  if (!ownerInstance) return
  ownerInstance.callMethod('moveStop')
}
function setCutCenter(ownerInstance) {
  var cutX = (cropper.windowWidth - cropper.canvasWidth) * 0.5
  var cutY = (cropper.windowHeight - cropper.canvasHeight) * 0.5
  cropper.imgTop = cropper.imgTop - cropper.cutY + cutY
  cropper.cutY = cutY
  cropper.imgLeft = cropper.imgLeft - cropper.cutX + cutX
  cropper.cutX = cutX
  cutDetectionPosition(ownerInstance)
  imgTransform(ownerInstance)
  updateData(ownerInstance)
}
// æ£€æµ‹å‰ªè£æ¡†ä½ç½®æ˜¯å¦åœ¨å…è®¸çš„范围内(屏幕内)
function cutDetectionPosition(ownerInstance) {
  var windowHeight = cropper.windowHeight,
    windowWidth = cropper.windowWidth;
  // æ£€æµ‹ä¸Šè¾¹è·æ˜¯å¦åœ¨èŒƒå›´å†…
  var cutDetectionPositionTop = function() {
    if (cropper.cutY < 0) {
      cropper.cutY = 0
    }
    if (cropper.cutY > windowHeight - cropper.canvasHeight) {
      cropper.cutY = windowHeight - cropper.canvasHeight
    }
  }
  // æ£€æµ‹å·¦è¾¹è·æ˜¯å¦åœ¨èŒƒå›´å†…
  var cutDetectionPositionLeft = function() {
    if (cropper.cutX < 0) {
      cropper.cutX = 0
    }
    if (cropper.cutX > windowWidth - cropper.canvasWidth) {
      cropper.cutX = windowWidth - cropper.canvasWidth
    }
  }
  // è£å‰ªæ¡†åæ ‡å¤„理(如果只写一个参数则另一个默认为0,都不写默认为居中)
  if (cropper.cutX === null && cropper.cutY === null) {
    var cutX = (windowWidth - cropper.canvasWidth) * 0.5,
      cutY = (windowHeight - cropper.canvasHeight) * 0.5;
    cropper.cutX = cutX
    cropper.cutY = cutY
  } else if (cropper.cutX !== null && cropper.cutX !== null) {
    cutDetectionPositionTop()
    cutDetectionPositionLeft()
  } else if (cropper.cutX !== null && cropper.cutY === null) {
    cutDetectionPositionLeft()
    cropper.cutY = (windowHeight - cropper.canvasHeight) / 2
  } else if (cropper.cutX === null && cropper.cutY !== null) {
    cutDetectionPositionTop()
    cropper.cutX = (windowWidth - cropper.canvasWidth) / 2
  }
}
// å›¾ç‰‡è¾¹ç¼˜æ£€æµ‹-缩放
function imgMarginDetectionScale(ownerInstance, delay) {
  var scale = cropper.scale,
    imgWidth = cropper.imgWidth,
    imgHeight = cropper.imgHeight;
  if ((cropper.angle / 90) % 2) {
    imgWidth = cropper.imgHeight
    imgHeight = cropper.imgWidth
  }
  if (imgWidth * scale < cropper.canvasWidth) {
    scale = cropper.canvasWidth / imgWidth
  }
  if (imgHeight * scale < cropper.canvasHeight) {
    scale = Math.max(scale, cropper.canvasHeight / imgHeight)
  }
  imgMarginDetectionPosition(ownerInstance, scale, delay)
}
// å›¾ç‰‡è¾¹ç¼˜æ£€æµ‹-位置
function imgMarginDetectionPosition(ownerInstance, scale, delay) {
  var left = cropper.imgLeft,
    top = cropper.imgTop,
    imgWidth = cropper.imgWidth,
    imgHeight = cropper.imgHeight;
  scale = scale || cropper.scale
  if ((cropper.angle / 90) % 2) {
    imgWidth = cropper.imgHeight
    imgHeight = cropper.imgWidth
  }
  left = cropper.cutX + (imgWidth * scale) / 2 >= left ? left : cropper.cutX + (imgWidth * scale) / 2
  left = cropper.cutX + cropper.canvasWidth - (imgWidth * scale) / 2 <= left ? left : cropper.cutX + cropper.canvasWidth - (imgWidth * scale) / 2
  top = cropper.cutY + (imgHeight * scale) / 2 >= top ? top : cropper.cutY + (imgHeight * scale) / 2
  top = cropper.cutY + cropper.canvasHeight - (imgHeight * scale) / 2 <= top ? top : cropper.cutY + cropper.canvasHeight - (imgHeight * scale) / 2
  cropper.imgLeft = left
  cropper.imgTop = top
  cropper.scale = scale
  if (!delay || delay === 'null') {
    imgTransform(ownerInstance)
  }
}
// æ”¹å˜æˆªå–值大小
function computeCutSize(ownerInstance) {
  if (cropper.canvasWidth > cropper.windowWidth) {
    cropper.canvasWidth = cropper.windowWidth
  } else if (cropper.canvasWidth + cropper.cutX > cropper.windowWidth) {
    cropper.cutX = cropper.windowWidth - cropper.cutX
  }
  if (cropper.canvasHeight > cropper.windowHeight) {
    cropper.canvasHeight = cropper.windowHeight
  } else if (cropper.canvasHeight + cropper.cutY > cropper.windowHeight) {
    cropper.cutY = cropper.windowHeight - cropper.cutY
  }
}
// å›¾ç‰‡åŠ¨ç”»
function imgTransform(ownerInstance) {
  var image = ownerInstance.selectComponent('.tn-cropper__image')
  if (!image) return
  var x = cropper.imgLeft - cropper.imgWidth / 2,
    y = cropper.imgTop - cropper.imgHeight / 2;
  image.setStyle({
    'transform': 'translate3d('+ x + 'px,' + y + 'px,0) scale(' + cropper.scale +') rotate(' + cropper.angle + 'deg)'
  })
}
// å›¾ç‰‡é‡ç½®
function imageReset(ownerInstance) {
  cropper.scale = 1
  cropper.angle = 0
  imgTransform(ownerInstance)
}
// é«˜åº¦å˜åŒ–
function canvasHeight(ownerInstance) {
  if (!ownerInstance) return
  computeCutSize(ownerInstance)
}
// å®½åº¦å˜åŒ–
function canvasWidth(ownerInstance) {
  if (!ownerInstance) return
  computeCutSize(ownerInstance)
}
// æ›´æ–°æ•°æ®
function updateData(ownerInstance) {
  if (!ownerInstance) return
  ownerInstance.callMethod('change', {
    cutX: cropper.cutX,
    cutY: cropper.cutY,
    imgWidth: cropper.imgWidth,
    imgHeight: cropper.imgHeight,
    scale: cropper.scale,
    angle: cropper.angle,
    imgTop: cropper.imgTop,
    imgLeft: cropper.imgLeft
  })
}
module.exports = {
  touchStart: touchStart,
  touchMove: touchMove,
  touchEnd: touchEnd,
  propsChange: propsChange
}
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-cropper/tn-cropper.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,570 @@
<template>
  <view class="tn-cropper-class tn-cropper" @touchmove.stop.prevent="stop">
    <image
      v-if="imageUrl"
      :src="imageUrl"
      class="tn-cropper__image"
      :style="{
        width: (imgWidth ? imgWidth : width) + 'px',
        height: (imgHeight ? imgHeight : height) + 'px',
        transitionDuration: (animation ? 0.3 : 0) + 's'
      }"
      mode="widthFix"
      :data-minScale="minScale"
      :data-maxScale="maxScale"
      @load="imageLoad"
      @error="imageLoad"
      @touchstart="wxs.touchStart"
      @touchmove="wxs.touchMove"
      @touchend="wxs.touchEnd"
    ></image>
    <view
      class="tn-cropper__wrapper"
      :style="{
        width: width + 'px',
        height: height + 'px',
        borderRadius: isRound ? '50%' : '0'
      }"
    >
      <view
        class="tn-cropper__border"
        :style="{
          border: borderStyle,
          borderRadius: isRound ? '50%' : '0',
        }"
        :prop="props"
        :change:prop="wxs.propsChange"
        :data-width="width"
        :data-height="height"
        :data-windowHeight="systemInfo.windowHeight || 600"
        :data-windowWidth="systemInfo.windowWidth || 400"
        :data-imgTop="imgTop"
        :data-imgWidth="imgWidth"
        :data-imgHeight="imgHeight"
        :data-angle="angle"
      ></view>
    </view>
    <canvas
      class="tn-cropper__canvas"
      :style="{
        width: width * scaleRatio + 'px',
        height: height * scaleRatio + 'px'
      }"
      :canvas-id="CANVAS_ID"
      :id="CANVAS_ID"
      :disable-scroll="true"
    ></canvas>
    <view
      v-if="!custom"
      class="tn-cropper__tabbar"
    >
      <view class="tn-cropper__tabbar__btn tn-cropper__tabber__cancel" @tap.stop="back">取消</view>
      <view class="tn-cropper__tabbar__rotate" :class="[`tn-icon-${rotateIcon}`]" @tap.stop="setAngle"></view>
      <view class="tn-cropper__tabbar__btn tn-cropper__tabber__confirm" @tap.stop="getCutImage">完成</view>
    </view>
  </view>
</template>
<script src="./index.wxs" lang="wxs" module="wxs"></script>
<script>
  export default {
    name: 'tn-cropper',
    props: {
      // å›¾ç‰‡è·¯å¾„
      imageUrl: {
        type: String,
        default: ''
      },
      // è£å‰ªæ¡†é«˜åº¦ px
      height: {
        type: Number,
        default: 280
      },
      // è£å‰ªæ¡†çš„宽度 px
      width: {
        type: Number,
        default: 280
      },
      // æ˜¯å¦ä¸ºåœ†å½¢è£å‰ªæ¡†
      isRound: {
        type: Boolean,
        default: false
      },
      // è£å‰ªæ¡†è¾¹æ¡†æ ·å¼
      borderStyle: {
        type: String,
        default: '1rpx solid #FFF'
      },
      // ç”Ÿæˆçš„图片尺寸相对于裁剪框的比例
      scaleRatio: {
        type: Number,
        default: 1
      },
      // è£å‰ªåŽçš„图片质量
      // å–值范围为:(0, 1]
      quality: {
        type: Number,
        default: 0.8
      },
      // æ˜¯å¦è¿”回base64(H5默认为base64)
      returnBase64: {
        type: Boolean,
        default: false
      },
      // å›¾ç‰‡æ—‹è½¬è§’度
      rotateAngle: {
        type: Number,
        default: 0
      },
      // å›¾ç‰‡æœ€å°ç¼©æ”¾æ¯”
      minScale: {
        type: Number,
        default: 0.5
      },
      // å›¾ç‰‡æœ€å¤§ç¼©æ”¾æ¯”
      maxScale: {
        type: Number,
        default: 2
      },
      // è‡ªå®šä¹‰æ“ä½œæ (设置后会隐藏默认的底部操作栏)
      custom: {
        type: Boolean,
        default: false
      },
      // æ˜¯å¦åœ¨å€¼å‘生改变的时候开始裁剪
      // custom为true时生效
      startCutting: {
        type: Boolean,
        default: false
      },
      // è£å‰ªæ—¶æ˜¯å¦æ˜¾ç¤ºloading
      loading: {
        type: Boolean,
        default: true
      },
      // æ—‹è½¬å›¾ç‰‡å›¾æ ‡
      rotateIcon: {
        type: String,
        default: 'circle-arrow'
      }
    },
    data() {
      return {
        // canvas容器id
        CANVAS_ID: 'tn-cropper-canvas',
        // ç§»åŠ¨è£å‰ªè¶…æ—¶æ—¶é—´å®šæ—¶å™¨
        TIME_CUT_CENTER: null,
        // canvas容器
        ctx: null,
        // ç”»å¸ƒx轴起点
        cutX: 0,
        // ç”»å¸ƒy轴起点
        cutY: 0,
        // å›¾ç‰‡å®½åº¦
        imgWidth: 0,
        // å›¾ç‰‡é«˜åº¦
        imgHeight: 0,
        // å›¾ç‰‡åº•部位置
        imgTop: 0,
        // å›¾ç‰‡å·¦è¾¹ä½ç½®
        imgLeft: 0,
        // å›¾ç‰‡ç¼©æ”¾æ¯”
        scale: 1,
        // å›¾ç‰‡æ—‹è½¬è§’度
        angle: 0,
        // å¼€å¯åŠ¨ç”»è¿‡æ¸¡æ•ˆæžœ
        animation: false,
        // åŠ¨ç”»å®šæ—¶å™¨
        animationTime: null,
        // ç³»ç»Ÿä¿¡æ¯
        systemInfo: {},
        // ä¼ é€’的参数
        props: '',
        // æ ‡è®°æ˜¯å¦å‘生改变
        sizeChange: 0,
        angleChange: 0,
        resetChange: 0,
        centerChange: 0
      }
    },
    watch: {
      imageUrl(val) {
        this.imageReset()
        this.showLoading()
        uni.getImageInfo({
          src: val,
          success: (res) => {
            // è®¡ç®—图片尺寸
            this.imgComputeSize(res.width, res.height)
            this.angleChange++
            this.props = `3,${this.angleChange}`
          },
          fail: (err) => {
            console.log(err);
            this.imgComputeSize()
            this.angleChange++
            this.props = `3,${this.angleChange}`
          }
        })
      },
      rotateAngle(val) {
        this.animation = true
        this.angle = val
        this.angleChanged(val)
      },
      animation(val) {
        clearTimeout(this.animationTime)
        if (val) {
          this.animationTime = setTimeout(() => {
            this.animation = false
          }, 200)
        }
      },
      startCutting(val) {
        if (this.custom && val) {
          this.getCutImage()
        }
      }
    },
    mounted() {
      this.systemInfo = uni.getSystemInfoSync()
      this.imgTop = this.systemInfo.windowHeight / 2
      this.imgLeft = this.systemInfo.windowWidth / 2
      this.ctx = uni.createCanvasContext(this.CANVAS_ID, this)
      // åˆå§‹åŒ–
      this.$nextTick(() => {
        this.props = '1,1'
      })
      setTimeout(() => {
        this.$emit('ready', {})
      }, 200)
    },
    methods: {
      // å°†ç½‘络图片转换为本地图片【同步执行】
      async getLocalImage(url) {
        return await new Promise((resolve, reject) => {
          uni.downloadFile({
            url: url,
            success: (res) => {
              resolve(res.tempFilePath)
            },
            fail: (err) => {
              reject(false)
            }
          })
        })
      },
      // è¿”回裁剪后的图片信息
      getCutImage() {
        if (!this.imageUrl) {
          uni.showToast({
            title: '请选择图片',
            icon: 'none'
          })
          return
        }
        this.loading && this.showLoading()
        const draw = async () => {
          // å›¾ç‰‡å®žé™…大小
          let imgWidth = this.imgWidth * this.scale * this.scaleRatio
          let imgHeight = this.imgHeight * this.scale * this.scaleRatio
          // canvas和图片的相对距离
          let xpos = this.imgLeft - this.cutX
          let ypos = this.imgTop - this.cutY
          let imgUrl = this.imageUrl
          // #ifdef APP-PLUS || MP-WEIXIN
          if (~this.imageUrl.indexOf('https:')) {
            imgUrl = await this.getLocalImage(this.imageUrl)
          }
          // #endif
          // æ—‹è½¬ç”»å¸ƒ
          this.ctx.translate(xpos * this.scaleRatio, ypos * this.scaleRatio)
          // å¦‚果时圆形则截取圆形
          if (this.isRound) {
            const r = this.width > this.height ? Math.floor(this.height / 2) : Math.floor(this.width / 2)
            let translateX = Math.floor(this.width / 2)
            let translateY = Math.floor(this.height / 2)
            this.ctx.beginPath()
            this.ctx.arc(translateX - (xpos * this.scaleRatio), translateY - (ypos * this.scaleRatio), r, 0, (360 * Math.PI) / 180)
            this.ctx.closePath()
            this.ctx.stroke()
            this.ctx.clip()
          }
          this.ctx.rotate((this.angle * Math.PI) / 180)
          this.ctx.drawImage(imgUrl, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight)
          // æ¸…空后再继续绘制
          this.ctx.draw(false, () => {
            let params = {
              width: this.width * this.scaleRatio,
              height: Math.round(this.height * this.scaleRatio),
              destWidth: this.width * this.scaleRatio,
              destHeight: Math.round(this.height) * this.scaleRatio,
              fileType: 'png',
              quality: this.quality
            }
            let data = {
              url: '',
              base64: '',
              width: this.width * this.scaleRatio,
              height: this.height * this.scaleRatio
            }
            // #ifdef MP-ALIPAY
            if (this.returnBase64) {
              this.ctx.toDataURL(params).then((urlData) => {
                data.base64 = urlData
                this.loading && uni.hideLoading()
                this.$emit('cropper', data)
              })
            } else {
              this.ctx.toTempFilePath({
                ...params,
                success: (res) => {
                  data.url = res.apFilePath
                  this.loading && uni.hideLoading()
                  this.$emit('cropper', data)
                }
              })
            }
            // #endif
            let base64Flag = this.returnBase64
            // #ifndef MP-ALIPAY
            // #ifdef MP-BAIDU || MP-TOUTIAO || H5
            base64Flag = false
            // #endif
            if (base64Flag) {
              uni.canvasGetImageData({
                canvasId: this.CANVAS_ID,
                x: 0,
                y: 0,
                width: this.width * this.scaleRatio,
                height: Math.round(this.height * this.scaleRatio),
                success: (res) => {
                  const arrayBuffer = new Uint8Array(res.data)
                  const base64 = uni.arrayBufferToBase64(arrayBuffer)
                  data.base64 = base64
                  this.loading && uni.hideLoading()
                  this.$emit('cropper', data)
                }
              }, this)
            } else {
              uni.canvasToTempFilePath({
                ...params,
                canvasId: this.CANVAS_ID,
                success: (res) => {
                  data.url = res.tempFilePath
                  // #ifdef H5
                  data.base64 = res.tempFilePath
                  // #endif
                  this.loading && uni.hideLoading()
                  this.$emit('cropper', data)
                }
              }, this)
            }
            // #endif
          })
        }
        draw()
      },
      // ä¿®æ”¹å›¾ç‰‡åŽè§¦å‘的函数
      change(e) {
        this.cutX = e.cutX || 0
        this.cutY = e.cutY || 0
        this.imgWidth = e.imgWidth || this.imgWidth
        this.imgHeight = e.imgHeight || this.imgHeight
        this.scale = e.scale || 1
        this.angle = e.angle || 0
        this.imgTop = e.imgTop || 0
        this.imgLeft = e.imgLeft || 0
      },
      // é‡ç½®å›¾ç‰‡
      imageReset() {
        this.scale = 1
        this.angle = 0
        let systemInfo = this.systemInfo.windowHeight ? this.systemInfo : uni.getSystemInfoSync()
        this.imgTop = systemInfo.windowHeight / 2
        this.imgLeft = systemInfo.windowWidth / 2
        this.resetChange++
        this.props = `4,${this.resetChange}`
        // åˆå§‹æ—‹è½¬è§’度
        this.$emit('initAngle', {})
      },
      // å›¾ç‰‡çš„生成的尺寸
      imgComputeSize(width, height) {
        // é»˜è®¤æŒ‰å›¾ç‰‡çš„æœ€å°è¾¹ = å¯¹åº”的裁剪框尺寸
        let imgWidth = width,
          imgHeight = height;
        if (imgWidth && imgHeight) {
          if (imgWidth / imgHeight > this.width / this.height) {
            imgHeight = this.height
            imgWidth = (width / height) * imgHeight
          } else {
            imgWidth = this.width
            imgHeight = (height / width) * imgWidth
          }
        } else {
          let systemInfo = this.systemInfo.windowHeight ? this.systemInfo : uni.getSystemInfoSync()
          imgWidth = systemInfo.windowWidth
          imgHeight = 0
        }
        this.imgWidth = imgWidth
        this.imgHeight = imgHeight
        this.sizeChange++
        this.props = `2,${this.sizeChange}`
      },
      // å›¾ç‰‡åŠ è½½å®Œæ¯•
      imageLoad(e) {
        this.imageReset()
        uni.hideLoading()
        this.$emit('imageLoad', {})
      },
      // ç§»åŠ¨ç»“æŸ
      moveStop() {
        clearTimeout(this.TIME_CUT_CENTER)
        this.TIME_CUT_CENTER = setTimeout(() => {
          this.centerChange++
          this.props = `5,${this.centerChange}`
        }, 688)
      },
      // ç§»åЍ䏭
      moveDuring() {
        clearTimeout(this.TIME_CUT_CENTER)
      },
      // æ˜¾ç¤ºåŠ è½½æ¡†
      showLoading() {
        uni.showLoading({
          title: '请稍等......',
          mask: true
        })
      },
      // åœæ­¢
      stop() {},
      // å–消/返回
      back() {
        uni.navigateBack()
      },
      // è§’度改变
      angleChanged(val) {
        this.moveStop()
        if (val % 90) {
          this.angle = Math.round(val / 90) * 90
        }
        this.angleChange++
        this.props = `3,${this.angleChange}`
      },
      // è®¾ç½®è§’度
      setAngle() {
        this.animation = true
        this.angle = this.angle + 90
        this.angleChanged(this.angle)
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-cropper {
    width: 100vw;
    height: 100vh;
    background: linear-gradient(-120deg, #F15BB5, #9A5CE5, #01BEFF, #00F5D4);
    // background: linear-gradient(-120deg,  #9A5CE5, #01BEFF, #00F5D4, #43e97b);
    // background: linear-gradient(-120deg,#c471f5, #ec008c, #ff4e50,#f9d423);
    // background: linear-gradient(-120deg, #0976ea, #c471f5, #f956b6, #ea7e0a);
    position: fixed;
    top: 0;
    left: 0;
    z-index: 1;
    &__image {
      width: 100%;
      border-style: none;
      position: absolute;
      top: 0;
      left: 0;
      z-index: 2;
      -webkit-backface-visibility: hidden;
      backface-visibility: hidden;
      transform-origin: center;
    }
    &__canvas {
      position: fixed;
      z-index: 10;
      left: -2000px;
      top: -2000px;
      pointer-events: none;
    }
    &__wrapper {
      position: fixed;
      z-index: 4;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      border: 3000px solid rgba(0, 0, 0, 0.55);
      pointer-events: none;
      box-sizing: content-box;
    }
    &__border {
      position: absolute;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      box-sizing: border-box;
      pointer-events: none;
    }
    &__tabbar {
      width: 100%;
      height: 120rpx;
      padding: 0 40rpx;
      box-sizing: border-box;
      position: fixed;
      left: 0;
      bottom: 0;
      z-index: 99;
      display: flex;
      align-items: center;
      justify-content: space-between;
      color: #FFFFFF;
      font-size: 32rpx;
      &::after {
        content: '';
        position: absolute;
        top: 0;
        right: 0;
        left: 0;
        border-top: 1rpx solid rgba(255, 255, 255, 0.2);
        -webkit-transform: scaleY(0.5) translateZ(0);
        transform: scaleY(0.5) translateZ(0);
        transform-origin: 0 100%;
      }
      &__btn {
        height: 80rpx;
        display: flex;
        align-items: center;
      }
      &__rotate {
        width: 44rpx;
        height: 44rpx;
        font-size: 40rpx;
        text-align: center;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-custom-swiper-item/index.wxs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,288 @@
function setTimeout(instance, cb, time) {
  if (time > 0) {
    var s = getDate().getTime()
    var fn = function () {
        if (getDate().getTime() - s > time) {
            cb && cb()
        } else
            instance.requestAnimationFrame(fn)
    }
    fn()
  }
  else
    cb && cb()
}
// åˆ¤æ–­è§¦æ‘¸çš„移动方向
function decideSwiperDirection(startTouches, currentTouches, vertical) {
  // éœ‡åŠ¨åç§»å®¹å·®
  var toleranceShake = 150
  // ç§»åЍ容差
  var toleranceTranslate = 10
  if (!vertical) {
    // æ°´å¹³æ–¹å‘移动
    if (Math.abs(currentTouches.y - startTouches.y) <= toleranceShake) {
      // console.log(currentTouches.x, startTouches.x);
      if (Math.abs(currentTouches.x - startTouches.x) > toleranceTranslate) {
        if (currentTouches.x - startTouches.x > 0) {
          return 'right'
        } else if (currentTouches.x - startTouches.x < 0) {
          return 'left'
        }
      }
    }
  } else {
    // åž‚直方向移动
    if (Math.abs(currentTouches.x - startTouches.x) <= toleranceShake) {
      // console.log(currentTouches.x, startTouches.x);
      if (Math.abs(currentTouches.y - startTouches.y) > toleranceTranslate) {
        if (currentTouches.y - startTouches.y > 0) {
          return 'down'
        } else if (currentTouches.y - startTouches.y < 0) {
          return 'up'
        }
      }
    }
  }
  return ''
}
// swiperItem参数数据更新
var itemDataObserver = function(newVal, oldVal, ownerInstance, instance) {
  if (!newVal || newVal === 'undefined') return
  var state = ownerInstance.getState()
  state.itemData = newVal
}
// swiperIndex数据更新
var currentIndexObserver = function(newVal, oldVal, ownerInstance, instance) {
  if ((!newVal && newVal != 0) || newVal === 'undefined') return
  var state = ownerInstance.getState()
  state.currentIndex = newVal
}
// containerData数据更新
var containerDataObserver = function(newVal, oldVal, ownerInstance, instance) {
  if (!newVal || newVal === 'undefined') return
  var state = ownerInstance.getState()
  state.containerData = newVal
}
// å¼€å§‹è§¦æ‘¸
var touchStart = function(event, ownerInstance) {
  console.log('touchStart');
  var instance = event.instance
  var dataset = instance.getDataset()
  var state = ownerInstance.getState()
  var itemData = state.itemData
  var containerData = state.containerData
  // ç”±äºŽå½“前SwiperIndex初始为0,可能会导致swiperIndex数据没有更新
  if (!state.currentIndex || state.currentIndex === 'undefined') {
    state.currentIndex = 0
  }
  if (!containerData || containerData.circular === 'undefined') {
    containerData.circular = false
  }
  state.containerData = containerData
  // å¦‚果当前切换动画还没执行结束,再次触摸会重新加载对应的swiperContainer的信息
  // console.log(containerData.animationFinish);
  if (!containerData.animationFinish) {
    ownerInstance.callMethod('changeParentSwiperContainerStyleStatus',{
      status: 'reload'
    })
  }
  // åˆ¤æ–­æ˜¯å¦ä¸ºä¸ºå½“前显示的SwiperItem
  if (itemData.index != state.currentIndex) return
  var touches = event.changedTouches[0]
  if (!touches) return
  // æ ‡è®°æ»‘动开始时间
  state.touchStartTime = getDate().getTime()
  // è®°å½•当前滑动开始的x,y坐标
  state.touchRelactive = {
    x: touches.pageX,
    y: touches.pageY
  }
  // è®°å½•触摸id,用于处理多指的情况
  state.touchId = touches.identifier
  // æ ‡è®°å¼€å§‹è§¦æ‘¸
  state.touching = true
  ownerInstance.callMethod('updateTouchingStatus', {
    status: true
  })
}
// æ­£åœ¨ç§»åЍ
var touchMove = function(event, ownerInstance) {
  console.log('touchMove');
  var instance = event.instance
  var dataset = instance.getDataset()
  var state = ownerInstance.getState()
  var itemData = state.itemData
  var containerData = state.containerData
  // åˆ¤æ–­æ˜¯å¦ä¸ºä¸ºå½“前显示的SwiperItem
  if (itemData.index != state.currentIndex) return
  // åˆ¤æ–­æ˜¯å¦å¼€å§‹è§¦æ‘¸
  if (!state.touching) return
  var touches = event.changedTouches[0]
  if (!touches) return
  // åˆ¤æ–­æ˜¯å¦ä¸ºåŒä¸€ä¸ªè§¦æ‘¸ç‚¹
  if (state.touchId != touches.identifier) return
  var currentTouchRelactive = {
    x: touches.pageX,
    y: touches.pageY
  }
  // è®¡ç®—相对位移比例
  if (containerData.vertical) {
    var touchDistance = currentTouchRelactive.y - state.touchRelactive.y
    var itemHeight = itemData.itemHeight
    var distanceRate = touchDistance / itemHeight
    // console.log(currentTouchRelactive.y, touchDistance, itemHeight, distanceRate);
    // åˆ¤æ–­æ˜¯å¦ä¸ºè¡”接轮播,如果不是衔接轮播,如果当前为第一个swiperItem并且向下滑、当前为最后一个swiperItem并且向上滑时不进行操作
    if (!containerData.circular &&
      ((state.currentIndex === 0 && touchDistance > 0) || (state.currentIndex === containerData.swiperItemLength - 1 && touchDistance < 0))
    ) {
      return
    }
    // å¦‚果超出了距离则不进行操作
    if((Math.abs(touchDistance) > (itemData.itemTop + itemData.itemHeight))) {
      ownerInstance.callMethod('updateParentSwiperContainerStyle', {
        value: distanceRate < 0 ? -1 : 1
      })
      return
    }
  } else {
    var touchDistance = currentTouchRelactive.x - state.touchRelactive.x
    var itemWidth = itemData.itemWidth
    var distanceRate = touchDistance / itemWidth
    // console.log(currentTouchRelactive.x, touchDistance, itemWidth, distanceRate);
    // åˆ¤æ–­æ˜¯å¦ä¸ºè¡”接轮播,如果不是衔接轮播,如果当前为第一个swiperItem并且向右滑、当前为最后一个swiperItem并且向左滑时不进行操作
    if (!containerData.circular &&
      ((state.currentIndex === 0 && touchDistance > 0) || (state.currentIndex === containerData.swiperItemLength - 1 && touchDistance < 0))
    ) {
      return
    }
    // å¦‚果超出了距离则不进行操作
    if((Math.abs(touchDistance) > (itemData.itemLeft + itemData.itemWidth))) {
      ownerInstance.callMethod('updateParentSwiperContainerStyle', {
        value: distanceRate < 0 ? -1 : 1
      })
      return
    }
  }
  ownerInstance.callMethod('updateParentSwiperContainerStyle', {
    value: distanceRate
  })
}
// ç§»åŠ¨ç»“æŸ
var touchEnd = function(event, ownerInstance) {
  console.log('touchEnd');
  var instance = event.instance
  var dataset = instance.getDataset()
  var state = ownerInstance.getState()
  var itemData = state.itemData
  var containerData = state.containerData
  // åˆ¤æ–­æ˜¯å¦ä¸ºä¸ºå½“前显示的SwiperItem
  if (itemData.index != state.currentIndex) return
  // åˆ¤æ–­æ˜¯å¦å¼€å§‹è§¦æ‘¸
  if (!state.touching) return
  var touches = event.changedTouches[0]
  if (!touches) return
  // åˆ¤æ–­æ˜¯å¦ä¸ºåŒä¸€ä¸ªè§¦æ‘¸ç‚¹
  if (state.touchId != touches.identifier) return
  var currentTime = getDate().getTime()
  var currentTouchRelactive = {
    x: touches.pageX,
    y: touches.pageY
  }
  if (containerData.vertical) {
    // åˆ¤æ–­è§¦æ‘¸ç§»åŠ¨æ–¹å‘
    var direction = decideSwiperDirection(state.touchRelactive, currentTouchRelactive, true)
    // åˆ¤æ–­æ˜¯å¦ä¸ºè¡”接轮播,如果不是衔接轮播,如果当前为第一个swiperItem并且向下滑、当前为最后一个swiperItem并且向上滑时不进行操作
    if (containerData.circular ||
      !((state.currentIndex === 0 && direction === 'down') || (state.currentIndex === containerData.swiperItemLength - 1 && direction === 'up'))
    ) {
      // åˆ¤æ–­è§¦æ‘¸çš„æ—¶é—´å’Œç§»åŠ¨çš„è·ç¦»æ˜¯å¦è¶…è¿‡äº†å½“å‰itemHeight的一半,如果是则执行切换操作
      // console.log(currentTime - state.touchStartTime, Math.abs(currentTouchRelactive.y - state.touchRelactive.y));
      if ((currentTime - state.touchStartTime) > 200 && Math.abs(currentTouchRelactive.y - state.touchRelactive.y) < itemData.itemHeight / 2) {
        ownerInstance.callMethod('changeParentSwiperContainerStyleStatus',{
          status: 'reset'
        })
      } else {
        // console.log(direction, state.touchRelactive.y, currentTouchRelactive.y);
        ownerInstance.callMethod('updateParentSwiperContainerStyleWithDirection', {
          direction: direction
        })
      }
    }
  } else {
    // åˆ¤æ–­è§¦æ‘¸ç§»åŠ¨æ–¹å‘
    var direction = decideSwiperDirection(state.touchRelactive, currentTouchRelactive, false)
    // åˆ¤æ–­æ˜¯å¦ä¸ºè¡”接轮播,如果不是衔接轮播,如果当前为第一个swiperItem并且向右滑、当前为最后一个swiperItem并且向左滑时不进行操作
    if (containerData.circular ||
      !((state.currentIndex === 0 && direction === 'right') || (state.currentIndex === containerData.swiperItemLength - 1 && direction === 'left'))
    ) {
      // åˆ¤æ–­è§¦æ‘¸çš„æ—¶é—´å’Œç§»åŠ¨çš„è·ç¦»æ˜¯å¦è¶…è¿‡äº†å½“å‰itemWidth的一半,如果是则执行切换操作
      // console.log(currentTime - state.touchStartTime, Math.abs(currentTouchRelactive.x - state.touchRelactive.x));
      if ((currentTime - state.touchStartTime) > 200 && Math.abs(currentTouchRelactive.x - state.touchRelactive.x) < itemData.itemWidth / 2) {
        ownerInstance.callMethod('changeParentSwiperContainerStyleStatus',{
          status: 'reset'
        })
      } else {
        // console.log(direction, state.touchRelactive.x, currentTouchRelactive.x);
        ownerInstance.callMethod('updateParentSwiperContainerStyleWithDirection', {
          direction: direction
        })
      }
    }
  }
  // æ¸…除标记
  state.touchId = null
  state.touchRelactive = null
  state.touchStartTime = 0
  // æ ‡è®°åœæ­¢è§¦æ‘¸
  state.touching = true
  ownerInstance.callMethod('updateTouchingStatus', {
    status: false
  })
}
module.exports = {
  itemDataObserver: itemDataObserver,
  currentIndexObserver: currentIndexObserver,
  containerDataObserver: containerDataObserver,
  touchStart: touchStart,
  touchMove: touchMove,
  touchEnd: touchEnd
}
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-custom-swiper-item/tn-custom-swiper-item.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,277 @@
<template>
  <!-- #ifdef MP-WEIXIN -->
  <view
    class="tn-c-swiper-item"
    :style="[swiperStyle]"
    :itemData="itemData"
    :currentIndex="currentIndex"
    :containerData="containerData"
    :change:itemData="wxs.itemDataObserver"
    :change:currentIndex="wxs.currentIndexObserver"
    :change:containerData="wxs.containerDataObserver"
    @touchstart="wxs.touchStart"
    :catch:touchmove="touching?wxs.touchMove:''"
    :catch:touchend="touching?wxs.touchEnd:''"
  >
    <view class="item__container tn-c-swiper-item__container" :style="[containerStyle]">
      <slot></slot>
    </view>
  </view>
  <!-- #endif -->
  <!-- #ifndef MP-WEIXIN -->
  <view
    class="tn-c-swiper-item"
    :style="[swiperStyle]"
    :itemData="itemData"
    :currentIndex="currentIndex"
    :containerData="containerData"
    :change:itemData="wxs.itemDataObserver"
    :change:currentIndex="wxs.currentIndexObserver"
    :change:containerData="wxs.containerDataObserver"
    @touchstart="wxs.touchStart"
    @touchmove="wxs.touchMove"
    @touchend="wxs.touchEnd"
  >
    <view class="item__container tn-c-swiper-item__container" :style="[containerStyle]">
      <slot></slot>
    </view>
  </view>
  <!-- #endif -->
</template>
<script src="./index.wxs" lang="wxs" module="wxs"></script>
<script>
  export default {
    name: 'tn-custom-swiper-item',
    props: {
    },
    computed: {
      // swiperItem公共数据
      itemData() {
        return {
          index: this.index,
          itemWidth: this.itemWidth,
          itemHeight: this.itemHeight,
          itemTop: this.itemTop,
          itemLeft: this.itemLeft
        }
      },
      currentIndex() {
        return this.parentData.currentIndex
      },
      containerData() {
        return {
          duration: this.parentData.duration,
          animationFinish: this.parentData.swiperContainerAnimationFinish,
          circular: this.parentData.circular,
          swiperItemLength: this.swiperItemLength,
          vertical: this.parentData.vertical
        }
      },
      swiperStyle() {
        let style = {}
        style.transform = `translate3d(${this.translateX}%, ${this.translateY}%, 0px)`
        return style
      },
      containerStyle() {
        let style = {}
        if (this.parentData.customSwiperStyle && Object.keys(this.parentData.customSwiperStyle).length > 0) {
          style = this.parentData.customSwiperStyle
        }
        if ((this.currentIndex === 0 && this.index === this.swiperItemLength - 1) || (this.index === this.currentIndex - 1) &&
          (this.parentData.prevSwiperStyle && Object.keys(this.parentData.prevSwiperStyle).length > 0)
        ) {
          // å‰ä¸€ä¸ªswiperItem
          const copyStyle = JSON.parse(JSON.stringify(style))
          style = Object.assign(copyStyle, this.parentData.prevSwiperStyle)
        }
        if ((this.currentIndex === this.swiperItemLength - 1 && this.index === 0) || (this.index === this.currentIndex + 1) &&
          (this.parentData.nextSwiperStyle && Object.keys(this.parentData.nextSwiperStyle).length > 0)
        ) {
          // åŽä¸€ä¸ªswiperItem
          const copyStyle = JSON.parse(JSON.stringify(style))
          style = Object.assign(copyStyle, this.parentData.nextSwiperStyle)
        }
        return style
      }
    },
    data() {
      return {
        // çˆ¶ç»„件参数
        parentData: {
          duration: 500,
          currentIndex: 0,
          swiperContainerAnimationFinish: false,
          circular: false,
          vertical: false,
          prevSwiperStyle: {},
          customSwiperStyle: {},
          nextSwiperStyle: {}
        },
        // æ ‡è®°å½“前是否正在触摸
        touching: true,
        // å½“前swiperItem的偏移位置
        translateX: 0,
        translateY: 0,
        // å½“前swiperItem的宽高
        itemWidth: 0,
        itemHeight: 0,
        // å½“前swiperItem的位置信息
        itemTop: 0,
        itemLeft: 0,
        // å½“前swiperItem的状态 prev current next
        status: 'current',
        // å½“前swiperItem的index序号
        index: 0,
        // swiperItem的的数量
        swiperItemLength: 0
      }
    },
    created() {
      this.parent = false
      this.updateParentData()
      // èŽ·å–å½“å‰çˆ¶ç»„ä»¶children的数量作为当前swiperItem的序号
      this.index = this.parent.children.length
      this.parent && this.parent.children.push(this)
    },
    mounted() {
      this.$nextTick(() => {
        this.initSwiperItem()
      })
    },
    methods: {
      // åˆå§‹åŒ–swiperItem
      initSwiperItem() {
        this.getSwiperItemRect(() => {
          this.parent.updateAllSwiperItemStyle()
          this.parentData.swiperContainerAnimationFinish = true
        })
      },
      // èŽ·å–swiperItem的信息
      async getSwiperItemRect(callback) {
        const swiperItemRes = await this._tGetRect('.tn-c-swiper-item')
        if (!swiperItemRes.height || !swiperItemRes.width) {
          setTimeout(() => {
            this.getSwiperItemRect()
          }, 30)
          return
        }
        this.itemWidth = swiperItemRes.width
        this.itemHeight = swiperItemRes.height
        this.itemTop = swiperItemRes.top
        this.itemLeft = swiperItemRes.left
        callback && callback()
      },
      // æ›´æ–°swiperItem样式
      updateSwiperItemStyle(swiperItemLength, currentIndex = undefined) {
        currentIndex = currentIndex != undefined ? currentIndex : this.parentData.currentIndex
        this.swiperItemLength = swiperItemLength
        // æ ¹æ®å½“前swiperItem的序号设置偏移位置
        // åˆ¤æ–­å½“前swiperItem是否为第一个,如果是则将最后的swiperItem移动到当前的前一个位置(即最前面)
        if (currentIndex === 0 && this.index === swiperItemLength - 1) {
          if (this.parentData.vertical) {
            this.translateX = 0
            this.translateY = -100
          } else {
            this.translateX = -100
            this.translateY = 0
          }
        }
        // åˆ¤æ–­å½“前swiperItem是否为最后一个,如果是则将最前的swiperItem移动到当前的后一个位置(即最后面)
        else if (currentIndex === swiperItemLength - 1 && this.index === 0) {
          if (this.parentData.vertical) {
            this.translateX = 0
            this.translateY = swiperItemLength * 100
          } else {
            this.translateX = swiperItemLength * 100
            this.translateY = 0
          }
        }
        // æ­£å¸¸æƒ…况
        else {
          if (this.parentData.vertical) {
            this.translateX = 0
            this.translateY = this.index * 100
          } else {
            this.translateX = this.index * 100
            this.translateY = 0
          }
        }
      },
      // æ›´æ–°çˆ¶ç»„件的偏移位置信息
      updateParentSwiperContainerStyle(e) {
        this.parent.updateSwiperContainerStyleWithValue(e.value)
      },
      // æ ¹æ®æ–¹å‘更新父组件的偏移位置信息
      updateParentSwiperContainerStyleWithDirection(e) {
        this.parent.updateSwiperContainerStyleWithDirection(e.direction)
      },
      // ä¿®æ”¹çˆ¶ç»„件的偏移位置的状态
      changeParentSwiperContainerStyleStatus(e) {
        // reset -> é‡ç½® reload -> é‡è½½
        this.parent.updateSwiperContainerStyleWithDirection(e.status)
      },
      // æ›´æ–°çˆ¶ç»„件信息
      updateParentData() {
        this.getParentData('tn-custom-swiper')
      },
      // æ›´æ–°è§¦æ‘¸çŠ¶æ€
      updateTouchingStatus(e) {
        this.touching = e.status
        if (e.status) {
          this.parent.stopAutoPlay()
        } else {
          this.parent.startAutoPlay()
        }
      },
      // æå–对应用户自定义样式
      extractCustomStyle(customStyle) {
        let data = {
          transform: {},
          style: {}
        }
        if (!customStyle) return data
        // å…è®¸è®¾ç½®çš„transform参数
        const allowTransformProps = ['scale','scaleX','scaleY','scaleZ','rotate','rotateX','rotateY','rotateZ']
        for (let prop in customStyle) {
          if (prop.startsWith('transformProp')) {
            // transform里面的样式
            let transformProp = prop.substring('transformProp'.length)
            const index = allowTransformProps.findIndex((item) => {
              return item.toLowerCase() === transformProp.toLowerCase()
            })
            if (index !== -1) {
              transformProp = allowTransformProps[index]
              data.transform[transformProp] = customStyle[prop]
            }
          } else {
            // æ™®é€šæ ·å¼
            data.style[prop] = customStyle[prop]
          }
        }
        return data
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-c-swiper-item {
    width: 100%;
    height: 100%;
    position: absolute;
    display: block;
    will-change: transform;
    cursor: none;
    transform: translate3d(0px, 0px, 0px);
    .item__container {
      width: 100%;
      height: 100%;
      display: block;
      position: absolute;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-custom-swiper/tn-custom-swiper.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,535 @@
<template>
  <view
    class="tn-c-swiper-class tn-c-swiper"
  >
    <!-- è½®æ’­item容器-->
    <view class="tn-swiper__container" :style="[swiperContainerStyle]" :animation="containerAnimation">
      <slot></slot>
    </view>
    <!-- è½®æ’­æŒ‡ç¤ºå™¨-->
    <view v-if="indicator" class="tn-swiper__indicator" :class="[`tn-swiper__indicator--${vertical ? 'vertical' : 'horizontal'}`]" :style="[indicatorStyle]">
      <!-- æ–¹å½¢ -->
      <block v-if="indicatorType === 'rect'">
        <view
          v-for="(item, index) in children.length"
          :key="index"
          class="tn-swiper__indicator__rect"
          :class="[
            `tn-swiper__indicator__rect--${vertical ? 'vertical' : 'horizontal'}`,
            currentIndex === index ? `tn-swiper__indicator__rect--active tn-swiper__indicator__rect--active--${vertical ? 'vertical' : 'horizontal'}` : ''
          ]"
          :style="[indicatorPointStyle(index)]"
        ></view>
      </block>
      <!-- ç‚¹ -->
      <block v-if="indicatorType === 'dot'">
        <view
          v-for="(item, index) in children.length"
          :key="index"
          class="tn-swiper__indicator__dot"
          :class="[
            `tn-swiper__indicator__dot--${vertical ? 'vertical' : 'horizontal'}`,
            currentIndex === index ? `tn-swiper__indicator__dot--active tn-swiper__indicator__dot--active--${vertical ? 'vertical' : 'horizontal'}` : ''
          ]"
          :style="[indicatorPointStyle(index)]"
        ></view>
      </block>
      <!-- åœ†è§’方形 -->
      <block v-if="indicatorType === 'round'">
        <view
          v-for="(item, index) in children.length"
          :key="index"
          class="tn-swiper__indicator__round"
          :class="[
            `tn-swiper__indicator__round--${vertical ? 'vertical' : 'horizontal'}`,
            currentIndex === index ? `tn-swiper__indicator__round--active tn-swiper__indicator__round--active--${vertical ? 'vertical' : 'horizontal'}` : ''
          ]"
          :style="[indicatorPointStyle(index)]"
        ></view>
      </block>
      <!-- åºå· -->
      <block v-if="indicatorType === 'number' && !vertical">
        <view class="tn-swiper__indicator__number">{{ currentIndex + 1 }}/{{ children.length }}</view>
      </block>
    </view>
  </view>
</template>
<script>
  export default {
    name: 'tn-custom-swiper',
    props: {
      // å½“前所在的轮播位置
      current: {
        type: Number,
        default: 0
      },
      // è‡ªåŠ¨åˆ‡æ¢
      autoplay: {
        type: Boolean,
        default: false
      },
      // è‡ªåŠ¨åˆ‡æ¢æ—¶é—´é—´éš”
      interval: {
        type: Number,
        default: 5000
      },
      // æ»‘动动画时长
      duration: {
        type: Number,
        default: 500
      },
      // æ˜¯å¦é‡‡ç”¨è¡”接滑动
      circular: {
        type: Boolean,
        default: false
      },
      // æ»‘动方向为纵向
      vertical: {
        type: Boolean,
        default: false
      },
      // æ˜¾ç¤ºæŒ‡ç¤ºç‚¹
      indicator: {
        type: Boolean,
        default: false
      },
      // æŒ‡ç¤ºç‚¹ç±»åž‹
      // rect -> æ–¹å½¢ round -> åœ†è§’方形 dot -> ç‚¹ number -> è½®æ’­å›¾ä¸‹æ ‡
      indicatorType: {
        type: String,
        default: 'dot'
      },
      // æŒ‡ç¤ºç‚¹çš„位置
      // topLeft \ topCenter \ topRight \ bottomLeft \ bottomCenter \ bottomRight
      indicatorPosition: {
        type: String,
        default: 'bottomCenter'
      },
      // æŒ‡ç¤ºç‚¹æ¿€æ´»æ—¶é¢œè‰²
      indicatorActiveColor: {
        type: String,
        default: ''
      },
      // æŒ‡ç¤ºç‚¹æœªæ¿€æ´»æ—¶é¢œè‰²
      indicatorInactiveColor: {
        type: String,
        default: ''
      },
      // å‰ä¸€ä¸ªè½®æ’­çš„自定义样式
      prevSwiperStyle: {
        type: Object,
        default() {
          return {}
        }
      },
      // å½“前轮播的自定义样式
      customSwiperStyle: {
        type: Object,
        default() {
          return {}
        }
      },
      // åŽä¸€ä¸ªè½®æ’­çš„自定义样式
      nextSwiperStyle: {
        type: Object,
        default() {
          return {}
        }
      }
    },
    computed: {
      parentData() {
        return [
          this.duration,
          this.currentIndex,
          this.swiperContainerAnimationFinish,
          this.circular,
          this.vertical,
          this.prevSwiperStyle,
          this.customSwiperStyle,
          this.nextSwiperStyle
        ]
      },
      indicatorStyle() {
        let style = {}
        if (this.vertical) {
          if (this.indicatorPosition === 'topLeft' || this.indicatorPosition === 'bottomLeft') style.justifyContent = 'flex-start'
          if (this.indicatorPosition === 'topCenter' || this.indicatorPosition === 'bottomCenter') style.justifyContent =  'center'
          if (this.indicatorPosition === 'topRight' || this.indicatorPosition === 'bottomRight') style.justifyContent =  'flex-end'
          if (['topLeft','topCenter','topRight'].indexOf(this.indicatorPosition) >= 0) {
            if (this.vertical) {
              style.right = '12rpx'
              style.left = 'auto'
            } else {
              style.top = '12rpx'
              style.bottom = 'auto'
            }
          } else {
            if (this.vertical) {
              style.right = 'auto'
              style.left = '12rpx'
            } else {
              style.top = 'auto'
              style.bottom = '12rpx'
            }
          }
        } else {
          if (this.indicatorPosition === 'topLeft' || this.indicatorPosition === 'bottomLeft') style.justifyContent = 'flex-start'
          if (this.indicatorPosition === 'topCenter' || this.indicatorPosition === 'bottomCenter') style.justifyContent =  'center'
          if (this.indicatorPosition === 'topRight' || this.indicatorPosition === 'bottomRight') style.justifyContent =  'flex-end'
          if (['topLeft','topCenter','topRight'].indexOf(this.indicatorPosition) >= 0) {
            style.top = '12rpx'
            style.bottom = 'auto'
          } else {
            style.top = 'auto'
            style.bottom = '12rpx'
          }
        }
        return style
      },
      indicatorPointStyle() {
        return (index) => {
          let style = {}
          if (index === this.currentIndex && this.indicatorActiveColor !== '') {
            style.backgroundColor = this.indicatorActiveColor
          } else if (this.indicatorInactiveColor !== '') {
            style.backgroundColor = this.indicatorInactiveColor
          }
          return style
        }
      }
    },
    watch: {
      parentData() {
        if (this.children.length) {
          this.children.forEach((item) => {
            // åˆ¤æ–­å­ç»„件如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
            typeof(item.updateParentData) === 'function' && item.updateParentData()
          })
        }
      },
      current(nVal, oVal) {
        if (this.currentIndex === nVal) return
        this.currentIndex = nVal > this.children.length ? this.children.length - 1 : nVal
        this.swiperContainerAnimationFinish = false
        // è®¾ç½®åŠ¨ç”»è¿‡æ¸¡æ—¶é—´
        this.swiperContainerStyle.transitionDuration = `${this.duration + 90}ms`
        this.updateSwiperContainerItem(oVal)
      }
    },
    data() {
      return {
        // æ¸…除动画定时器
        clearAnimationTimer: null,
        // å‰åŽè¡”接执行定时器
        convergeTimer: null,
        // è‡ªåŠ¨è½®æ’­Timer
        autoPlayTimer: null,
        // å½“前选中的轮播
        currentIndex: this.current,
        // swiperContainer样式
        swiperContainerStyle: {
          transform: 'translate3d(0px, 0px, 0px)',
          transitionDuration: '0ms'
        },
        // swiperContainer动画
        containerAnimation: {},
        // æ»‘动动画结束标记
        swiperContainerAnimationFinish: false
      }
    },
    created() {
      this.children = []
    },
    mounted() {
      this.$nextTick(() => {
        const index = this.currentIndex > this.children.length ? this.children.length - 1 : this.currentIndex
        this.updateSwiperContainerStyle(index)
        this.startAutoPlay()
      })
    },
    methods: {
      // æ›´æ–°å…¨éƒ¨swiperItem的样式
      updateAllSwiperItemStyle() {
        this.children.forEach((item, index) => {
          typeof(item.updateSwiperItemStyle) === 'function' && item.updateSwiperItemStyle(this.children.length)
        })
      },
      // æ ¹æ®swiperIndex更新swiperItemContainer的样式
      updateSwiperContainerStyle(index) {
        if (this.vertical) {
          this.swiperContainerStyle.transform = `translate3d(0px, ${-index * 100}%, 0px)`
        } else {
          this.swiperContainerStyle.transform = `translate3d(${-index * 100}%, 0px, 0px)`
        }
      },
      // æ ¹æ®ä¼ é€’的值更新swiperItemContainer的位置
      updateSwiperContainerStyleWithValue(value) {
        if (this.vertical) {
          this.swiperContainerStyle.transform = `translate3d(0px, ${(-this.currentIndex * 100) + value * 100}%, 0px)`
        } else {
          this.swiperContainerStyle.transform = `translate3d(${(-this.currentIndex * 100) + value * 100}%, 0px, 0px)`
        }
      },
      // æ ¹æ®ä¼ é€’的方向更新swiperItemContainer的位置
      updateSwiperContainerStyleWithDirection(direction) {
        const oldCurrent = this.currentIndex
        const childrenLength = this.children.length
        const lastSwiperItemIndex = childrenLength - 1
        this.swiperContainerAnimationFinish = false
        // å‘后切换一个SwiperItem
        if (direction === 'reset') {
          // è®¾ç½®åŠ¨ç”»è¿‡æ¸¡æ—¶é—´
          this.swiperContainerStyle.transitionDuration = `${this.duration}ms`
          this.updateSwiperContainerStyle(this.currentIndex)
          this.clearAnimationTimer = setTimeout(() => {
            this.clearSwiperContainerAnimation()
          }, this.duration)
        } else if (direction === 'reload') {
          this.clearConvergeSwiperItemTimer()
          this.clearSwiperContainerAnimation()
          this.updateSwiperItemStyle(0)
          this.updateSwiperItemStyle(lastSwiperItemIndex)
        } else {
          if (direction === 'left' || direction === 'up') {
            if (oldCurrent === childrenLength - 1 && !this.circular) {
              this.clearSwiperContainerAnimation()
              this.clearConvergeSwiperItemTimer()
              return
            }
            this.currentIndex = oldCurrent + 1 >= childrenLength ? 0 : oldCurrent + 1
          } else if (direction === 'right' || direction === 'down') {
            if (oldCurrent === 0 && !this.circular) {
              this.clearSwiperContainerAnimation()
              this.clearConvergeSwiperItemTimer()
              return
            }
            this.currentIndex = oldCurrent - 1 < 0 ? childrenLength - 1 : oldCurrent - 1
          }
          // è®¾ç½®åŠ¨ç”»è¿‡æ¸¡æ—¶é—´
          this.swiperContainerStyle.transitionDuration = `${this.duration + 90}ms`
          // this.updateSwiperItemContainerRect(this.currentIndex)
        }
        // console.log(direction, oldCurrent, this.currentIndex);
        this.updateSwiperContainerItem(oldCurrent)
        // åˆ‡æ¢è½®æ’­æ—¶è§¦å‘事件
        this.$emit('change', {
          current: this.currentIndex
        })
      },
      // è®¾ç½®è‡ªåŠ¨è½®æ’­
      startAutoPlay() {
        if (this.autoplay && !this.autoPlayTimer && this.circular) {
          this.autoPlayTimer = setInterval(() => {
            this.updateSwiperContainerStyleWithDirection('left')
          }, this.interval)
        }
      },
      // åœæ­¢è‡ªåŠ¨è½®æ’­
      stopAutoPlay() {
        if (this.autoPlayTimer) {
          clearInterval(this.autoPlayTimer)
          this.autoPlayTimer = null
        }
      },
      // æ›´æ–°swiperContainer和swiperItem相关联信息
      updateSwiperContainerItem(oldCurrent) {
        const childrenLength = this.children.length
        const lastSwiperItemIndex = childrenLength - 1
        // åˆ¤æ–­å½“前是否为头尾,如果是更新对应的头尾SwiperItem样式
        // æ›´æ–°swiperItemContainer的样式
        if (oldCurrent === 0 && this.currentIndex === lastSwiperItemIndex) {
          // å…ˆç§»åŠ¨åˆ°æœ€å·¦è¾¹ç„¶åŽå†åŽ»é™¤åŠ¨ç”»åç§»åˆ°æ­£å¸¸çš„ä½ç½®
          // this.swiperContainerStyle.transform = `translate3d(100%, 0px, 0px)`
          this.updateSwiperContainerStyle(-1)
          this.clearSwiperContainerAnimationTimer()
          this.clearAnimationTimer = setTimeout(() => {
            this.convergeSwiperItem()
          }, this.duration)
        } else if (oldCurrent === lastSwiperItemIndex && this.currentIndex === 0) {
          // å…ˆç§»åŠ¨åˆ°æœ€å³è¾¹ç„¶åŽå†åŽ»é™¤åŠ¨ç”»åç§»åˆ°æ­£å¸¸çš„ä½ç½®
          // this.swiperContainerStyle.transform = `translate3d(${-childrenLength * 100}%, 0px, 0px)`
          this.updateSwiperContainerStyle(childrenLength)
          this.clearSwiperContainerAnimationTimer()
          this.clearAnimationTimer = setTimeout(() => {
            this.convergeSwiperItem()
          }, this.duration)
        } else {
          this.updateSwiperContainerStyle(this.currentIndex)
          this.updateSwiperItemStyle(0)
          this.updateSwiperItemStyle(lastSwiperItemIndex)
          this.clearAnimationTimer = setTimeout(() => {
            this.clearSwiperContainerAnimation()
          }, this.duration)
        }
      },
      // æ›´æ–°å¯¹åº”swiperItem的信息
      updateSwiperItemStyle(index) {
        const childrenLength = this.children.length
        if (index < 0) index = 0
        if (index > childrenLength - 1) index = childrenLength - 1
        typeof(this.children[index].updateSwiperItemStyle) === 'function' && this.children[index].updateSwiperItemStyle(childrenLength, this.currentIndex)
      },
      // æ›´æ–°å¯¹åº”swiperItem的容器信息
      updateSwiperItemContainerRect(index) {
        const childrenLength = this.children.length
        if (index < 0) index = 0
        if (index > childrenLength - 1) index = childrenLength - 1
        typeof(this.children[index].getSwiperItemRect) === 'function' && this.children[index].getSwiperItemRect()
      },
      // æ‰§è¡Œå‰åŽè¡”接
      convergeSwiperItem() {
        const lastSwiperItemIndex = this.children.length - 1
        this.clearSwiperContainerAnimation()
        this.clearConvergeSwiperItemTimer()
        this.convergeTimer = setTimeout(() => {
          this.updateSwiperItemStyle(0)
          this.updateSwiperItemStyle(lastSwiperItemIndex)
          this.updateSwiperContainerStyle(this.currentIndex)
          this.clearConvergeSwiperItemTimer()
        }, 30)
      },
      // åœæ­¢/清除切换动画
      clearSwiperContainerAnimation() {
        this.swiperContainerStyle.transitionDuration = `0ms`
        this.swiperContainerAnimationFinish = true
        this.clearSwiperContainerAnimationTimer()
      },
      // åœæ­¢/清除执行前后衔接定时器
      clearConvergeSwiperItemTimer() {
        if (this.convergeTimer) {
          clearTimeout(this.convergeTimer)
          this.convergeTimer = null
        }
      },
      // åœæ­¢/清除切换动画定时器
      clearSwiperContainerAnimationTimer() {
        if (this.clearAnimationTimer) {
          clearTimeout(this.clearAnimationTimer)
          this.clearAnimationTimer = null
        }
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-c-swiper {
    position: relative;
    overflow: hidden;
    width: 100%;
    height: 100%;
    .tn-swiper {
      &__container {
        width: 100%;
        height: 100%;
        position: absolute;
        top: 0;
        left: 0;
        will-change: transform;
        transition-property: all;
        transition-timing-function: ease-out;
      }
      &__indicator {
        position: absolute;
        display: flex;
        z-index: 1;
        &--horizontal {
          padding: 0 24rpx;
          flex-direction: row;
          width: 100%;
        }
        &--vertical {
          padding: 24rpx 0;
          flex-direction: column;
          height: 100%;
        }
        &__rect {
          background-color: rgba(0, 0, 0, 0.3);
          transition: all 0.5s;
          &--horizontal {
            width: 26rpx;
            height: 8rpx;
          }
          &--vertical {
            width: 8rpx;
            height: 26rpx;
          }
          &--active {
            background-color: rgba(255, 255, 255, 0.8);
          }
        }
        &__dot {
          width: 14rpx;
          height: 14rpx;
          border-radius: 20rpx;
          background-color: rgba(0, 0, 0, 0.3);
          transition: all 0.5s;
          &--horizontal {
            margin: 0 6rpx;
          }
          &--vertical {
            margin: 6rpx 0;
          }
          &--active {
            background-color: rgba(255, 255, 255, 0.8);
          }
        }
        &__round {
          width: 14rpx;
          height: 14rpx;
          border-radius: 20rpx;
          background-color: rgba(0, 0, 0, 0.3);
          transition: all 0.5s;
          &--horizontal {
            margin: 0 6rpx;
          }
          &--vertical {
            margin: 6rpx 0;
          }
          &--active {
            background-color: rgba(255, 255, 255, 0.8);
            &--horizontal {
              width: 34rpx;
            }
            &--vertical {
              height: 34rpx;
            }
          }
        }
        &__number {
          padding: 6rpx 16rpx;
          line-height: 1;
          background-color: rgba(0, 0, 0, 0.3);
          color: rgba(255, 255, 255, 0.8);
          border-radius: 100rpx;
          font-size: 26rpx;
        }
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-drag/index.wxs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,265 @@
// åˆ¤æ–­æ˜¯å¦å‡ºç•Œ
var isOutRange = function(x1, y1, x2, y2, x3, y3) {
  return x1 < 0 || x1 >= y1 || x2 < 0 || x2 >= y2 || x3 < 0 || x3 >= y3
}
var edit = false
function bool(str) {
  return str === 'true' || str === true
}
/**
 * æŽ’序核心
 * @param {Object} startKey å¼€å§‹æ—¶ä½ç½®
 * @param {Object} endKey ç»“束时位置
 * @param {Object} instance wxs内的局部变量快照
 */
var sortCore = function(startKey, endKey, state) {
  var basedata = state.basedata
  var excludeFix = function(sortKey, type) {
    // fixed å…ƒç´ ä½ç½®ä¸ä¼šå˜åŒ–, è¿™é‡Œç›´æŽ¥ç”¨ sortKey èŽ·å–ï¼Œæ›´åŠ ä¾¿æ·
    if (state.list[sortKey].fixed) {
      var _sortKey = type ? --sortKey : ++sortKey
      return excludeFix(sortKey, type)
    }
    return sortKey
  }
  // å…ˆèŽ·å–åˆ° endKey å¯¹åº”çš„ realKey, é˜²æ­¢ä¸‹é¢æŽ’序过程中该 realKey è¢«ä¿®æ”¹
  var endRealKey = -1
  state.list.forEach(function(item) {
    if (item.sortKey === endKey) endRealKey = item.realKey
  })
  return state.list.map(function(item) {
    if (item.fixed) return item
    var sortKey = item.sortKey
    var realKey = item.realKey
    if (startKey < endKey) {
      // æ­£åºæ‹–动
      if (sortKey > startKey && sortKey <= endKey) {
        --realKey
        sortKey =  excludeFix(--sortKey, true)
      } else if (sortKey === startKey) {
        realKey = endRealKey
        sortKey = endKey
      }
    } else if (startKey > endKey) {
      // å€’序拖动
      if (sortKey >= endKey && sortKey < startKey) {
        ++realKey
        sortKey = excludeFix(++sortKey, false)
      } else if (sortKey === startKey) {
        realKey = endRealKey
        sortKey = endKey
      }
    }
    if (item.sortKey != sortKey) {
      item.translateX = (sortKey % basedata.columns) * 100 + '%'
      item.translateY = Math.floor(sortKey / basedata.columns) * 100 + '%'
      item.sortKey = sortKey
      item.realKey = realKey
    }
    return item
  })
}
var triggerCustomEvent = function(list, type, instance) {
  if (!instance) return
  var _list = [],
    listData = [];
  list.forEach(function(item) {
    _list[item.sortKey] = item
  })
  _list.forEach(function(item) {
    listData.push(item.data)
  })
  // ç¼–译到小程序 funcName作为参数传递导致事件不执行
  switch(type) {
    case 'change':
      instance.callMethod('change', {data: listData})
      break
    case 'sortEnd':
      instance.callMethod('sortEnd', {data: listData})
      break
  }
}
var listObserver = function(newVal, oldVal, ownerInstance, instance) {
  var state = ownerInstance.getState()
  state.itemsInstance = ownerInstance.selectAllComponents('.tn-drag__item')
  state.list = newVal || []
  state.list.forEach(function(item, index) {
    var itemInstance = state.itemsInstance[index]
    if (item && itemInstance) {
      itemInstance.setStyle({
        'transform': 'translate3d('+ item.translateX + ',' + item.translateY +', 0)'
      })
      if (item.fixed) itemInstance.addClass('tn-drag__fixed')
    }
  })
}
var baseDataObserver = function(newVal, oldVal, ownerInstance, instance) {
  var state = ownerInstance.getState()
  state.basedata = newVal
}
var longPress = function(event, ownerInstance) {
  var instance = event.instance
  var dataset = instance.getDataset()
  var state = ownerInstance.getState()
  edit = bool(dataset.edit)
  if (!edit) return
  if (!state.basedata || state.basedata === 'undefined') {
    state.basedata = JSON.parse(dataset.basedata)
  }
  var basedata = state.basedata
  var touches = event.changedTouches[0]
  if (!touches) return
  state.current = +dataset.index
  // åˆå§‹é¡¹æ˜¯å›ºå®šé¡¹åˆ™è¿”回
  var item = state.list[state.current]
  if (item && item.fixed) return
  // å¦‚果已经在 drag ä¸­åˆ™è¿”回, é˜²æ­¢å¤šæŒ‡è§¦å‘ drag åŠ¨ä½œ, touchstart äº‹ä»¶ä¸­æœ‰æ•ˆæžœ
  if (state.dragging) return
  ownerInstance.callMethod("drag", {
    dragging: true
  })
  // è®¡ç®—X, Y轴初始位移,使item中心移动到点击处,单列的时候X轴初始不做位移
  state.translateX = basedata.columns === 1 ? 0 : touches.pageX - (basedata.itemWidth / 2 + basedata.left)
  state.translateY = touches.pageY - (basedata.itemHeight / 2 + basedata.top)
  state.touchId = touches.identifier
  instance.setStyle({
    'transform': 'translate3d(' + state.translateX + 'px,' + state.translateY +'px, 0)'
  })
  state.itemsInstance.forEach(function(item, index) {
    item.removeClass("tn-drag__transition").removeClass("tn-drag__current")
    item.addClass(index === state.current ? "tn-drag__current" : "tn-drag__transition")
  })
  ownerInstance.callMethod("vibrate")
  state.dragging = true
}
var touchStart = function(event, ownerInstance) {
  var instance = event.instance
  var dataset = instance.getDataset()
  edit = bool(dataset.edit)
}
var touchMove = function(event, ownerInstance) {
  var instance = event.instance
  var dataset = instance.getDataset()
  var state = ownerInstance.getState()
  var basedata = state.basedata
  if (!state.dragging || !edit) return
  var touches = event.changedTouches[0]
  if (!touches) return
  // å¦‚果不是同一个触发点则返回
  if (state.touchId !== touches.identifier) return
  // è®¡ç®—X,Y轴位移, å•列时候X轴初始不做位移
  var translateX = basedata.columns === 1 ? 0 : touches.pageX - (basedata.itemWidth / 2 + basedata.left)
  var translateY = touches.pageY - (basedata.itemHeight / 2 + basedata.top)
  // åˆ°é¡¶åˆ°ä½Žè‡ªåŠ¨æ»‘åŠ¨
  if (touches.clientY > basedata.windowHeight - basedata.itemHeight - basedata.realBottomSize) {
    // å½“前触摸点pageY + item高度 - (屏幕高度 - åº•部固定区域高度)
    ownerInstance.callMethod('pageScroll', {
      scrollTop: touches.pageY + basedata.itemHeight - (basedata.windowHeight - basedata.realBottomSize)
    })
  } else if (touches.clientY < basedata.itemHeight + basedata.realTopSize) {
    // å½“前触摸点pageY - item高度 - é¡¶éƒ¨å›ºå®šåŒºåŸŸé«˜
    ownerInstance.callMethod('pageScroll', {
      scrollTop: touches.pageY - basedata.itemHeight - basedata.realTopSize
    })
  }
  // è®¾ç½®å½“前激活元素的偏移量
  instance.setStyle({
    'transform': 'translate3d('+ translateX + 'px,' + translateY + 'px, 0)'
  })
  var startKey = state.list[state.current].sortKey
  var currentX = Math.round(translateX / basedata.itemWidth)
  var currentY = Math.round(translateY / basedata.itemHeight)
  var endKey = currentX + basedata.columns * currentY
  // ç›®æ ‡é¡¹æ—¶å›ºå®šé¡¹åˆ™è¿”回
  var item = state.list[endKey]
  if (item && item.fixed) return
  // X轴或者Y轴超出范围则返回
  if (isOutRange(currentX, basedata.columns, currentY, basedata.rows, endKey, state.list.length)) return
  // é˜²æ­¢æ‹–拽过程中发生乱序问题
  if (startKey === endKey || startKey === state.preStartKey) return
  state.preStartKey = startKey
  var list = sortCore(startKey, endKey, state)
  state.itemsInstance.forEach(function(itemInstance, index) {
    var item = list[index]
    if (index !== state.current) {
      itemInstance.setStyle({
        'transform': 'translate3d('+ item.translateX + ',' + item.translateY +', 0)'
      })
    }
  })
  // ownerInstance.callMethod('vibrate')
  ownerInstance.callMethod('listDataChange', {
    data: list
  })
  triggerCustomEvent(list, "change", ownerInstance)
}
var touchEnd = function(event, ownerInstance) {
  var instance = event.instance
  var dataset = instance.getDataset()
  var state = ownerInstance.getState()
  var basedata = state.basedata
  if (!state.dragging || !edit) return
  triggerCustomEvent(state.list, "sortEnd", ownerInstance)
  instance.addClass('tn-drag__transition')
  instance.setStyle({
    'transform': 'translate3d('+ state.list[state.current].translateX + ',' + state.list[state.current].translateY + ', 0)'
  })
  state.itemsInstance.forEach(function(item, index) {
    item.removeClass('tn-drag__transition')
  })
  state.preStartKey = -1
  state.dragging = false
  ownerInstance.callMethod('drag', {
    dragging: false
  })
  state.current = -1
  state.translateX = 0
  state.translateY = 0
}
module.exports = {
    longPress: longPress,
    touchStart: touchStart,
    touchMove: touchMove,
    touchEnd: touchEnd,
    baseDataObserver: baseDataObserver,
    listObserver: listObserver
}
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-drag/tn-drag.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,278 @@
<template>
  <view
    class="tn-drag-class tn-drag"
    :style="{
      height: wrapHeight + 'rpx'
    }"
    :list="listData"
    :basedata="baseData"
    :change:list="wxs.listObserver"
    :change:basedata="wxs.baseDataObserver"
  >
    <!-- #ifdef MP-WEIXIN -->
    <view
      v-for="(item, index) in listData"
      :key="item.id"
      class="tn-drag__item"
      :style="{
        width: 100 / columns + '%',
        height: itemHeight + 'rpx'
      }"
      :data-index="index"
      :data-basedata="baseData"
      :data-edit="edit"
      @longpress="wxs.longPress"
      @touchstart="wxs.touchStart"
      :catch:touchmove="dragging?wxs.touchMove:''"
      :catch:touchend="dragging?wxs.touchEnd:''"
    >
      <slot :entity="item.data" :fixed="item.fixed" :index="index" :height="itemHeight" :isEdit="edit"></slot>
    </view>
    <!-- #endif -->
    <!-- #ifndef MP-WEIXIN -->
    <view
      v-for="(item, index) in listData"
      :key="item.id"
      class="tn-drag__item"
      :style="{
        width: 100 / columns + '%',
        height: itemHeight + 'rpx'
      }"
      @longpress="wxs.longPress"
      :data-index="index"
      :data-basedata="baseData"
      :data-edit="edit"
      @touchstart="wxs.touchStart"
      @touchmove="wxs.touchMove"
      @touchend="wxs.touchEnd"
    >
      <slot :entity="item.data" :fixed="item.fixed" :index="index" :height="itemHeight" :isEdit="edit"></slot>
    </view>
    <!-- #endif -->
  </view>
</template>
<script src="./index.wxs" lang="wxs" module="wxs"></script>
<script>
  export default {
    name: 'tn-drag',
    props: {
      // æ•°æ®æº
      // å¦‚果属性中包含fixed,则标识当前数据不允许拖动
      list: {
        type: Array,
        default () {
          return []
        }
      },
      // æ˜¯å¦å…è®¸æ‹–动编辑
      edit: {
        type: Boolean,
        default: true
      },
      // åˆ—æ•°
      columns: {
        type: Number,
        default: 3
      },
      // item元素高度, å•位rpx
      itemHeight: {
        type: Number,
        default: 0
      },
      // å½“前父元素滚动的高度
      scrollTop: {
        type: Number,
        default: 0
      }
    },
    computed: {
      wrapHeight() {
        return this.rows * this.itemHeight
      }
    },
    data() {
      return {
        // æœªæ¸²æŸ“前节点数据
        baseData: {},
        // æ‹–动后的数据
        dragData: [],
        // è¡Œæ•°
        rows: 0,
        // æ¸²æŸ“数据
        listData: [],
        // æ ‡è®°æ˜¯å¦æ­£åœ¨æ‹–动
        dragging: false
      }
    },
    watch: {
      list(val) {
        this.listData = []
        this.$nextTick(() => {
          this.init()
        })
      },
      columns(val) {
        this.listData = []
        this.$nextTick(() => {
          this.init()
        })
      }
    },
    mounted() {
      this.$nextTick(() => {
        this.init()
      })
    },
    methods: {
      // åˆå§‹åŒ–
      init() {
        this.dragging = true
        const initDragItem = item => {
          const obj = {
            ...item
          }
          const fixed = obj?.fixed || false
          delete obj["fixed"]
          return {
            id: this.unique(),
            fixed,
            data: {
              ...obj
            }
          }
        }
        let i = 0
        const listData = (this.list || []).map((item, index) => {
          let listItem = initDragItem(item)
          // çœŸå®žæŽ’序
          listItem.realKey = i++
          // æ•´ä½“排序
          listItem.sortKey = index
          listItem.translateX = `${(listItem.sortKey % this.columns) * 100}%`
          listItem.translateY = `${Math.floor(listItem.sortKey / this.columns) * 100}%`
          return listItem
        })
        this.rows = Math.ceil(listData.length / this.columns)
        this.listData = listData
        this.dragData = listData
        if (listData.length === 0) return
        // console.log(listData);
        // åˆå§‹åŒ–dom元素
        this.$nextTick(() => {
          this.initRect()
        })
      },
      // åˆå§‹åŒ–dom元素
      initRect() {
        const {
          windowWidth,
          windowHeight
        } = uni.getSystemInfoSync()
        let baseData = {}
        baseData.windowHeight = windowHeight
        baseData.realTopSize = 0
        baseData.realBottomSize = 0
        baseData.columns = this.columns
        baseData.rows = this.rows
        const query = uni.createSelectorQuery().in(this)
        query.select('.tn-drag').boundingClientRect()
        query.select('.tn-drag__item').boundingClientRect()
        query.exec(res => {
          if (!res) {
            setTimeout(() => {
              this.initRect()
            }, 10)
            return
          }
          baseData.itemWidth = res[1].width
          baseData.itemHeight = res[1].height
          baseData.left = res[0].left
          baseData.top = res[0].top + this.scrollTop
          this.dragging = false
          this.baseData = baseData
        })
      },
      // è§¦å‘震动
      vibrate() {
        uni.vibrateShort()
      },
      // æ»šåŠ¨åˆ°æŒ‡å®šçš„ä½ç½®
      pageScroll(e) {
        uni.pageScrollTo({
          scrollTop: e.scrollTop,
          duration: 0
        })
      },
      // ä¿®æ”¹æ‹–动状态
      drag(e) {
        this.dragging = e.dragging
      },
      // æ‹–拽数据发生改变
      listDataChange(e) {
        this.dragData = e.data
      },
      // item被点击
      itemClick(index) {
        const item = this.dragData[index]
        this.$emit('click', {
          key: item.realKey,
          data: item.data
        })
      },
      // æ‹–拽结束事件
      sortEnd(e) {
        this.$emit('end', {
          data: e.data
        })
      },
      // æŽ’序发生改变事件
      change(e) {
        this.$emit('change', {
          data: e.data
        })
      },
      // ç”Ÿæˆå…ƒç´ å”¯ä¸€id
      unique(n = 6) {
        let id = ''
        for (let i = 0; i < n; i++) id += Math.floor(Math.random() * 10)
        return 'tn_' + new Date().getTime() + id
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-drag {
    position: relative;
    &__item {
      position: absolute;
      z-index: 2;
      top: 0;
      left: 0;
    }
    &__transition {
      transition: transform 0.25s !important;
    }
    &__current {
      z-index: 10 !important;
    }
    &__fixed {
      z-index: 1 !important;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-empty/tn-empty.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,190 @@
<template>
  <view v-if="show" class="tn-empty-class tn-empty" :style="[emptyStyle]">
    <view
      v-if="!isImage"
      class="tn-empty__icon"
      :class="[icon ? `tn-icon-${icon}` : `tn-icon-empty-${mode}`]"
      :style="[iconStyle]"
    ></view>
    <image
      v-else
      class="tn-empty__image"
      :style="[imageStyle]"
      :src="icon"
      mode="widthFix"
    ></image>
    <view
      class="tn-empty__text"
      :style="[textStyle]"
    >{{ text ? text : icons[mode]}}</view>
    <view v-if="$slots.default || $slots.$default" class="tn-empty__slot">
      <slot/>
    </view>
  </view>
</template>
<script>
  export default {
    name: 'tn-empty',
    props: {
      // æ˜¾ç¤ºç©ºç™½é¡µ
      show: {
        type: Boolean,
        default: true
      },
      // å†…ç½®icon的名称
      // å›¾ç‰‡è·¯å¾„,建议使用绝对路径
      icon: {
        type: String,
        default: ''
      },
      // é¢„置图标类型
      mode: {
        type: String,
        default: 'data'
      },
      // æç¤ºæ–‡å­—
      text: {
        type: String,
        default: ''
      },
      // æ–‡å­—颜色
      textColor: {
        type: String,
        default: ''
      },
      // æ–‡å­—大小,单位rpx
      textSize: {
        type: Number,
        default: 0
      },
      // å›¾æ ‡é¢œè‰²
      iconColor: {
        type: String,
        default: ''
      },
      // å›¾æ ‡å¤§å°ï¼Œå•位rpx
      iconSize: {
        type: Number,
        default: 0
      },
      // å›¾ç‰‡å®½åº¦ï¼ˆå½“图标为图片时生效),单位rpx
      imgWidth: {
        type: Number,
        default: 0
      },
      // å›¾ç‰‡é«˜åº¦ï¼ˆå½“图标为图片时生效),单位rpx
      imgHeight: {
        type: Number,
        default: 0
      },
      // è‡ªå®šä¹‰ç»„件样式
      customStyle: {
        type: Object,
        default() {
          return {}
        }
      }
    },
    computed: {
      emptyStyle() {
        let style = {}
        Object.assign(style, this.customStyle)
        return style
      },
      iconStyle() {
        let style = {}
        if (this.iconSize) {
          style.fontSize = this.iconSize + 'rpx'
        }
        if (this.iconColor) {
          style.color = this.iconColor
        }
        return style
      },
      imageStyle() {
        let style = {}
        if (this.imgWidth) {
          style.width = this.imgWidth + 'rpx'
        }
        if (this.imgHeight) {
          style.height = this.imgHeight + 'rpx'
        }
        return style
      },
      textStyle() {
        let style = {}
        if (this.textColor) {
          style.color = this.textColor
        }
        if (this.textSize) {
          style.fontSize = this.textSize + 'rpx'
        }
        return style
      },
      // åˆ¤æ–­ä¼ é€’çš„icon是否为图片
      isImage() {
        return this.icon.indexOf('/') >= 0
      }
    },
    data() {
      return {
        icons: {
          cart: '购物车为空',
          page: '页面不存在',
          search: '搜索结果为空',
          address: '地址为空',
          network: '网络不通',
          order: '订单为空',
          coupon: '优惠券为空',
          favor: '暂无收藏',
          permission: '无权限',
          history: '历史记录为空',
          message: '暂无消息',
          list: '列表为空',
          data: '暂无数据',
          comment: '暂无评论'
        }
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    &__icon {
      margin-top: 14rpx;
      color: #AAAAAA;
      font-size: 90rpx;
    }
    &__image {
      width: 160rpx;
      height: 160rpx;
    }
    &__text {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      margin-top: 20rpx;
      color: #AAAAAA;
      font-size: 30rpx;
    }
    &__slot {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      margin-top: 20rpx;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-fab/tn-fab.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,523 @@
<template>
  <view class="tn-fab-class tn-fab" @touchmove.stop.prevent>
    <view
      class="tn-fab__box"
      :class="{'tn-fab--right': left === 'auto'}"
      :style="{
        left: $t.string.getLengthUnitValue(fabLeft || left),
        right: $t.string.getLengthUnitValue(fabRight || right),
        bottom: $t.string.getLengthUnitValue(fabBottom || bottom),
        zIndex: elZIndex
      }"
    >
      <view
        v-if="visibleSync"
        class="tn-fab__btns"
        :class="[`tn-fab__btns__animation--${animationType}`,
          showFab ? `tn-fab__btns--visible--${animationType}` : ''
        ]"
      >
        <view
          v-for="(item, index) in btnList"
          :key="index"
          class="tn-fab__item"
          :class="[
            `tn-fab__item__animation--${animationType}`,
            {'tn-fab__item--left': right === 'auto'}
          ]"
          :style="[fabItemStyle(index)]"
          @tap.stop="handleClick(index)"
        >
          <!-- å¸¦å›¾æ ‡æˆ–者图片时显示的文字信息 -->
          <view
            v-if="animationType !== 'around' && (item.imgUrl || item.icon)"
            :class="[left === 'auto' ? 'tn-fab__item__text--right' : 'tn-fab__item__text--left']"
            :style="{
              color: item.textColor || '#FFF',
              fontSize: $t.string.getLengthUnitValue(item.textSize || 28)
            }"
          >{{ item.text || '' }}</view>
          <!-- å¸¦å›¾ç‰‡æˆ–者图标时的图片、图标信息 -->
          <view
            class="tn-fab__item__btn"
            :class="[!item.bgColor ? backgroundColorClass : '']"
            :style="{
              width: $t.string.getLengthUnitValue(width),
              height: $t.string.getLengthUnitValue(height),
              lineHeight: $t.string.getLengthUnitValue(height),
              backgroundColor: item.bgColor || backgroundColorStyle || '#01BEFF',
              borderRadius: $t.string.getLengthUnitValue(radius)
            }"
          >
            <!-- æ— å›¾ç‰‡å’Œæ— å›¾æ ‡æ—¶åªæ˜¾ç¤ºæ–‡å­— -->
            <view
              v-if="!item.imgUrl && !item.icon"
              class="tn-fab__item__btn__title"
              :style="{
                color: item.textColor || '#fff',
                fontSize: $t.string.getLengthUnitValue(item.textSize || 28)
              }"
            >{{ item.text || '' }}</view>
            <!-- å›¾æ ‡ -->
            <view
              v-if="item.icon"
              class="tn-fab__item__btn__icon"
              :class="[`tn-icon-${item.icon}`]"
              :style="{
                color: item.iconColor || '#fff',
                fontSize: $t.string.getLengthUnitValue(item.iconSize || iconSize || 64)
              }"
            ></view>
            <!-- å›¾ç‰‡ -->
            <image
              v-else-if="!item.icon && item.imgUrl"
              class="tn-fab__item__btn__image"
              :style="{
                width: $t.string.getLengthUnitValue(item.imgWidth || 64),
                height: $t.string.getLengthUnitValue(item.imgHeight || 64),
              }"
              :src="item.imgUrl"
            ></image>
          </view>
        </view>
      </view>
      <view
        class="tn-fab__item__btn tn-fab__item__btn--fab"
        :class="[backgroundColorClass, fontColorClass, {'tn-fab__item__btn--active': showFab}]"
        :style="{
          width: $t.string.getLengthUnitValue(width),
          height: $t.string.getLengthUnitValue(height),
          backgroundColor: backgroundColorStyle || !backgroundColorClass ? '#01BEFF' : '',
          color: fontColorStyle || '#fff',
          borderRadius: $t.string.getLengthUnitValue(radius),
          zIndex: elZIndex - 1
        }"
        @tap.stop="fabClick"
      >
        <slot>
          <view class="tn-fab__item__btn__icon" :class="[`tn-icon-${icon}`]" :style="{fontSize: $t.string.getLengthUnitValue(iconSize || 64)}"></view>
        </slot>
      </view>
    </view>
    <view v-if="visibleSync && showMask" class="tn-fab__mask" :class="{'tn-fab__mask--visible': showFab}" :style="{zIndex: elZIndex - 3}" @tap="clickMask()"></view>
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    name: 'tn-fab',
    mixins: [componentsColorMixin],
    props: {
      // æŒ‰é’®åˆ—表
      // {
      //   // èƒŒæ™¯é¢œè‰²
      //   bgColor: '#fff',
      //   // å›¾ç‰‡åœ°å€
      //   imgUrl: '',
      //   // å›¾ç‰‡å®½åº¦
      //   imgWidth: 60,
      //   // å›¾ç‰‡é«˜åº¦
      //   imgHeight: 60,
      //   // å›¾æ ‡åç§°
      //   icon: '',
      //   // å›¾æ ‡å°ºå¯¸
      //   iconSize: 60,
      //   // å›¾æ ‡é¢œè‰²
      //   iconColor: '#fff',
      //   // æç¤ºæ–‡å­—
      //   text: '',
      //   // æ–‡å­—大小
      //   textSize: 30,
      //   // å­—体颜色
      //   textColor: '#fff'
      // }
      btnList: {
        type: Array,
        default() {
          return []
        }
      },
      // è‡ªå®šä¹‰æ‚¬æµ®æŒ‰é’®å†…容
      customBtn: {
        type: Boolean,
        default: false
      },
      // æ‚¬æµ®æŒ‰é’®çš„宽度
      width: {
        type: [String, Number],
        default: 88
      },
      // æ‚¬æµ®æŒ‰é’®çš„高度
      height: {
        type: [String, Number],
        default: 88
      },
      // å›¾æ ‡å¤§å°
      iconSize: {
        type: [String, Number],
        default: 64
      },
      // å›¾æ ‡åç§°
      icon: {
        type: String,
        default: 'open'
      },
      // æŒ‰é’®åœ†è§’
      radius: {
        type: [String, Number],
        default: '50%'
      },
      // æŒ‰é’®è·ç¦»å·¦è¾¹çš„位置
      left: {
        type: [String, Number],
        default: 'auto'
      },
      // æŒ‰é’®è·ç¦»å³è¾¹çš„位置
      right: {
        type: [String, Number],
        default: 'auto'
      },
      // æŒ‰é’®è·ç¦»åº•部的位置
      bottom: {
        type: [String, Number],
        default: 100
      },
      // å±•示动画类型 up å¾€ä¸Šå±•示 around çŽ¯ç»•
      animationType: {
        type: String,
        default: 'up'
      },
      // å½“动画为圆环时,每个弹出按钮之间的距离, å•位px
      aroundBtnDistance: {
        type: Number,
        default: 10
      },
      zIndex: {
        type: Number,
        default: 0
      },
      // æ˜¾ç¤ºé®ç½©
      showMask: {
        type: Boolean,
        default: true
      },
      // ç‚¹å‡»é®ç½©æ˜¯å¦å¯ä»¥å…³é—­
      maskCloseable: {
        type: Boolean,
        default: true
      }
    },
    data() {
      return {
        showFab: false,
        visibleSync: false,
        timer: null,
        fabLeft: 0,
        fabRight: 0,
        fabBottom: 0,
        fabBtnInfo: {
          width: 0,
          height: 0,
          left: 0,
          right: 0,
          bottom: 0
        },
        systemInfo: {
          width: 0,
          height: 0
        },
        updateProps: false
      }
    },
    computed: {
      elZIndex() {
        return this.zIndex || this.$t.zIndex.fab
      },
      propsData() {
        return [this.width, this.height, this.left, this.right, this.bottom]
      },
      fabItemStyle() {
        return (index) => {
          let style = {
            zIndex: this.elZIndex - 2
          }
          if (this.animationType === 'up' || !this.showFab) {
            return style
          }
          let base = 1
          if (this.left === 'auto') {
            base = 1
          } else if (this.right === 'auto') {
            base = -1
          }
          style.transform = `rotate(${base * index * 60}deg) translateX(${(this.aroundBtnDistance + this.fabBtnInfo.width) * (-(base))}px)`
          return style
        }
      }
    },
    watch: {
      propsData() {
        // æ›´æ–°æŒ‰é’®ä¿¡æ¯
        this.updateProps = true
      }
    },
    mounted() {
      this.$nextTick(() => {
        this.getFabBtnRectInfo()
      })
    },
    beforeDestroy() {
      if (this.timer) {
        clearTimeout(this.timer)
      }
    },
    methods: {
      // æŒ‰é’®ç‚¹å‡»äº‹ä»¶
      handleClick(index) {
        this.close()
        this.$emit('click', {index: index})
      },
      // ç‚¹å‡»æ‚¬æµ®æŒ‰é’®
      fabClick() {
        if (this.showFab) {
          this.close()
        } else {
          // console.log(this.visibleSync);
          if (this.visibleSync) {
            this.visibleSync = false
            return
          }
          this.open()
        }
      },
      // ç‚¹å‡»é®ç½©
      clickMask() {
        if (!this.showMask || !this.maskCloseable) return
        this.close()
      },
      open() {
        this.change('visibleSync', 'showFab', true)
        this.translateFabPosition()
      },
      close() {
        this.change('showFab', 'visibleSync', false)
        this.fabLeft = 0
        this.fabRight = 0
        this.fabBottom = 0
      },
      // å…³é—­æ—¶å…ˆé€šè¿‡åŠ¨ç”»éšè—å¼¹çª—å’Œé®ç½©ï¼Œå†ç§»é™¤æ•´ä¸ªç»„ä»¶
      // æ‰“开时,先渲染组件,延时一定时间再让遮罩和弹窗的动画起作用
      change(param1, param2, status) {
        this[param1] = status
        if (status) {
          // #ifdef H5 || MP
          this.timer = setTimeout(() => {
            this[param2] = status
            this.$emit(status ? 'open' : 'close')
            clearTimeout(this.timer)
          }, 10)
          // #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')
            clearTimeout(this.timer)
          }, 250)
        }
      },
      /******************** æ—‹è½¬åŠ¨ç”»ç›¸å…³å‡½æ•° ********************/
      // èŽ·å–æŒ‰é’®çš„ä¿¡æ¯
      async getFabBtnRectInfo() {
        const systemInfo = uni.getSystemInfoSync()
        const btnRectInfo = await this._tGetRect('.tn-fab__item__btn--fab')
        if (!btnRectInfo) {
          setTimeout(() => {
            this.getFabBtnRectInfo()
          }, 10)
          return
        }
        console.log(btnRectInfo);
        this.systemInfo = {
          width: systemInfo.windowWidth,
          height: systemInfo.windowHeight
        }
        this.fabBtnInfo.width = btnRectInfo.width
        this.fabBtnInfo.height = btnRectInfo.height
        this.fabBtnInfo.left = btnRectInfo.left
        this.fabBtnInfo.right = btnRectInfo.right
        this.fabBtnInfo.bottom = btnRectInfo.bottom
      },
      // æ›´æ–°æ‚¬æµ®æŒ‰é’®çš„位置
      translateFabPosition() {
        if (this.updateProps) {
          this.getFabBtnRectInfo()
          this.updateProps = false
        }
        if (this.animationType === 'up') return
        // æŒ‰é’®ç»„的宽度
        const btnGroupWidth = this.fabBtnInfo.width + this.aroundBtnDistance + 10
        // åˆ¤æ–­å½“前按钮是在左边还是右边
        if (this.left === 'auto' && btnGroupWidth > this.systemInfo.width - this.fabBtnInfo.right) {
          // è·ç¦»ä¸å¤Ÿéœ€è¦ç§»åЍ
          this.fabRight = btnGroupWidth + 'px'
        } else if (this.right === 'auto' && btnGroupWidth > this.fabBtnInfo.left) {
          this.fabLeft = btnGroupWidth + 'px'
        }
        if (btnGroupWidth > this.systemInfo.height - this.fabBtnInfo.bottom) {
          this.fabBottom = btnGroupWidth + 'px'
        }
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-fab {
    &__box {
      display: flex;
      justify-content: center;
      align-items: flex-start;
      flex-direction: column;
      position: fixed;
      transition: all 0.25s ease-in-out;
    }
    &--right {
      align-items: flex-end;
    }
    &__btns {
      transition: all 0.25s cubic-bezier(0,.13,0,1.43);
      transform-origin: 80% bottom;
      &__animation--up {
        opacity: 0;
        transform: translateY(100%);
      }
      &__animation--around {
        position: absolute;
        top: 0;
        left: 0;
      }
      &--visible--up {
        // visibility: visible;
        opacity: 1;
        transform: translateY(0);
      }
      &--visible--around {
        // visibility: visible;
        // opacity: 1;
      }
    }
    &__item {
      display: flex;
      justify-content: flex-end;
      align-items: center;
      padding-bottom: 20rpx;
      &__animation--around {
        position: absolute;
        top: 0;
        left: 0;
        transition: transform 0.25s ease-in-out;
        transform-origin: 50% 50%;
        padding-bottom: 0 !important;
      }
      &--left {
        flex-flow: row-reverse;
      }
      &__text {
        &--left {
          padding-left: 14rpx;
        }
        &--right {
          padding-right: 14rpx;
        }
      }
      &__btn {
        display: flex;
        align-items: center;
        justify-content: center;
        box-shadow: 0 0 5rpx 2rpx rgba(0, 0, 0, 0.07);
        transition: all 0.2s linear;
        &--active {
          animation-name: fab-button-animation;
          animation-duration: 0.2s;
          animation-timing-function: cubic-bezier(0,.13,0,1.43);
        }
        &__title {
          width: 90%;
          text-align: center;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
        }
        &__icon {
          text-align: center;
          font-size: 64rpx;
        }
        &__image {
          display: block;
        }
      }
    }
    &__mask {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: $tn-mask-bg-color;
      transition: all 0.2s ease-in-out;
      opacity: 0;
      &--visible {
        opacity: 1;
      }
    }
  }
  @keyframes fab-button-animation {
    0% {
      transform: scale(0.6);
    }
    // 20% {
    //   transform: scale(1.8);
    // }
    // 40% {
    //   transform: scale(0.4);
    // }
    // 50% {
    //   transform: scale(1.4);
    // }
    // 80% {
    //   transform: scale(0.8);
    // }
    100% {
      transform: scale(1);
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-form-item/tn-form-item.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,457 @@
<template>
  <view
    class="tn-form-item-class tn-form-item"
    :class="{
      'tn-border-solid-bottom': elBorderBottom,
      'tn-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom')
    }"
  >
    <view
      class="tn-form-item__body"
      :style="{
        flexDirection: elLabelPosition == 'left' ? 'row' : 'column'
      }"
    >
      <!-- å¤„理微信小程序中设置属性的问题,不设置值的时候会变成true -->
      <view
        class="tn-form-item--left"
        :style="{
          width: wLabelWidth,
          flex: `0 0 ${wLabelWidth}`,
          marginBottom: elLabelPosition == 'left' ? 0 : '10rpx'
        }"
      >
        <!-- å—对齐 -->
        <view v-if="required || leftIcon || label" class="tn-form-item--left__content"
          :style="[leftContentStyle]"
        >
          <!-- nvue不支持伪元素before -->
          <view v-if="leftIcon" class="tn-form-item--left__content__icon">
            <view :class="[`tn-icon-${leftIcon}`]" :style="leftIconStyle"></view>
          </view>
          <!-- <view
            class="tn-form-item--left__content__label"
            :style="[elLabelStyle, {
              'justify-content': elLabelAlign === 'left' ? 'flex-satrt' : elLabelAlign === 'center' ? 'center' : 'flex-end'
            }]"
          >
            {{label}}
          </view> -->
          <view
            class="tn-form-item--left__content__label"
            :style="[elLabelStyle]"
          >
            {{label}}
          </view>
          <text v-if="required" class="tn-form-item--left__content--required">*</text>
        </view>
      </view>
      <view class="tn-form-item--right tn-flex">
        <view class="tn-form-item--right__content">
          <view class="tn-form-item--right__content__slot">
            <slot></slot>
          </view>
          <view v-if="$slots.right || rightIcon" class="tn-form-item--right__content__icon tn-flex">
            <view v-if="rightIcon" :class="[`tn-icon-${rightIcon}`]" :style="rightIconStyle"></view>
            <slot name="right"></slot>
          </view>
        </view>
      </view>
    </view>
    <view
      v-if="validateState === 'error' && showError('message')"
      class="tn-form-item__message"
      :style="{
        paddingLeft: elLabelPosition === 'left' ? elLabelWidth + 'rpx' : '0'
      }"
    >
      {{validateMessage}}
    </view>
  </view>
</template>
<script>
  import Emitter from '../../libs/utils/emitter.js'
  import schema from '../../libs/utils/async-validator.js'
  // åŽ»é™¤è­¦å‘Šä¿¡æ¯
  schema.warning = function() {}
  export default {
    mixins: [Emitter],
    name: 'tn-form-item',
    inject: {
      tnForm: {
        default() {
          return null
        }
      }
    },
    props: {
      // label提示语
      label: {
        type: String,
        default: ''
      },
      // ç»‘定的值
      prop: {
        type: String,
        default: ''
      },
      // æ˜¯å¦æ˜¾ç¤ºè¡¨å•域的下划线边框
      borderBottom: {
        type:Boolean,
        default: true
      },
      // label(标签名称)的位置
      // left - å·¦è¾¹
      // top - ä¸Šè¾¹
      labelPosition: {
        type: String,
        default: ''
      },
      // label的宽度
      labelWidth: {
        type: Number,
        default: 0
      },
      // label的对齐方式
      // left - å·¦å¯¹é½
      // top - ä¸Šå¯¹é½
      // right - å³å¯¹é½
      // bottom - ä¸‹å¯¹é½
      labelAlign: {
        type: String,
        default: ''
      },
      // label çš„æ ·å¼
      labelStyle: {
        type: Object,
        default() {
          return {}
        }
      },
      // å·¦ä¾§å›¾æ ‡
      leftIcon: {
        type: String,
        default: ''
      },
      // å³ä¾§å›¾æ ‡
      rightIcon: {
        type: String,
        default: ''
      },
      // å·¦ä¾§å›¾æ ‡æ ·å¼
      leftIconStyle: {
        type: Object,
        default() {
          return {}
        }
      },
      // å³ä¾§å›¾æ ‡æ ·å¼
      rightIconStyle: {
        type: Object,
        default() {
          return {}
        }
      },
      // æ˜¯å¦æ˜¾ç¤ºå¿…填项的*,不做校验用途
      required: {
        type: Boolean,
        default: false
      }
    },
    computed: {
      // å¤„理微信小程序label的宽度
      wLabelWidth() {
        // å¦‚果用户设置label为空字符串(微信小程序空字符串最终会变成字符串的'true'),意味着要将label的位置宽度设置为auto
        return this.elLabelPosition === 'left' ? (this.label === 'true' || this.label === '' ? 'auto' : this.elLabelWidth + 'rpx') : '100%'
      },
      // æ˜¯å¦æ˜¾ç¤ºé”™è¯¯æç¤º
      showError() {
        return type => {
          if (this.errorType.indexOf('none') >= 0) return false
          else if (this.errorType.indexOf(type) >= 0) return true
          else return false
        }
      },
      // label的宽度(默认值为90)
      elLabelWidth() {
        return this.labelWidth != 0 ? this.labelWidth : (this.parentData.labelWidth != 0 ? this.parentData.labelWidth : 90)
      },
      // label的样式
      elLabelStyle() {
        return Object.keys(this.labelStyle).length ? this.labelStyle : (Object.keys(this.parentData.labelStyle).length ? 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() {
        return this.borderBottom !== '' ? this.borderBottom : (this.parentData.borderBottom !== '' ? this.parentData.borderBottom : true)
      },
      leftContentStyle() {
        let style = {}
        if (this.elLabelPosition === 'left') {
          switch(this.elLabelAlign) {
            case 'left':
              style.justifyContent = 'flex-start'
              break
            case 'center':
              style.justifyContent = 'center'
              break
            default:
              style.justifyContent = 'flex-end'
              break
          }
        }
        return style
      }
    },
    data() {
      return {
        // é»˜è®¤å€¼
        initialValue: '',
        // æ˜¯å¦æ ¡éªŒæˆåŠŸ
        validateState: '',
        // æ ¡éªŒå¤±è´¥æç¤ºä¿¡æ¯
        validateMessage: '',
        // é”™è¯¯çš„æç¤ºæ–¹å¼ï¼ˆå‚考form组件)
        errorType: ['message'],
        // å½“前子组件输入的值
        fieldValue: '',
        // çˆ¶ç»„件的参数
        // ç”±äºŽå†computed中无法得知this.parent的变化,所以放在data中
        parentData: {
          borderBottom: true,
          labelWidth: 90,
          labelPosition: 'left',
          labelAlign: 'left',
          labelStyle: {},
        }
      }
    },
    watch: {
      validateState(val) {
        this.broadcastInputError()
      },
      "tnForm.errorType"(val) {
        this.errorType = val
        this.broadcastInputError()
      }
    },
    mounted() {
      // ç»„件创建完成后,保存当前实例到form组件中
      // æ”¯ä»˜å®ã€å¤´æ¡å°ç¨‹åºä¸æ”¯æŒprovide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用\
      this.parent = this.$t.$parent.call(this, 'tn-form')
      if (this.parent) {
        // éåކparentData属性,将parent中同名的属性赋值给parentData
        Object.keys(this.parentData).map(key => {
          this.parentData[key] = this.parent[key]
        })
        // å¦‚果没有传入prop或者tnForm为空(单独使用form-item组件的时候),就不进行校验
        if (this.prop) {
          // å°†æœ¬å®žä¾‹æ·»åŠ åˆ°çˆ¶ç»„ä»¶ä¸­
          this.parent.fields.push(this)
          this.errorType = this.parent.errorType
          // è®¾ç½®åˆå§‹å€¼
          this.initialValue = this.fieldValue
          // æ·»åŠ è¡¨å•æ ¡éªŒï¼Œè¿™é‡Œå¿…é¡»è¦å†™åœ¨$nextTick中,因为tn-form的rules是通过ref手动传入的
          // ä¸åœ¨$nextTick中的话,可能会造成执行此处代码时,父组件还没通过ref把规则给tn-form,导致规则为空
          this.$nextTick(() => {
            this.setRules()
          })
        }
      }
    },
    beforeDestroy() {
      // ç»„件销毁前,将实例从tn-form的缓存中移除
      // å¦‚果当前没有prop的话表示当前不进行删除
      if (this.parent && this.prop) {
        this.parent.fields.map((item, index) => {
          if (item === this) this.parent.fields.splice(index, 1)
        })
      }
    },
    methods: {
      // å‘input组件发出错误事件
      broadcastInputError() {
        this.broadcast('tn-input', 'on-form-item-error', this.validateState === 'error' && this.showError('border'))
      },
      // è®¾ç½®æ ¡éªŒè§„则
      setRules() {
        let that = this
        // ä»Žçˆ¶ç»„ä»¶tn-form拿到当前tn-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)
      },
      // ä»Žform的rules属性中取出当前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规则
      getFilterRule(triggerType = '') {
        let rules = this.getRules()
        // æ•´ä½“验证表单时,triggerType为空字符串,此时返回所有规则进行验证
        if (!triggerType) return rules
        // æŸäº›åœºæ™¯å¯èƒ½çš„判断规则,可能不存在trigger属性,故先判断是否存在此属性
        // åŽ†éåˆ¤æ–­è§„åˆ™æ˜¯å¦æœ‰å¯¹åº”çš„äº‹ä»¶ï¼Œæ¯”å¦‚blur,change触发等的事件
        // ä½¿ç”¨indexOf判断,是因为某些时候设置的验证规则的trigger属性可能为多个,比如['blur','change']
        return rules.filter(rule => rule.trigger && rule.trigger.indexOf(triggerType) !== -1)
      },
      // æ ¡éªŒæ•°æ®
      validation(trigger, callback = ()=>{}) {
        // æ ¡éªŒä¹‹å‰å…ˆèŽ·å–éœ€è¦æ ¡éªŒçš„å€¼
        this.fieldValue = this.parent.model[this.prop]
        // blur和change是否有当前方式的校验规则
        let rules = this.getFilterRule(trigger)
        // åˆ¤æ–­æ˜¯å¦æœ‰éªŒè¯è§„则,如果没有规则,也调用回调方法,否则父组件tn-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)
        })
      },
      // æ¸…空当前item信息
      resetField() {
        this.parent.model[this.prop] = this.initialValue
        // æ¸…空错误标记
        this.validateState = 'success'
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-form-item {
    display: flex;
    flex-direction: column;
    padding: 20rpx 0;
    font-size: 28rpx;
    color: $tn-font-color;
    box-sizing: border-box;
    line-height: $tn-form-item-height;
    &__border-bottom--error:after {
      border-color: $tn-color-red;
    }
    &__body {
      display: flex;
      flex-direction: row;
    }
    &--left {
      display: flex;
      flex-direction: row;
      align-items: center;
      &__content {
        display: flex;
        flex-direction: row;
        position: relative;
        align-items: center;
        padding-right: 18rpx;
        flex: 1;
        &--required {
          position: relative;
          right: 0;
          vertical-align: middle;
          color: $tn-color-red;
        }
        &__icon {
          color: $tn-font-sub-color;
          margin-right: 8rpx;
        }
        &__label {
          // display: flex;
          // flex-direction: row;
          // align-items: center;
          // flex: 1;
        }
      }
    }
    &--right {
      flex: 1;
      &__content {
        display: flex;
        flex-direction: row;
        align-items: center;
        flex: 1;
        &__slot {
          flex: 1;
          /* #ifndef MP */
          display: flex;
          flex-direction: row;
          align-items: center;
          /* #endif */
        }
        &__icon {
          margin-left: 10rpx;
          color: $tn-font-sub-color;
          font-size: 30rpx;
        }
      }
    }
    &__message {
      font-size: 24rpx;
      line-height: 24rpx;
      color: $tn-color-red;
      margin-top: 12rpx;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-form/tn-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,139 @@
<template>
  <view class="tn-form-class tn-form">
    <slot></slot>
  </view>
</template>
<script>
  export default {
    name: 'tn-form',
    props: {
      // è¡¨å•数据对象(需要验证的表单数据)
      model: {
        type: Object,
        default() {
          return {}
        }
      },
      // å‘生错误时的提示方式
      // toast - å¼¹å‡ºtoast框
      // message - æç¤ºä¿¡æ¯
      // border - å¦‚果设置了边框,边框会变成红色
      // border-bottom - ä¸‹è¾¹æ¡†ä¼šå‘ˆçŽ°çº¢è‰²
      // none - æ— æç¤º
      errorType: {
        type: Array,
        default() {
          return ['message', 'toast']
        }
      },
      // æ˜¯å¦æ˜¾ç¤ºè¡¨å•域的下划线边框
      borderBottom: {
        type:Boolean,
        default: true
      },
      // label(标签名称)的位置
      // left - å·¦è¾¹
      // top - ä¸Šè¾¹
      labelPosition: {
        type: String,
        default: 'left'
      },
      // label的宽度
      labelWidth: {
        type: Number,
        default: 90
      },
      // label的对齐方式
      // left - å·¦å¯¹é½
      // center - å±…中对齐
      // right - å³å¯¹é½
      labelAlign: {
        type: String,
        default: 'left'
      },
      // label çš„æ ·å¼
      labelStyle: {
        type: Object,
        default() {
          return {}
        }
      }
    },
    // å‘子孙传递数据
    provide() {
      return {
        tnForm: this
      }
    },
    data() {
      return {
        rules: {}
      }
    },
    created() {
      // å­˜å‚¨å½“前form下的所有form-item的实例
      // ä¸èƒ½å®šä¹‰å†data中,否则小程序会循环引用而报错
      this.fields = []
    },
    methods: {
      /**
       * è®¾ç½®è§„则
       *
       * @param {Object} rules
       */
      setRules(rules) {
        this.rules = rules
      },
      /**
       * æ¸…空form-item组件
       */
      resetFields() {
        this.fields.map(field => {
          field.resetField()
        })
      },
      /**
       * æ ¡éªŒæ•°æ®
       * @param {Object} callback æ ¡éªŒå›žè°ƒæ–¹æ³•
       */
      validate(callback) {
        return new Promise(resolve => {
          // æ ‡è®°æ ¡éªŒæ˜¯å¦é€šè¿‡
          let valid = true
          // æ ‡è®°æ˜¯å¦æ£€æŸ¥å®Œæ¯•
          let count = 0
          // å­˜æ”¾é”™è¯¯ä¿¡æ¯
          let errors = []
          // å¯¹æ‰€æœ‰form-item进行校验
          this.fields.map(field => {
            // è°ƒç”¨å¯¹åº”form-item实例的validation校验方法
            field.validation('', error => {
              // å¦‚果有一个form-item校验不通过,则整个表单校验不通过
              if (error) {
                valid = false
                errors.push(error)
              }
              // å½“遍历完所有的form-item的校验规则,返回信息
              if (++count === this.fields.length) {
                resolve(valid)
                // åˆ¤æ–­æ˜¯å¦è®¾ç½®äº†toast的提示方式,只提示表单域中最前面的一条错误信息
                if (this.errorType.indexOf('none') === -1 &&
                    this.errorType.indexOf('toast') >= 0 &&
                    errors.length > 0) {
                  this.$t.message.toast(errors[0])
                }
                // è°ƒç”¨å›žè°ƒæ–¹æ³•
                if (typeof callback == 'function') callback(valid)
              }
            })
          })
        })
      }
    }
  }
</script>
<style>
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-goods-nav/tn-goods-nav.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,382 @@
<template>
  <view
    class="tn-goods-nav-class tn-goods-nav"
    :class="[
      backgroundColorClass,
      {
        'tn-goods-nav--fixed': fixed,
        'tn-safe-area-inset-bottom': safeAreaInsetBottom,
        'tn-goods-nav--shadow': shadow
      }
    ]"
    :style="[backgroundColorStyle, navStyle]"
  >
    <view class="options">
      <view
        v-for="(item, index) in optionsData"
        :key="index"
        class="options__item"
        :class="[{'options__item--avatar': item.showAvatar}]"
        @tap="handleOptionClick(index)"
      >
        <block v-if="item.showAvatar">
          <tn-avatar
            :src="item.avatar"
            size="sm"
            :badge="item.showBadge"
            :badgeText="item.count"
            :badgeBgColor="item.countBackgroundColor"
            :badgeColor="item.countFontColor"
            :badgeSize="26"
          ></tn-avatar>
        </block>
        <block v-else>
          <view class="options__item__icon" :class="[`tn-icon-${item.icon}`]" :style="[optionStyle(index, 'icon')]">
            <tn-badge v-if="item.showBadge" :absolute="true" :backgroundColor="item.countBackgroundColor" :fontColor="item.countFontColor" :fontSize="16" padding="2rpx 5rpx">{{ item.count }}</tn-badge>
          </view>
          <view class="options__item__text" :style="[optionStyle(index, 'text')]">{{ item.text }}</view>
        </block>
      </view>
    </view>
    <view class="buttons">
      <view
        v-for="(item, index) in buttonGroupsData"
        :key="index"
        class="buttons__item"
        :class="[buttonClass(index)]"
        :style="[buttonStyle(index)]"
        @tap="handleButtonClick(index)"
      >
        <view class="buttons__item__text">{{ item.text }}</view>
      </view>
    </view>
  </view>
</template>
<script>
  export default {
    name: 'tn-goods-nav',
    props: {
      // é€‰é¡¹ä¿¡æ¯
      // å»ºè®®ä¸è¶…过3个
      // {
      //   icon: '', // å›¾æ ‡åç§°
      //   text: '', // æ˜¾ç¤ºçš„æ–‡æœ¬
      //   count: '', // è§’标的值
      //   countBackgroundColor: '', // è§’标背景颜色
      //   countFontColor: '', // è§’标字体颜色
      //   iconColor: '', // å›¾æ ‡é¢œè‰²
      //   textColor: '', // æ–‡æœ¬é¢œè‰²
      //   avatar: '', // æ˜¾ç¤ºå¤´åƒï¼ˆæ­¤æ—¶å°†ä¸æ˜¾ç¤ºå›¾æ ‡å’Œæ–‡æœ¬ï¼‰
      // }
      options: {
        type: Array,
        default() {
          return [{
            icon: 'shop',
            text: '店铺'
          },{
            icon: 'service',
            text: '客服'
          },{
            icon: 'star',
            text: '收藏'
          }]
        }
      },
      // æŒ‰é’®ç»„
      // å»ºè®®ä¸è¶…过2个
      // {
      //   text: '', // æ˜¾ç¤ºçš„æ–‡æœ¬
      //   backgroundColor: '', // æŒ‰é’®èƒŒæ™¯é¢œè‰²
      //   color: '' // æ–‡æœ¬é¢œè‰²
      // }
      buttonGroups: {
        type: Array,
        default() {
          return [{
            text: '加入购物车',
            backgroundColor: '#FFA726',
            color: '#FFFFFF'
          },{
            text: '结算',
            backgroundColor: '#FF7043',
            color: '#FFFFFF'
          }]
        }
      },
      // èƒŒæ™¯é¢œè‰²
      backgroundColor: {
        type: String,
        default: ''
      },
      // å¯¼èˆªçš„高度,单位rpx
      height: {
        type: Number,
        default: 0
      },
      // æ˜¾ç¤ºé˜´å½±
      shadow: {
        type: Boolean,
        default: false
      },
      // å¯¼èˆªçš„层级
      zIndex: {
        type: Number,
        default: 0
      },
      // æŒ‰é’®ç±»åž‹
      // rect -> æ–¹å½¢ paddingRect -> ä¸Šä¸‹å¸¦è¾¹è·æ–¹å½¢ round -> åœ†è§’
      buttonType: {
        type: String,
        default: 'rect'
      },
      // æ˜¯å¦å›ºå®šåœ¨åº•部
      fixed: {
        type: Boolean,
        default: false
      },
      // æ˜¯å¦å¼€å¯åº•部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
      safeAreaInsetBottom: {
          type: Boolean,
          default: false
      }
    },
    computed: {
      backgroundColorStyle() {
        return this.$t.color.getBackgroundColorStyle(this.backgroundColor)
      },
      backgroundColorClass() {
        return this.$t.color.getBackgroundColorInternalClass(this.backgroundColor)
      },
      navStyle() {
        let style = {}
        if (this.height) {
          style.height = this.height + 'rpx'
        }
        style.zIndex = this.zIndex ? this.zIndex : this.$t.zIndex.goodsNav
        return style
      },
      // é€‰é¡¹style
      optionStyle() {
        return (index, type) => {
          let style = {}
          const item = this.optionsData[index]
          if (type === 'icon' && item.iconColor) {
            style.color = item.iconColor
          } else if (type === 'text' && item.fontColor) {
            style.color = item.fontColor
          }
          return style
        }
      },
      // æŒ‰é’®class
      buttonClass() {
        return (index) => {
          let clazz = ''
          const item = this.buttonGroupsData[index]
          if (item.backgroundColorClass) {
            clazz += ` ${item.backgroundColorClass}`
          }
          if (item.colorClass) {
            clazz += ` ${item.colorClass}`
          }
          clazz += ` buttons__item--${this.$t.string.humpConvertChar(this.buttonType, '-')}`
          return clazz
        }
      },
      // æŒ‰é’®style
      buttonStyle() {
        return (index) => {
          let style = {}
          const item = this.buttonGroupsData[index]
          if (item.backgroundColorStyle) {
            style.backgroundColor = item.backgroundColorStyle
          }
          if (item.colorStyle) {
            style.color = item.colorStyle
          }
          return style
        }
      }
    },
    watch: {
      options(val) {
        this.initData()
      },
      buttonGroups(val) {
        this.initData()
      }
    },
    data() {
      return {
        // ä¿å­˜é€‰é¡¹æ•°æ®
        optionsData: [],
        // ä¿å­˜æŒ‰é’®ç»„数据
        buttonGroupsData: []
      }
    },
    created() {
      this.initData()
    },
    methods: {
      // åˆå§‹åŒ–选项和按钮数据
      initData() {
        this.handleOptionsData()
        this.handleButtonGroupsData()
      },
      // é€‰é¡¹ç‚¹å‡»äº‹ä»¶
      handleOptionClick(index) {
        this.$emit('optionClick', {
          index: index
        })
      },
      // æŒ‰é’®ç‚¹å‡»äº‹ä»¶
      handleButtonClick(index) {
        this.$emit('buttonClick', {
          index: index
        })
      },
      // å¤„理选项组数据
      handleOptionsData() {
        this.optionsData = this.options.map((item) => {
          let option = {...item}
          option.showAvatar = item.hasOwnProperty('avatar')
          if (item.hasOwnProperty('count')) {
            const count = this.$t.number.formatNumberString(item.count, 2)
            option.showBadge = true
            option.count = typeof count === 'number' ? String(count) : count
            option.countBackgroundColor = item.countBackgroundColor ? item.countBackgroundColor : '#01BEFF'
            option.countFontColor = item.countFontColor ? item.countFontColor : '#FFFFFF'
          }
          return option
        })
      },
      // å¤„理按钮组数据
      handleButtonGroupsData() {
        this.buttonGroupsData = this.buttonGroups.map((item) => {
          let button = {...item}
          if (item.hasOwnProperty('backgroundColor')) {
            button.backgroundColorClass = this.$t.color.getBackgroundColorInternalClass(item.backgroundColor)
            button.backgroundColorStyle = this.$t.color.getBackgroundColorStyle(item.backgroundColor)
          }
          if (item.hasOwnProperty('color')) {
            button.colorClass = this.$t.color.getBackgroundColorInternalClass(item.color)
            button.colorStyle = this.$t.color.getBackgroundColorStyle(item.color)
          }
          return button
        })
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-goods-nav {
    background-color: #FFFFFF;
    display: flex;
    flex-direction: row;
    align-items: center;
    height: 88rpx;
    width: 100%;
    box-sizing: content-box;
    &--shadow {
      box-shadow: 0rpx -10rpx 30rpx 0rpx rgba(0, 0, 0, 0.05);
      &::before {
        content: " ";
        position: absolute;
        width: 100%;
        height: 100%;
        bottom: 0;
        left: 0;
        right: 0;
        margin: auto;
        background-color: transparent;
        z-index: -1;
      }
    }
    &--fixed {
      position: fixed;
      bottom: 0;
      left: 0;
      right: 0;
    }
    .options {
      display: flex;
      flex-direction: row;
      align-items: center;
      height: 100%;
      color: #AAAAAA;
      &__item {
        padding: 0 26rpx;
        &--avatar {
          padding: 0rpx 0rpx 0rpx 26rpx !important;
        }
        &__icon {
          position: relative;
          font-size: 36rpx;
          margin-bottom: 6rpx;
        }
        &__text {
          font-size: 22rpx;
        }
      }
    }
    .buttons {
      flex: 1;
      display: flex;
      flex-direction: row;
      align-items: center;
      height: 100%;
      &__item {
        flex: 1;
        padding: 0 10rpx;
        display: flex;
        align-items: center;
        justify-content: center;
        &--rect {
          height: 100%;
        }
        &--padding-rect {
          height: 80%;
        }
        &--round {
          height: 75%;
          &:first-child {
            border-top-left-radius: 100rpx;
            border-bottom-left-radius: 100rpx;
          }
          &:last-child {
            border-top-right-radius: 100rpx;
            border-bottom-right-radius: 100rpx;
          }
        }
        &__text {
          display: inline-block;
          font-weight: bold;
          font-size: 30rpx;
          white-space: nowrap;
          text-overflow: ellipsis;
          overflow: hidden;
        }
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-grid-item/tn-grid-item.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,114 @@
<template>
  <view
    class="tn-grid-item-class tn-grid-item"
    :class="[
      backgroundColorClass
    ]"
    :hover-class="hoverClass"
    :hover-stay-time="150"
    :style="{
      backgroundColor: backgroundColorStyle,
      width: gridWidth
    }"
    @tap="click"
  >
    <view
      class="tn-grid-item__box"
    >
      <slot></slot>
    </view>
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    mixins: [ componentsColorMixin ],
    name: 'tn-grid-item',
    props: {
      // åºå·
      index: {
        type: [Number, String],
        default: ''
      }
    },
    data() {
      return {
        // çˆ¶ç»„件数据
        parentData: {
          // æŒ‰ä¸‹åŽ»çš„æ ·å¼
          hoverClass: '',
          col: 3
        }
      }
    },
    created() {
      // çˆ¶ç»„件实例
      this.updateParentData()
      this.parent.children.push(this)
    },
    computed: {
      // è®¡ç®—每个宫格的宽度
      gridWidth() {
        // #ifdef MP-WEIXIN
        return '100%'
        // #endif
        // #ifndef MP-WEIXIN
        return 100 / Number(this.parentData.col) + '%'
        // #endif
      },
      // ç‚¹å‡»æ•ˆæžœ
      hoverClass() {
        return this.parentData.hoverClass !== 'none'
                 ? this.parentData.hoverClass + ' tn-grid-item--hover'
                 : this.parentData.hoverClass
      }
    },
    methods: {
      // èŽ·å–çˆ¶ç»„ä»¶å‚æ•°
      updateParentData() {
        this.getParentData('tn-grid')
      },
      click() {
        this.$emit('click', this.index)
        this.parent && this.parent.click(this.index)
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-grid-item {
    box-sizing: border-box;
    background-color: #FFFFFF;
    /* #ifndef APP-NVUE */
    display: flex;
    flex-direction: row;
    /* #endif */
    align-items: center;
    justify-content: center;
    position: relative;
    flex-direction: column;
    /* #ifdef MP */
    // float: left;
    /* #endif */
    &__box {
      /* #ifndef APP-NVUE */
      display: flex;
      flex-direction: row;
      /* #endif */
      align-items: center;
      justify-content: center;
      flex-direction: column;
      flex: 1;
      width: 100%;
      height: 100%;
    }
    &--hover {
      background: $tn-space-color !important;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-grid/tn-grid.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,111 @@
<template>
  <view
    class="tn-grid-class tn-grid"
    :style="{
      justifyContent: gridAlignStyle
    }"
  >
    <slot></slot>
  </view>
</template>
<script>
  export default {
    name: 'tn-grid',
    props: {
      // åˆ†æˆå‡ åˆ—
      col: {
        type: [Number, String],
        default: 3
      },
      // å®«æ ¼å¯¹é½æ–¹å¼
      // left å·¦å¯¹é½ center å±…中对齐 right å³å¯¹é½
      align: {
        type: String,
        default: 'left'
      },
      // ç‚¹å‡»æ—¶çš„æ•ˆæžœï¼Œnone没有效果
      hoverClass: {
        type: String,
        default: 'tn-hover'
      }
    },
    data() {
      return {
      }
    },
    watch: {
      // å½“父组件和子组件需要共享参数变化,通知子组件
      parentData() {
        if (this.children.length) {
          this.children.map(child => {
            // åˆ¤æ–­å­ç»„件是否有updateParentData方式,有才执行
            typeof(child.updateParentData) === 'function' && child.updateParentData()
          })
        }
      }
    },
    created() {
      // å¦‚果将children定义在data中,在微信小程序会造成循环引用而报错
      this.children = []
    },
    computed: {
      // è®¡ç®—父组件的值是否发生变化
      parentData() {
        return [this.hoverClass, this.col, this.border]
      },
      // å®«æ ¼å¯¹é½æ–¹å¼
      gridAlignStyle() {
        switch(this.align) {
          case 'left':
            return 'flex-start'
          case 'center':
            return 'center'
          case 'right':
            return 'flex-end'
          default:
            return 'flex-start'
        }
      }
    },
    methods: {
      click(index) {
        this.$emit('click', index)
      }
    }
  }
</script>
<style lang="scss" scoped>
  // ç»„件中兼容小程序的方式,不过不能使用对齐方式
  // .tn-grid {
  //   width: 100%;
  //   /* #ifdef MP */
  //   position: relative;
  //   box-sizing: border-box;
  //   overflow: hidden;
  //   /* #endif */
  //   /* #ifndef MP */
  //   /* #ifndef APP-NVUE */
  //   display: flex;
  //   flex-direction: row;
  //   /* #endif */
  //   flex-wrap: wrap;
  //   align-items: center;
  //   /* #endif */
  // }
  // åœ¨ä½¿ç”¨ç»„件时兼容小程序
  .tn-grid {
    width: 100%;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-image-upload-drag/tn-image-upload-drag.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,995 @@
<template>
  <view v-if="!disabled" class="tn-image-upload-class tn-image-upload">
    <movable-area
      class="tn-image-upload__movable-area"
      :style="{
        height: movableAreaHeight
      }"
      @mouseenter="mouseEnterArea"
      @mouseleave="mouseLeaveArea"
    >
      <block
        v-for="(item, index) in lists"
        :key="item.id"
      >
        <movable-view
          class="tn-image-upload__movable-view"
          :style="{
            width: $t.string.getLengthUnitValue(width),
            height: $t.string.getLengthUnitValue(height),
            zIndex: item.zIndex,
            opacity: item.opacity,
          }"
          :x="item.x"
          :y="item.y"
          direction="all"
          :damping="40"
          :disabled="item.disabled"
          @change="movableChange($event, item)"
          @touchstart="movableStart(item)"
          @mousedown="movableStart(item)"
          @touchend="movableEnd(item)"
          @mouseup="movableEnd(item)"
          @longpress="movableLongPress(item)"
        >
          <view
            class="tn-image-upload__item tn-image-upload__item-preview"
            :style="{
              width: $t.string.getLengthUnitValue(itemWidth, 'px'),
              height: $t.string.getLengthUnitValue(itemHeight, 'px'),
              transform: `scale(${item.scale})`
            }"
          >
            <!-- åˆ é™¤æŒ‰é’® -->
            <view
              v-if="deleteable"
              class="tn-image-upload__item-preview__delete"
              @tap.stop="deleteItem(index)"
              :style="{
                borderTopColor: deleteBackgroundColor
              }"
            >
              <view
                class="tn-image-upload__item-preview__delete--icon"
                :class="[`tn-icon-${deleteIcon}`]"
                :style="{
                  color: deleteColor
                }"
              ></view>
            </view>
            <!-- è¿›åº¦æ¡ -->
            <tn-line-progress
              v-if="showProgress && item.data.progress > 0 && !item.data.error"
              class="tn-image-upload__item-preview__progress"
              :percent="item.data.progress"
              :showPercent="false"
              :round="false"
              :height="8"
            ></tn-line-progress>
            <!-- é‡è¯•按钮 -->
            <view v-if="item.data.error" class="tn-image-upload__item-preview__error-btn" @tap.stop="retry(index)">点击重试</view>
            <!-- å›¾ç‰‡ä¿¡æ¯ -->
            <image
              class="tn-image-upload__item-preview__image"
              :src="item.data.url || item.data.path"
              :mode="imageMode"
              @tap.stop="doPreviewImage(item.data.url || item.data.path, index)"
            ></image>
          </view>
        </movable-view>
      </block>
      <!-- æ·»åŠ æŒ‰é’® -->
      <view
        v-if="maxCount > lists.length"
        class="tn-image-upload__add"
        :style="{
          top: addBtn.y + 'px',
          left: addBtn.x + 'px',
          width: $t.string.getLengthUnitValue(itemWidth, 'px'),
          height: $t.string.getLengthUnitValue(itemHeight, 'px')
        }"
        @tap="selectFile"
      >
        <!-- æ·»åŠ æŒ‰é’® -->
        <view
          class="tn-image-upload__item tn-image-upload__item-add"
          hover-class="tn-hover-class"
          hover-stay-time="150"
          :style="{
            width: $t.string.getLengthUnitValue(itemWidth, 'px'),
            height: $t.string.getLengthUnitValue(itemHeight, 'px')
          }"
        >
          <view class="tn-image-upload__item-add--icon tn-icon-add"></view>
          <view class="tn-image-upload__item-add__tips">{{ uploadText }}</view>
        </view>
      </view>
    </movable-area>
  </view>
</template>
<script>
  export default {
    name: 'tn-image-upload-drag',
    props: {
      // å·²ä¸Šä¼ çš„æ–‡ä»¶åˆ—表
      fileList: {
        type: Array,
        default() {
          return []
        }
      },
      // ä¸Šä¼ å›¾ç‰‡åœ°å€
      action: {
        type: String,
        default: ''
      },
      // ä¸Šä¼ æ–‡ä»¶çš„字段名称
      name: {
        type: String,
        default: 'file'
      },
      // å¤´éƒ¨ä¿¡æ¯
      header: {
        type: Object,
        default() {
          return {}
        }
      },
      // æºå¸¦çš„参数
      formData: {
        type: Object,
        default() {
          return {}
        }
      },
      // æ˜¯å¦ç¦ç”¨
      disabled: {
        type: Boolean,
        default: false
      },
      // æ˜¯å¦è‡ªåŠ¨ä¸Šä¼ 
      autoUpload: {
        type: Boolean,
        default: true
      },
      // æœ€å¤§ä¸Šä¼ æ•°é‡
      maxCount: {
        type: Number,
        default: 9
      },
      // é¢„览上传图片的裁剪模式
      imageMode: {
        type: String,
        default: 'aspectFill'
      },
      // ç‚¹å‡»å›¾ç‰‡æ˜¯å¦å…¨å±é¢„览
      previewFullImage: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦æ˜¾ç¤ºè¿›åº¦æ¡
      showProgress: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦æ˜¾ç¤ºåˆ é™¤æŒ‰é’®
      deleteable: {
        type: Boolean,
        default: true
      },
      // åˆ é™¤æŒ‰é’®å›¾æ ‡
      deleteIcon: {
        type: String,
        default: 'close'
      },
      // åˆ é™¤æŒ‰é’®çš„背景颜色
      deleteBackgroundColor: {
        type: String,
        default: ''
      },
      // åˆ é™¤æŒ‰é’®çš„颜色
      deleteColor: {
        type: String,
        default: ''
      },
      // ä¸Šä¼ åŒºåŸŸæç¤ºæ–‡å­—
      uploadText: {
        type: String,
        default: '选择图片'
      },
      // æ˜¾ç¤ºtoast提示
      showTips: {
        type: Boolean,
        default: true
      },
      // é¢„览图片和选择图片区域的宽度
      width: {
        type: Number,
        default: 200
      },
      // é¢„览图片和选择图片区域的高度
      height: {
        type: Number,
        default: 200
      },
      // é€‰æ‹©å›¾ç‰‡çš„尺寸
      // å‚考上传文档 https://uniapp.dcloud.io/api/media/image
      sizeType: {
        type: Array,
        default() {
          return ['original', 'compressed']
        }
      },
      // å›¾ç‰‡æ¥æº
      sourceType: {
        type: Array,
        default() {
          return ['album', 'camera']
        }
      },
      // æ˜¯å¦å¯ä»¥å¤šé€‰
      multiple: {
        type: Boolean,
        default: true
      },
      // æ–‡ä»¶å¤§å°(byte)
      maxSize: {
        type: Number,
        default: 10 * 1024 * 1024
      },
      // å…è®¸ä¸Šä¼ çš„类型
      limitType: {
        type: Array,
        default() {
          return ['png','jpg','jpeg','webp','gif','image']
        }
      },
      // æ˜¯å¦è‡ªå®šè½¬æ¢ä¸ºjson
      toJson: {
        type: Boolean,
        default: true
      },
      // ä¸Šä¼ å‰é’©å­å‡½æ•°ï¼Œæ¯ä¸ªæ–‡ä»¶ä¸Šä¼ å‰éƒ½ä¼šæ‰§è¡Œ
      beforeUpload: {
        type: Function,
        default: null
      },
      // åˆ é™¤æ–‡ä»¶å‰é’©å­å‡½æ•°
      beforeRemove: {
        type: Function,
        default: null
      },
      index: {
        type: [Number, String],
        default: ''
      }
    },
    computed: {
      movableAreaHeight() {
        if (this.lists.length < this.maxCount) {
          return Math.ceil((this.lists.length + 1) / this.baseData.columns) * uni.upx2px(this.height) + 'px'
        } else {
          return Math.ceil(this.lists.length / this.baseData.columns) * uni.upx2px(this.height) + 'px'
        }
      },
      itemWidth() {
        return uni.upx2px(this.width) - (uni.upx2px(10) * 2)
      },
      itemHeight() {
        return uni.upx2px(this.height) - (uni.upx2px(10) * 2)
      }
    },
    data() {
      return {
        lists: [],
        uploading: false,
        baseData: {
          windowWidth: 0,
          columns: 0,
          dragItem: null,
          widthPx: 0,
          heightPx: 0
        },
        addBtn: {
          x: 0,
          y: 0
        },
        timer: null,
        dragging: false
      }
    },
    watch: {
      // fileList: {
      //   handler(val) {
      //     val.map(value => {
      //       // é¦–先检查内部是否已经添加过这张图片,因为外部绑定了一个对象给fileList的话(对象引用),进行修改外部fileList时,
      //       // ä¼šè§¦å‘watch,导致重新把原来的图片再次添加到this.lists
      //       // æ•°ç»„çš„some方法意思是,只要数组元素有任意一个元素条件符合,就返回true,而另一个数组的every方法的意思是数组所有元素都符合条件才返回true
      //       let tmp = this.lists.some(listVal => {
      //         return listVal.url === value.url
      //       })
      //       // å¦‚果内部没有这张图片,则添加到内部
      //       !tmp && this.lists.push({ url: value.url, error: false, progress: 100 })
      //     })
      //   },
      //   immediate: true
      // },
      lists(val) {
        this.$emit('on-list-change', this.sortList(), this.index)
      }
    },
    created() {
      this.baseData.windowWidth = uni.getSystemInfoSync().windowWidth
    },
    mounted() {
      this.$nextTick(() => {
        this.updateDragInfo()
      })
    },
    methods: {
      // æ¸…除列表
      clear() {
        this.lists = []
        this.updateAddBtnPositioin()
      },
      // é‡æ–°ä¸Šä¼ é˜Ÿåˆ—中上传失败所有文件
      reUpload() {
        this.uploadFile()
      },
      // é€‰æ‹©å›¾ç‰‡
      selectFile() {
        if (this.disabled) return
        const {
          name = '',
          maxCount,
          multiple,
          maxSize,
          sizeType,
          lists,
          camera,
          compressed,
          sourceType
        } = this
        let chooseFile = null
        const newMaxCount = maxCount - lists.length
        // åªé€‰æ‹©å›¾ç‰‡çš„æ—¶å€™ä½¿ç”¨ chooseImage æ¥å®žçް
        chooseFile = new Promise((resolve, reject) => {
          uni.chooseImage({
            count: multiple ? (newMaxCount > 9 ? 9 : newMaxCount) : 1,
            sourceType,
            sizeType,
            success: resolve,
            fail: reject
          })
        })
        chooseFile.then(res => {
          let file = null
          let listOldLength = lists.length
          res.tempFiles.map((val, index) => {
            if (!this.checkFileExt(val)) return
            // æ˜¯å¦è¶…出最大限制数量
            if (!multiple && index >= 1) return
            if (val.size > maxSize) {
              this.$emit('on-oversize', val, this.sortList(), this.index)
              this.showToast('超出可允许文件大小')
            } else {
              if (maxCount <= lists.length) {
                this.$emit('on-exceed', val, this.sortList(), this.index)
                this.showToast('超出最大允许的文件数')
                return
              }
              lists.push(this.handleDragListItem({
                url: val.path,
                progress: 0,
                error: false,
                file: val
              }))
              this.updateAddBtnPositioin()
            }
          })
          this.$emit('on-choose-complete', this.sortList(), this.index)
          if (this.autoUpload) this.uploadFile(listOldLength)
        }).catch(err => {
          this.$emit('on-choose-fail', err)
        })
      },
      // æç¤ºç”¨æˆ·ä¿¡æ¯
      showToast(message, force = false) {
        if (this.showTips || force) {
          this.$t.message.toast(message)
        }
      },
      // æ‰‹åŠ¨ä¸Šä¼ ï¼Œé€šè¿‡ref进行调用
      upload() {
        this.uploadFile()
      },
      // å¯¹å¤±è´¥å›¾ç‰‡è¿›è¡Œå†æ¬¡ä¸Šä¼ 
      retry(index) {
        this.lists[index].data.progress = 0
        this.lists[index].data.error = false
        this.lists[index].data.response = null
        this.$t.message.loading('重新上传')
        this.uploadFile(index)
      },
      // ä¸Šä¼ æ–‡ä»¶
      async uploadFile(index = 0) {
        if (this.disabled) return
        if (this.uploading) return
        // å…¨éƒ¨ä¸Šä¼ å®Œæˆ
        if (index >= this.lists.length) {
          this.$emit('on-uploaded', this.sortList(), this.index)
          return
        }
        // æ£€æŸ¥æ˜¯å¦å·²ç»å…¨éƒ¨ä¸Šä¼ æˆ–者上传中
        if (this.lists[index].data.progress === 100) {
          this.lists[index].data.uploadTask = null
          if (this.autoUpload) this.uploadFile(index + 1)
          return
        }
        // æ‰§è¡Œbefore-upload钩子
        if (this.beforeUpload && typeof(this.beforeUpload) === 'function') {
          // åœ¨å¾®ä¿¡ï¼Œæ”¯ä»˜å®ç­‰çŽ¯å¢ƒ(H5正常),会导致父组件定义的函数体中的this变成子组件的this
          // é€šè¿‡bind()方法,绑定父组件的this,让this的this为父组件的上下文
          // å› ä¸ºupload组件可能会被嵌套在其他组件内,比如tn-form,这时this.$parent其实为tn-form的this,
          // éžé¡µé¢çš„this,所以这里需要往上历遍,一直寻找到最顶端的$parent,这里用了this.$u.$parent.call(this)
          let beforeResponse = this.beforeUpload.bind(this.$t.$parent.call(this))(index, this.lists)
          // åˆ¤æ–­æ˜¯å¦è¿”回了Promise
          if (!!beforeResponse && typeof beforeResponse.then === 'function') {
            await beforeResponse.then(res => {
              // promise返回成功,不进行操作继续
            }).catch(err => {
              // è¿›å…¥catch回调的话,继续下一张
              return this.uploadFile(index + 1)
            })
          } else if (beforeResponse === false) {
            // å¦‚果返回flase,继续下一张图片上传
            return this.uploadFile(index + 1)
          } else {
            // ä¸ºtrue的情况,不进行操作
          }
        }
        // æ£€æŸ¥ä¸Šä¼ åœ°å€
        if (!this.action) {
          this.showToast('请配置上传地址', true)
          return
        }
        this.lists[index].data.error = false
        this.uploading = true
        // åˆ›å»ºä¸Šä¼ å¯¹è±¡
        const task = uni.uploadFile({
          url: this.action,
          filePath: this.lists[index].data.url,
          name: this.name,
          formData: this.formData,
          header: this.header,
          success: res => {
            // åˆ¤æ–­å•Šæ˜¯å¦ä¸ºjson字符串,将其转换为json格式
            let data = this.toJson && this.$t.test.jsonString(res.data) ? JSON.parse(res.data) : res.data
            if (![200, 201, 204].includes(res.statusCode)) {
              this.uploadError(index, data)
            } else {
              this.lists[index].data.response = data
              this.lists[index].data.progress = 100
              this.lists[index].data.error = false
              this.$emit('on-success', data, index, this.sortList(), this.index)
            }
          },
          fail: err => {
            this.uploadError(index, err)
          },
          complete: res => {
            this.$t.message.closeLoading()
            this.uploading = false
            this.uploadFile(index + 1)
            this.$emit('on-change', res, index, this.sortList(), this.index)
          }
        })
        this.lists[index].uploadTask = task
        task.onProgressUpdate(res => {
          if (res.progress > 0) {
            this.lists[index].data.progress = res.progress
            this.$emit('on-progress', res, index, this.sortList(), this.index)
          }
        })
      },
      // ä¸Šä¼ å¤±è´¥
      uploadError(index, err) {
        this.lists[index].data.progress = 0
        this.lists[index].data.error = true
        this.lists[index].data.response = null
        this.showToast('上传失败,请重试')
        this.$emit('on-error', err, index, this.sortList(), this.index)
      },
      // åˆ é™¤ä¸€ä¸ªå›¾ç‰‡
      deleteItem(index) {
        if (!this.deleteable) return
        this.$t.message.modal(
          '提示',
          '你确定要删除吗?',
          async () => {
            // å…ˆæ£€æŸ¥æ˜¯å¦æœ‰å®šä¹‰before-remove移除前钩子
            // æ‰§è¡Œbefore-remove钩子
            if (this.beforeRemove && typeof(this.beforeRemove) === 'function') {
              let beforeResponse = this.beforeRemove.bind(this.$t.$parent.call(this))(index, this.lists)
              // åˆ¤æ–­æ˜¯å¦è¿”回promise
              if (!!beforeResponse && typeof beforeResponse.then === 'function') {
                await beforeResponse.then(res => {
                  // promise返回成功不进行操作
                  this.handlerDeleteItem(index)
                }).catch(err => {
                  this.showToast('删除操作被中断')
                })
              } else if (beforeResponse === false) {
                this.showToast('删除操作被中断')
              } else {
                this.handlerDeleteItem(index)
              }
            } else {
              this.handlerDeleteItem(index)
            }
          }, true)
      },
      // ç§»é™¤æ–‡ä»¶æ“ä½œ
      handlerDeleteItem(index) {
        // å¦‚果文件正在上传中,终止上传任务
        if (this.lists[index].data.progress < 100 && this.lists[index].data.progress > 0) {
          typeof this.lists[index].data.uploadTask !== 'undefined' && this.lists[index].data.uploadTask.abort()
        }
        this.remove(index)
        this.$forceUpdate()
        this.$emit('on-remove', index, this.sortList(), this.index)
        this.showToast('删除成功')
      },
      // ç§»é™¤æ–‡ä»¶ï¼Œé€šè¿‡ref手动形式进行调用
      remove(index) {
        if (!this.deleteable) return
        // åˆ¤æ–­ç´¢å¼•合法
        if (index >= 0 && index < this.lists.length) {
          let currentItemIndex = this.lists[index].index
          this.lists.splice(index, 1)
          // é‡æ–°æŽ’列列表信息
          for (let item of this.lists) {
            if (item.index > currentItemIndex) {
              item.index -= 1
              item.x = item.positionX * this.baseData.widthPx
              item.y = item.positionY * this.baseData.heightPx
              item.positionX = item.index % this.baseData.columns
              item.positionY = Math.floor(item.index / this.baseData.columns)
              this.$nextTick(() => {
                item.x = item.positionX * this.baseData.widthPx
                item.y = item.positionY * this.baseData.heightPx
              })
            }
          }
          this.updateAddBtnPositioin()
        }
      },
      // é¢„览图片
      doPreviewImage(url, index) {
        if (!this.previewFullImage) return
        const images = this.lists.sort((l1, l2) => { return l1.index - l2.index}).map(item => item.data.url || item.data.path)
        uni.previewImage({
          urls: images,
          current: url,
          success: () => {
            this.$emit('on-preview', url, this.sortList(), this.index)
          },
          fail: () => {
            this.showToast('预览图片失败')
          }
        })
      },
      // æ£€æŸ¥æ–‡ä»¶åŽç¼€æ˜¯å¦åˆæ³•
      checkFileExt(file) {
        // æ˜¯å¦ä¸ºåˆæ³•后缀
        let noArrowExt = false
        // åŽç¼€å
        let fileExt = ''
        const reg = /.+\./
        // #ifdef H5
        fileExt = file.name.replace(reg, '').toLowerCase()
        // #endif
        // #ifndef H5
        fileExt = file.path.replace(reg, '').toLowerCase()
        // #endif
        noArrowExt = this.limitType.some(ext => {
          return ext.toLowerCase() === fileExt
        })
        if (!noArrowExt) this.showToast(`不支持${fileExt}格式的文件`)
        return noArrowExt
      },
      /********************* æ‹–拽处理 ********************/
      // æ›´æ–°æ‹–拽信息
      updateDragInfo() {
        this.baseData.widthPx = uni.upx2px(this.width)
        this.baseData.heightPx = uni.upx2px(this.height)
        // èŽ·å–å¯ç§»åŠ¨åŒºåŸŸçš„ä¿¡æ¯ï¼Œç”¨äºŽåˆ¤æ–­å½“å‰æœ‰å¤šå°‘åˆ—
        const query = uni.createSelectorQuery().in(this)
        query.select('.tn-image-upload__movable-area').boundingClientRect()
        query.exec((res) => {
          if (!res) {
            setTimeout(() => {
              this.updateDragInfo()
            }, 10)
            return
          }
          this.baseData.columns = Math.floor(res[0].width / this.baseData.widthPx)
          // åˆå§‹åŒ–可拖拽列表信息
          this.lists = []
          this.fileList.forEach((item) => {
            // é¦–先检查内部是否已经添加过这张图片,因为外部绑定了一个对象给fileList的话(对象引用),进行修改外部fileList时,
            // ä¼šè§¦å‘watch,导致重新把原来的图片再次添加到this.lists
            // æ•°ç»„çš„some方法意思是,只要数组元素有任意一个元素条件符合,就返回true,而另一个数组的every方法的意思是数组所有元素都符合条件才返回true
            let tmp = this.lists.map(value => {
              return value.data
            }).some(listVal => {
              return listVal.url === item.url
            })
            // å¦‚果内部没有这张图片,则添加到内部
            !tmp && this.lists.push(this.handleDragListItem({
              url: item.url,
              error: false,
              progress: 100
            }))
          })
          // æ›´æ–°æ·»åŠ æŒ‰é’®ä½ç½®
          this.updateAddBtnPositioin()
        })
      },
      // å¤„理拖拽列表信息
      handleDragListItem(item) {
        const positionX = this.lists.length % this.baseData.columns
        const positionY = Math.floor(this.lists.length / this.baseData.columns)
        const x = positionX * this.baseData.widthPx
        const y = positionY * this.baseData.heightPx
        return {
          id: this.unique(),
          x,
          y,
          preX: x,
          preY: y,
          positionX,
          positionY,
          zIndex:1,
          disabled: true,
          opacity: 1,
          scale: 1,
          index: this.lists.length,
          offset: 0,
          moveEnd: false,
          moving: false,
          data: {
            ...item
          }
        }
      },
      // ç”Ÿæˆå…ƒç´ å”¯ä¸€id
      unique(n = 6) {
        let id = ''
        for (let i = 0; i < n; i++) id += Math.floor(Math.random() * 10)
        return 'tn_' + new Date().getTime() + id
      },
      // æ›´æ–°æ·»åŠ æŒ‰é’®ä½ç½®
      updateAddBtnPositioin() {
        if (this.lists.length >= this.maxCount) return
        this.addBtn.x = (this.lists.length % this.baseData.columns) * this.baseData.widthPx
        this.addBtn.y = Math.floor(this.lists.length / this.baseData.columns) * this.baseData.heightPx
      },
      // èŽ·å–æŽ’åºåŽæ•°æ®
      sortList() {
        const list = this.lists.slice()
        list.sort((l1, l2) => {
          return l1.index - l2.index
        })
        return list.map(item => {
          return item.data
        })
      },
      mouseEnterArea () {
        // #ifdef H5
        this.lists.forEach(item => {
          item.disabled = false
        })
        // #endif
      },
      mouseLeaveArea () {
        // #ifdef H5
        if (this.baseData.dragItem) {
          this.lists.forEach(item => {
            item.disabled = true
            item.zIndex = 1
            item.offset = 0
            item.moveEnd = true
            if (item.id === this.baseData.dragItem.id) {
              if (this.timer) {
                clearTimeout(this.timer)
                this.timer = null
              }
              item.x = item.preX
              item.y = item.preY
              this.$nextTick(() => {
                item.x = item.positionX * this.baseData.widthPx
                item.y = item.positionY * this.baseData.heightPx
                this.baseData.dragItem = null
              })
            }
          })
          this.dragging = false
        }
        // #endif
      },
      movableLongPress(item) {
        // #ifndef H5
          uni.vibrateShort()
          // console.log("LongPress--------------------------------------------------------------");
          this.lists.forEach(value => {
            value.moving = false
          })
          this.dragging = true
          // è®¾ç½®å¯¹åº”的元素允许拖动
          const index = this.lists.findIndex(obj => {
            return obj.id === item.id
          })
          item.disabled = false
          item.opacity = 0.7
          item.scale = 1.1
          this.$set(this.lists, index, item)
        // #endif
      },
      movableChange (e, item) {
        if (!item || !this.dragging) return
        // console.log("movableChange");
        item.moving = true
        item.preX = e.detail.x
        item.preY = e.detail.y
        // console.log(item.preX, item.preY);
        if (e.detail.source === 'touch') {
          if (!item.moveEnd) {
            item.offset = Math.sqrt(
              Math.pow(item.preX - item.positionX * this.baseData.widthPx, 2) +
              Math.pow(item.preY - item.positionY * this.baseData.heightPx, 2))
          }
          let x = Math.floor((e.detail.x + this.baseData.widthPx / 2) / this.baseData.widthPx)
          if (x > this.baseData.columns) return
          let y = Math.floor((e.detail.y + this.baseData.heightPx / 2) / this.baseData.heightPx)
          let index = this.baseData.columns * y + x
          if (item.index !== index && index < this.lists.length) {
            for (let obj of this.lists) {
              if (item.index > index && obj.index >= index && obj.index < item.index) {
                this.updateItemPosition(obj, 1)
              } else if (item.index < index && obj.index <= index && obj.index > item.index) {
                this.updateItemPosition(obj, -1)
              } else if (item.id != obj.id) {
                // obj.offset = 0
                // console.log(obj.moving);
                // if (!obj.moving) {
                //   obj.preX = obj.positionX * this.baseData.widthPx
                //   obj.preY = obj.positionY * this.baseData.heightPx
                //   console.log("moving", obj.id, obj.preX, obj.preY);
                // }
                // obj.x = obj.preX
                // obj.y = obj.preY
                // // console.log(obj.id, obj.preX, obj.preY);
                // this.$nextTick(() => {
                //   obj.x = obj.positionX * this.baseData.widthPx
                //   obj.y = obj.positionY * this.baseData.heightPx
                // })
              }
            }
            item.index = index
            item.positionX = x
            item.positionY = y
            // TODO å‘送事件
          }
        }
      },
      movableStart (item) {
        // console.log("movableStart");
        this.lists.forEach(item => {
          item.zIndex = 1
          // #ifdef H5
          item.disabled = false
          // #endif
        })
        item.zIndex = 10
        item.moveEnd = false
        this.baseData.dragItem = item
        // #ifdef H5
        this.dragging =true
        this.timer = setTimeout(() => {
          item.opacity = 0.7
          item.scale = 1.1
          clearTimeout(this.timer)
          this.timer = null
        }, 200)
        // #endif
      },
      movableEnd (item) {
        if (!this.dragging) return
        // console.log("movableEnd");
        const index = this.lists.findIndex(obj => {
          return obj.id === item.id
        })
        if (!item.moving) {
          item.preX = item.positionX * this.baseData.widthPx
          item.preY = item.positionY * this.baseData.heightPx
        }
        item.x = item.preX
        item.y = item.preY
        item.offset = 0
        item.moveEnd = true
        item.moving = false
        item.disabled = true
        // console.log(item.x, item.y);
        // console.log(item.id, item.moving);
        // this.$set(this.lists, index, item)
        // this.lists[index] = item
        // console.log(this.lists[index]);
        this.lists.forEach(listValue => {
          listValue.moving = false
          listValue.disabled = true
        })
        this.$nextTick(() => {
          item.x = item.positionX * this.baseData.widthPx
          item.y = item.positionY * this.baseData.heightPx
          item.opacity = 1
          item.scale = 1
          this.baseData.dragItem = null
          this.dragging = false
          // console.log(item.x, item.y);
          this.$set(this.lists, index, item)
        })
        this.$emit('sort-list', this.sortList())
      },
      // æ›´æ–°å›¾ç‰‡ä½ç½®ä¿¡æ¯
      updateItemPosition(item, offset) {
        const index = this.lists.findIndex(obj => {
          return obj.id === item.id
        })
        item.index += offset
        item.offset = 0
        item.positionX = item.index % this.baseData.columns
        item.positionY = Math.floor(item.index / this.baseData.columns)
        if (!item.moving) {
          item.preX = item.positionX * this.baseData.widthPx
          item.preY = item.positionY * this.baseData.heightPx
        }
        item.x = item.preX
        item.y = item.preY
        // console.log("updateItemPosition", item.id, item.preX, item.preY);
        // this.$set(this.lists, index, item)
        this.$nextTick(() => {
          item.x = item.positionX * this.baseData.widthPx
          item.y = item.positionY * this.baseData.heightPx
          this.$set(this.lists, index, item)
        })
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-image-upload {
    position: relative;
    &__movable-area {
      width: 100%;
    }
    &__movable-view {
      overflow: hidden;
    }
    &__item {
      /* #ifndef APP-NVUE */
      display: flex;
      /* #endif */
      align-items: center;
      justify-content: center;
      width: 200rpx;
      height: 200rpx;
      background-color: transparent;
      position: relative;
      border-radius: 5rpx;
      overflow: hidden;
      &-preview {
        border: 1rpx solid $tn-space-color;
        &__delete {
          display: flex;
          align-items: center;
          justify-content: center;
          position: absolute;
          top: 0;
          right: 0;
          z-index: 10;
          border-top: 60rpx solid;
          border-left: 60rpx solid transparent;
          border-top-color: rgba(0,0,0,0.1);
          width: 0rpx;
          height: 0rpx;
          &--icon {
            position: absolute;
            top: -50rpx;
            right: 6rpx;
            color: #FFFFFF;
            font-size: 24rpx;
            line-height: 1;
          }
        }
        &__progress {
          position: absolute;
          width: auto;
          bottom: 0rpx;
          left: 0rpx;
          right: 0rpx;
          z-index: 9;
          /* #ifdef MP-WEIXIN */
          display: inline-flex;
          /* #endif */
        }
        &__error-btn {
          position: absolute;
          bottom: 0;
          left: 0;
          right: 0;
          background-color: rgba(0,0,0,0.5);
          color: #FFFFFF;
          font-size: 20rpx;
          padding: 8rpx 0;
          text-align: center;
          z-index: 9;
          line-height: 1;
        }
        &__image {
          display: block;
          width: 100%;
          height: 100%;
          // border-radius: 10rpx;
        }
      }
      &-add {
        flex-direction: column;
        color: $tn-content-color;
        font-size: 26rpx;
        &--icon {
          font-size: 40rpx;
        }
        &__tips {
          margin-top: 20rpx;
          line-height: 40rpx;
        }
      }
    }
    &__add {
      background-color: $tn-space-color;
      position: absolute;
      // border-radius: 10rpx;
      // margin: 10rpx;
      // margin-left: 0;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-image-upload/tn-image-upload.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,645 @@
<template>
  <view v-if="!disabled" class="tn-image-upload-class tn-image-upload">
    <block v-if="showUploadList">
      <view
        v-for="(item, index) in lists"
        :key="index"
        class="tn-image-upload__item tn-image-upload__item-preview"
        :style="{
          width: $t.string.getLengthUnitValue(width),
          height: $t.string.getLengthUnitValue(height)
        }"
      >
        <!-- åˆ é™¤æŒ‰é’® -->
        <view
          v-if="deleteable"
          class="tn-image-upload__item-preview__delete"
          @tap.stop="deleteItem(index)"
          :style="{
            borderTopColor: deleteBackgroundColor
          }"
        >
          <view
            class="tn-image-upload__item-preview__delete--icon"
            :class="[`tn-icon-${deleteIcon}`]"
            :style="{
              color: deleteColor
            }"
          ></view>
        </view>
        <!-- è¿›åº¦æ¡ -->
        <tn-line-progress
          v-if="showProgress && item.progress > 0 && !item.error"
          class="tn-image-upload__item-preview__progress"
          :percent="item.progress"
          :showPercent="false"
          :round="false"
          :height="8"
        ></tn-line-progress>
        <!-- é‡è¯•按钮 -->
        <view v-if="item.error" class="tn-image-upload__item-preview__error-btn" @tap.stop="retry(index)">点击重试</view>
        <!-- å›¾ç‰‡ä¿¡æ¯ -->
        <image
          class="tn-image-upload__item-preview__image"
          :src="item.url || item.path"
          :mode="imageMode"
          @tap.stop="doPreviewImage(item.url || item.path, index)"
        ></image>
      </view>
    </block>
    <!-- <view v-if="$slots.file || $slots.$file" style="width: 100%;">
    </view> -->
    <!-- è‡ªå®šä¹‰å›¾ç‰‡å±•示列表 -->
    <slot name="file" :file="lists"></slot>
    <!-- æ·»åŠ æŒ‰é’® -->
    <view v-if="maxCount > lists.length" class="tn-image-upload__add" :class="{'tn-image-upload__add--custom': customBtn}" @tap="selectFile">
      <!-- æ·»åŠ æŒ‰é’® -->
      <view
        v-if="!customBtn"
        class="tn-image-upload__item tn-image-upload__item-add"
        hover-class="tn-hover-class"
        hover-stay-time="150"
        :style="{
          width: $t.string.getLengthUnitValue(width),
          height: $t.string.getLengthUnitValue(height)
        }"
      >
        <view class="tn-image-upload__item-add--icon tn-icon-add"></view>
        <view class="tn-image-upload__item-add__tips">{{ uploadText }}</view>
      </view>
      <!-- è‡ªå®šä¹‰æ·»åŠ æŒ‰é’® -->
      <view>
        <slot name="addBtn"></slot>
      </view>
    </view>
  </view>
</template>
<script>
  export default {
    name: 'tn-image-upload',
    props: {
      // å·²ä¸Šä¼ çš„æ–‡ä»¶åˆ—表
      fileList: {
        type: Array,
        default() {
          return []
        }
      },
      // ä¸Šä¼ å›¾ç‰‡åœ°å€
      action: {
        type: String,
        default: ''
      },
      // ä¸Šä¼ æ–‡ä»¶çš„字段名称
      name: {
        type: String,
        default: 'file'
      },
      // å¤´éƒ¨ä¿¡æ¯
      header: {
        type: Object,
        default() {
          return {}
        }
      },
      // æºå¸¦çš„参数
      formData: {
        type: Object,
        default() {
          return {}
        }
      },
      // æ˜¯å¦ç¦ç”¨
      disabled: {
        type: Boolean,
        default: false
      },
      // æ˜¯å¦è‡ªåŠ¨ä¸Šä¼ 
      autoUpload: {
        type: Boolean,
        default: true
      },
      // æœ€å¤§ä¸Šä¼ æ•°é‡
      maxCount: {
        type: Number,
        default: 9
      },
      // æ˜¯å¦æ˜¾ç¤ºç»„件自带的图片预览
      showUploadList: {
        type: Boolean,
        default: true
      },
      // é¢„览上传图片的裁剪模式
      imageMode: {
        type: String,
        default: 'aspectFill'
      },
      // ç‚¹å‡»å›¾ç‰‡æ˜¯å¦å…¨å±é¢„览
      previewFullImage: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦æ˜¾ç¤ºè¿›åº¦æ¡
      showProgress: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦æ˜¾ç¤ºåˆ é™¤æŒ‰é’®
      deleteable: {
        type: Boolean,
        default: true
      },
      // åˆ é™¤æŒ‰é’®å›¾æ ‡
      deleteIcon: {
        type: String,
        default: 'close'
      },
      // åˆ é™¤æŒ‰é’®çš„背景颜色
      deleteBackgroundColor: {
        type: String,
        default: ''
      },
      // åˆ é™¤æŒ‰é’®çš„颜色
      deleteColor: {
        type: String,
        default: ''
      },
      // ä¸Šä¼ åŒºåŸŸæç¤ºæ–‡å­—
      uploadText: {
        type: String,
        default: '选择图片'
      },
      // æ˜¾ç¤ºtoast提示
      showTips: {
        type: Boolean,
        default: true
      },
      // è‡ªå®šä¹‰é€‰æ‹©å›¾æ ‡æŒ‰é’®
      customBtn: {
        type: Boolean,
        default: false
      },
      // é¢„览图片和选择图片区域的宽度
      width: {
        type: Number,
        default: 200
      },
      // é¢„览图片和选择图片区域的高度
      height: {
        type: Number,
        default: 200
      },
      // é€‰æ‹©å›¾ç‰‡çš„尺寸
      // å‚考上传文档 https://uniapp.dcloud.io/api/media/image
      sizeType: {
        type: Array,
        default() {
          return ['original', 'compressed']
        }
      },
      // å›¾ç‰‡æ¥æº
      sourceType: {
        type: Array,
        default() {
          return ['album', 'camera']
        }
      },
      // æ˜¯å¦å¯ä»¥å¤šé€‰
      multiple: {
        type: Boolean,
        default: true
      },
      // æ–‡ä»¶å¤§å°(byte)
      maxSize: {
        type: Number,
        default: 10 * 1024 * 1024
      },
      // å…è®¸ä¸Šä¼ çš„类型
      limitType: {
        type: Array,
        default() {
          return ['png','jpg','jpeg','webp','gif','image']
        }
      },
      // æ˜¯å¦è‡ªå®šè½¬æ¢ä¸ºjson
      toJson: {
        type: Boolean,
        default: true
      },
      // ä¸Šä¼ å‰é’©å­å‡½æ•°ï¼Œæ¯ä¸ªæ–‡ä»¶ä¸Šä¼ å‰éƒ½ä¼šæ‰§è¡Œ
      beforeUpload: {
        type: Function,
        default: null
      },
      // åˆ é™¤æ–‡ä»¶å‰é’©å­å‡½æ•°
      beforeRemove: {
        type: Function,
        default: null
      },
      index: {
        type: [Number, String],
        default: ''
      }
    },
    computed: {
    },
    data() {
      return {
        lists: [],
        uploading: false
      }
    },
    watch: {
      fileList: {
        handler(val) {
          val.map(value => {
            // é¦–先检查内部是否已经添加过这张图片,因为外部绑定了一个对象给fileList的话(对象引用),进行修改外部fileList时,
            // ä¼šè§¦å‘watch,导致重新把原来的图片再次添加到this.lists
            // æ•°ç»„çš„some方法意思是,只要数组元素有任意一个元素条件符合,就返回true,而另一个数组的every方法的意思是数组所有元素都符合条件才返回true
            let tmp = this.lists.some(listVal => {
              return listVal.url === value.url
            })
            // å¦‚果内部没有这张图片,则添加到内部
            !tmp && this.lists.push({ url: value.url, error: false, progress: 100 })
          })
        },
        immediate: true
      },
      lists(val) {
        this.$emit('on-list-change', val, this.index)
      }
    },
    methods: {
      // æ¸…除列表
      clear() {
        this.lists = []
      },
      // é‡æ–°ä¸Šä¼ é˜Ÿåˆ—中上传失败所有文件
      reUpload() {
        this.uploadFile()
      },
      // é€‰æ‹©å›¾ç‰‡
      selectFile() {
        if (this.disabled) return
        const {
          name = '',
          maxCount,
          multiple,
          maxSize,
          sizeType,
          lists,
          camera,
          compressed,
          sourceType
        } = this
        let chooseFile = null
        const newMaxCount = maxCount - lists.length
        // åªé€‰æ‹©å›¾ç‰‡çš„æ—¶å€™ä½¿ç”¨ chooseImage æ¥å®žçް
        chooseFile = new Promise((resolve, reject) => {
          uni.chooseImage({
            count: multiple ? (newMaxCount > 9 ? 9 : newMaxCount) : 1,
            sourceType,
            sizeType,
            success: resolve,
            fail: reject
          })
        })
        chooseFile.then(res => {
          let file = null
          let listOldLength = lists.length
          res.tempFiles.map((val, index) => {
            if (!this.checkFileExt(val)) return
            // æ˜¯å¦è¶…出最大限制数量
            if (!multiple && index >= 1) return
            if (val.size > maxSize) {
              this.$emit('on-oversize', val, lists, this.index)
              this.showToast('超出可允许文件大小')
            } else {
              if (maxCount <= lists.length) {
                this.$emit('on-exceed', val, lists, this.index)
                this.showToast('超出最大允许的文件数')
                return
              }
              lists.push({
                url: val.path,
                progress: 0,
                error: false,
                file: val
              })
            }
          })
          this.$emit('on-choose-complete', this.lists, this.index)
          if (this.autoUpload) this.uploadFile(listOldLength)
        }).catch(err => {
          this.$emit('on-choose-fail', err)
        })
      },
      // æç¤ºç”¨æˆ·ä¿¡æ¯
      showToast(message, force = false) {
        if (this.showTips || force) {
          this.$t.message.toast(message)
        }
      },
      // æ‰‹åŠ¨ä¸Šä¼ ï¼Œé€šè¿‡ref进行调用
      upload() {
        this.uploadFile()
      },
      // å¯¹å¤±è´¥å›¾ç‰‡è¿›è¡Œå†æ¬¡ä¸Šä¼ 
      retry(index) {
        this.lists[index].progress = 0
        this.lists[index].error = false
        this.lists[index].response = null
        this.$t.message.loading('重新上传')
        this.uploadFile(index)
      },
      // ä¸Šä¼ æ–‡ä»¶
      async uploadFile(index = 0) {
        if (this.disabled) return
        if (this.uploading) return
        // å…¨éƒ¨ä¸Šä¼ å®Œæˆ
        if (index >= this.lists.length) {
          this.$emit('on-uploaded', this.lists, this.index)
          return
        }
        // æ£€æŸ¥æ˜¯å¦å·²ç»å…¨éƒ¨ä¸Šä¼ æˆ–者上传中
        if (this.lists[index].progress === 100) {
          this.lists[index].uploadTask = null
          if (this.autoUpload) this.uploadFile(index + 1)
          return
        }
        // æ‰§è¡Œbefore-upload钩子
        if (this.beforeUpload && typeof(this.beforeUpload) === 'function') {
          // åœ¨å¾®ä¿¡ï¼Œæ”¯ä»˜å®ç­‰çŽ¯å¢ƒ(H5正常),会导致父组件定义的函数体中的this变成子组件的this
          // é€šè¿‡bind()方法,绑定父组件的this,让this的this为父组件的上下文
          // å› ä¸ºupload组件可能会被嵌套在其他组件内,比如tn-form,这时this.$parent其实为tn-form的this,
          // éžé¡µé¢çš„this,所以这里需要往上历遍,一直寻找到最顶端的$parent,这里用了this.$u.$parent.call(this)
          let beforeResponse = this.beforeUpload.bind(this.$t.$parent.call(this))(index, this.lists)
          // åˆ¤æ–­æ˜¯å¦è¿”回了Promise
          if (!!beforeResponse && typeof beforeResponse.then === 'function') {
            await beforeResponse.then(res => {
              // promise返回成功,不进行操作继续
            }).catch(err => {
              // è¿›å…¥catch回调的话,继续下一张
              return this.uploadFile(index + 1)
            })
          } else if (beforeResponse === false) {
            // å¦‚果返回flase,继续下一张图片上传
            return this.uploadFile(index + 1)
          } else {
            // ä¸ºtrue的情况,不进行操作
          }
        }
        // æ£€æŸ¥ä¸Šä¼ åœ°å€
        if (!this.action) {
          this.showToast('请配置上传地址', true)
          return
        }
        this.lists[index].error = false
        this.uploading = true
        // åˆ›å»ºä¸Šä¼ å¯¹è±¡
        const task = uni.uploadFile({
          url: this.action,
          filePath: this.lists[index].url,
          name: this.name,
          formData: this.formData,
          header: this.header,
          success: res => {
            // åˆ¤æ–­å•Šæ˜¯å¦ä¸ºjson字符串,将其转换为json格式
            let data = this.toJson && this.$t.test.jsonString(res.data) ? JSON.parse(res.data) : res.data
            if (![200, 201, 204].includes(res.statusCode)) {
              this.uploadError(index, data)
            } else {
              this.lists[index].response = data
              this.lists[index].progress = 100
              this.lists[index].error = false
              this.$emit('on-success', data, index, this.lists, this.index)
            }
          },
          fail: err => {
            this.uploadError(index, err)
          },
          complete: res => {
            this.$t.message.closeLoading()
            this.uploading = false
            this.uploadFile(index + 1)
            this.$emit('on-change', res, index, this.lists, this.index)
          }
        })
        this.lists[index].uploadTask = task
        task.onProgressUpdate(res => {
          if (res.progress > 0) {
            this.lists[index].progress = res.progress
            this.$emit('on-progress', res, index, this.lists, this.index)
          }
        })
      },
      // ä¸Šä¼ å¤±è´¥
      uploadError(index, err) {
        this.lists[index].progress = 0
        this.lists[index].error = true
        this.lists[index].response = null
        this.showToast('上传失败,请重试')
        this.$emit('on-error', err, index, this.lists, this.index)
      },
      // åˆ é™¤ä¸€ä¸ªå›¾ç‰‡
      deleteItem(index) {
        if (!this.deleteable) return
        this.$t.message.modal(
          '提示',
          '您确定要删除吗?',
          async () => {
            // å…ˆæ£€æŸ¥æ˜¯å¦æœ‰å®šä¹‰before-remove移除前钩子
            // æ‰§è¡Œbefore-remove钩子
            if (this.beforeRemove && typeof(this.beforeRemove) === 'function') {
              let beforeResponse = this.beforeRemove.bind(this.$t.$parent.call(this))(index, this.lists)
              // åˆ¤æ–­æ˜¯å¦è¿”回promise
              if (!!beforeResponse && typeof beforeResponse.then === 'function') {
                await beforeResponse.then(res => {
                  // promise返回成功不进行操作
                  this.handlerDeleteItem(index)
                }).catch(err => {
                  this.showToast('删除操作被中断')
                })
              } else if (beforeResponse === false) {
                this.showToast('删除操作被中断')
              } else {
                this.handlerDeleteItem(index)
              }
            } else {
              this.handlerDeleteItem(index)
            }
          }, true)
      },
      // ç§»é™¤æ–‡ä»¶æ“ä½œ
      handlerDeleteItem(index) {
        // å¦‚果文件正在上传中,终止上传任务
        if (this.lists[index].progress < 100 && this.lists[index].progress > 0) {
          typeof this.lists[index].uploadTask !== 'undefined' && this.lists[index].uploadTask.abort()
        }
        this.lists.splice(index, 1)
        this.$forceUpdate()
        this.$emit('on-remove', index, this.lists, this.index)
        this.showToast('删除成功')
      },
      // ç§»é™¤æ–‡ä»¶ï¼Œé€šè¿‡ref手动形式进行调用
      remove(index) {
        if (!this.deleteable) return
        // åˆ¤æ–­ç´¢å¼•合法
        if (index >= 0 && index < this.lists.length) {
          this.lists.splice(index, 1)
        }
      },
      // é¢„览图片
      doPreviewImage(url, index) {
        if (!this.previewFullImage) return
        const images = this.lists.map(item => item.url || item.path)
        uni.previewImage({
          urls: images,
          current: url,
          success: () => {
            this.$emit('on-preview', url, this.lists, this.index)
          },
          fail: () => {
            this.showToast('预览图片失败')
          }
        })
      },
      // æ£€æŸ¥æ–‡ä»¶åŽç¼€æ˜¯å¦åˆæ³•
      checkFileExt(file) {
        // æ˜¯å¦ä¸ºåˆæ³•后缀
        let noArrowExt = false
        // åŽç¼€å
        let fileExt = ''
        const reg = /.+\./
        // #ifdef H5
        fileExt = file.name.replace(reg, '').toLowerCase()
        // #endif
        // #ifndef H5
        fileExt = file.path.replace(reg, '').toLowerCase()
        // #endif
        noArrowExt = this.limitType.some(ext => {
          return ext.toLowerCase() === fileExt
        })
        if (!noArrowExt) this.showToast(`不支持${fileExt}格式的文件`)
        return noArrowExt
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-image-upload {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    &__item {
      /* #ifndef APP-NVUE */
      display: flex;
      /* #endif */
      align-items: center;
      justify-content: center;
      width: 200rpx;
      height: 200rpx;
      overflow: hidden;
      margin: 12rpx;
      margin-left: 0;
      background-color: $tn-font-holder-color;
      position: relative;
      border-radius: 10rpx;
      &-preview {
        border: 1rpx solid $tn-border-solid-color;
        &__delete {
          display: flex;
          align-items: center;
          justify-content: center;
          position: absolute;
          top: 0;
          right: 0;
          z-index: 10;
          border-top: 60rpx solid;
          border-left: 60rpx solid transparent;
          border-top-color: $tn-color-red;
          width: 0rpx;
          height: 0rpx;
          &--icon {
            position: absolute;
            top: -50rpx;
            right: 6rpx;
            color: #FFFFFF;
            font-size: 24rpx;
            line-height: 1;
          }
        }
        &__progress {
          position: absolute;
          width: auto;
          bottom: 0rpx;
          left: 0rpx;
          right: 0rpx;
          z-index: 9;
          /* #ifdef MP-WEIXIN */
          display: inline-flex;
          /* #endif */
        }
        &__error-btn {
          position: absolute;
          bottom: 0;
          left: 0;
          right: 0;
          background-color: $tn-color-red;
          color: #FFFFFF;
          font-size: 20rpx;
          padding: 8rpx 0;
          text-align: center;
          z-index: 9;
          line-height: 1;
        }
        &__image {
          display: block;
          width: 100%;
          height: 100%;
          border-radius: 10rpx;
        }
      }
      &-add {
        flex-direction: column;
        color: $tn-content-color;
        font-size: 26rpx;
        &--icon {
          font-size: 40rpx;
        }
        &__tips {
          margin-top: 20rpx;
          line-height: 40rpx;
        }
      }
    }
    &__add {
      width: auto;
      display: inline-block;
      &--custom {
        width: 100%;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-index-anchor/tn-index-anchor.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
<template>
  <!-- æ”¯ä»˜å®å°ç¨‹åºä½¿ç”¨_tGetRect()获取组件的根元素尺寸,所以在外面套一个"壳" -->
  <view>
    <view :id="elId" class="tn-index-anchor__wrap" :style="[wrapperStyle]">
      <view class="tn-index-anchor" :class="[active ? 'tn-index-anchor--active' : '']" :style="[customAnchorStyle]">
        <view v-if="useSlot">
          <slot></slot>
        </view>
        <block v-else>
          <text>{{ index }}</text>
        </block>
      </view>
    </view>
  </view>
</template>
<script>
  export default {
    name: 'tn-index-anchor',
    props: {
      // ä½¿ç”¨è‡ªå®šä¹‰å†…容
      useSlot: {
        type: Boolean,
        default: false
      },
      // ç´¢å¼•字符
      index: {
        type: String,
        default: ''
      },
      // è‡ªå®šä¹‰æ ·å¼
      customStyle: {
        type: Object,
        default() {
          return {}
        }
      }
    },
    computed: {
      customAnchorStyle() {
        return Object.assign(this.anchorStyle, this.customStyle)
      }
    },
    data() {
      return {
        elId: this.$t.uuid(),
        // å†…容的高度
        height: 0,
        // å†…容的top
        top: 0,
        // æ˜¯å¦è¢«æ¿€æ´»
        active: false,
        // æ ·å¼ï¼ˆçˆ¶ç»„件外部提供)
        wrapperStyle: {},
        anchorStyle: {}
      }
    },
    created() {
      this.parent = false
    },
    mounted() {
      this.parent = this.$t.$parent.call(this, 'tn-index-list')
      if (this.parent) {
        this.parent.childrens.push(this)
        this.parent.updateData()
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-index-anchor {
    width: 100%;
    box-sizing: border-box;
    padding: 8rpx 24rpx;
    color: $tn-font-color;
    font-size: 28rpx;
    font-weight: 500;
    line-height: 1.2;
    background-color: rgb(245, 245, 245);
    &--active {
      right: 0;
      left: 0;
      color: $tn-main-color;
      background-color: #FFFFFF;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-index-list/tn-index-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,361 @@
<template>
  <!-- æ”¯ä»˜å®å°ç¨‹åºä½¿ç”¨_tGetRect()获取组件的根元素尺寸,所以在外面套一个"壳" -->
  <view>
    <view class="tn-index-list-class tn-index-list">
      <slot></slot>
      <!-- ä¾§è¾¹æ  -->
      <view
        v-if="showSidebar"
        class="tn-index-list__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="tn-index-list__sidebar__item"
          :style="{
            zIndex: zIndex + 1,
            color: activeAnchorIndex === index ? activeColor : ''
          }"
        >
          {{ item }}
        </view>
      </view>
      <!-- é€‰ä¸­å¼¹å‡ºæ¡† -->
      <view
        v-if="touchMove && indexList[touchMoveIndex]"
        class="tn-index-list__alert"
        :style="{
          zIndex: selectAlertZIndex
        }"
      >
        <text>{{ indexList[touchMoveIndex] }}</text>
      </view>
    </view>
  </view>
</template>
<script>
  // ç”Ÿæˆ A-Z的字母列表
  let indexList = function() {
    let indexList = []
    let charCodeOfA = 'A'.charCodeAt(0)
    for (var i = 0; i < 26; i++) {
      indexList.push(String.fromCharCode(charCodeOfA + i))
    }
    return indexList
  }
  export default {
    name: 'tn-index-list',
    props: {
      // ç´¢å¼•列表
      indexList: {
        type: Array,
        default() {
          return indexList()
        }
      },
      // æ˜¯å¦è‡ªåЍ吏顶
      sticky: {
        type: Boolean,
        default: true
      },
      // è‡ªåŠ¨å¸é¡¶æ—¶è·ç¦»é¡¶éƒ¨çš„è·ç¦»ï¼Œå•ä½px
      stickyTop: {
        type: Number,
        default: 0
      },
      // è‡ªå®šä¹‰é¡¶æ çš„高度,单位px
      customBarHeight: {
        type: Number,
        default: 0
      },
      // å½“前滚动的高度
      // ç”±äºŽè‡ªå®šä¹‰ç»„件无法获取滚动高度,所以依赖传入
      scrollTop: {
        type: Number,
        default: 0
      },
      // é€‰ä¸­ç´¢å¼•时的颜色
      activeColor: {
        type: String,
        default: '#01BEFF'
      },
      // å¸é¡¶æ—¶çš„z-index
      zIndex: {
        type: Number,
        default: 0
      }
    },
    computed: {
      // é€‰ä¸­ç´¢å¼•列表弹出提示框的z-index
      selectAlertZIndex() {
        return this.$t.zIndex.toast
      },
      // å¸é¡¶çš„偏移高度
      stickyOffsetTop() {
        // #ifdef H5
        return this.stickyTop !== '' ? this.stickyTop : 44
        // #endif
        // #ifndef H5
        return this.stickyTop !== '' ? this.stickyTop : 0
        // #endif
      }
    },
    data() {
      return {
        // å½“前激活的列表锚点的序号
        activeAnchorIndex: 0,
        // æ˜¾ç¤ºä¾§è¾¹ç´¢å¼•栏
        showSidebar: true,
        // æ ‡è®°æ˜¯å¦å¼€å§‹è§¦æ‘¸ç§»åЍ
        touchMove: false,
        // å½“前触摸移动到对应索引的序号
        touchMoveIndex: 0,
        // æ»šåŠ¨åˆ°å¯¹åº”é”šç‚¹çš„åºå·
        scrollToAnchorIndex: 0,
        // ä¾§è¾¹æ çš„信息
        sidebar: {
          height: 0,
          top: 0
        },
        // å†…容区域高度
        height: 0,
        // å†…容区域top
        top: 0
      }
    },
    watch: {
      scrollTop() {
        this.updateData()
      }
    },
    created() {
      // åªèƒ½åœ¨created生命周期定义childrens,如果在data定义,会因为循环引用而报错
      this.childrens = []
    },
    methods: {
      // æ›´æ–°æ•°æ®
      updateData() {
        this.timer && clearTimeout(this.timer)
        this.timer = setTimeout(() => {
          this.showSidebar = !!this.childrens.length
          this.getRect().then(() => {
            this.onScroll()
          })
        }, 0)
      },
      // èŽ·å–å¯¹åº”çš„ä¿¡æ¯
      getRect() {
        return Promise.all([
          this.getAnchorRect(),
          this.getListRect(),
          this.getSidebarRect()
        ])
      },
      // èŽ·å–åˆ—è¡¨å†…å®¹å­å…ƒç´ ä¿¡æ¯
      getAnchorRect() {
        return Promise.all(this.childrens.map((child, index) => {
          child._tGetRect('.tn-index-anchor__wrap').then((rect) => {
            Object.assign(child, {
              height: rect.height,
              top: rect.top - this.customBarHeight
            })
          })
        }))
      },
      // èŽ·å–åˆ—è¡¨ä¿¡æ¯
      getListRect() {
        return this._tGetRect('.tn-index-list').then(rect => {
          Object.assign(this, {
            height: rect.height,
            top: rect.top + this.scrollTop
          })
        })
      },
      // èŽ·å–ä¾§è¾¹æ»šåŠ¨æ ä¿¡æ¯
      getSidebarRect() {
        return this._tGetRect('.tn-index-list__sidebar').then(rect => {
          this.sidebar = {
            height: rect.height,
            top: rect.top
          }
        })
      },
      // æ»šåŠ¨äº‹ä»¶
      onScroll() {
        const {
          childrens = []
        } = this
        if (!childrens.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 = childrens[active].top <= 0
          }
          childrens.forEach((item, index) => {
            if (index === active) {
              let wrapperStyle = ''
              let anchorStyle = {
                color: `${activeColor}`
              }
              if (isActiveAnchorSticky) {
                wrapperStyle = {
                  height: `${childrens[index].height}px`
                }
                anchorStyle = {
                  position: 'fixed',
                  top: `${stickyOffsetTop}px`,
                  zIndex: `${zIndex ? zIndex : this.$t.zIndex.indexListSticky}`,
                  color: `${activeColor}`
                }
              }
              item.active = true
              item.wrapperStyle = wrapperStyle
              item.anchorStyle = anchorStyle
            } else if (index === active - 1) {
              const currentAnchor = childrens[index]
              const currentOffsetTop = currentAnchor.top
              const targetOffsetTop = index === childrens.length - 1 ? this.top : childrens[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.$t.zIndex.indexListSticky}`,
                color: `${activeColor}`
              }
              item.active = false
              item.anchorStyle = anchorStyle
            } else {
              item.active = false
              item.wrapperStyle = ''
              item.anchorStyle = ''
            }
          })
        }
      },
      // è§¦æ‘¸ç§»åЍ
      onTouchMove(event) {
        this.touchMove = true
        const sidebarLength = this.childrens.length
        const touch = event.touches[0]
        const itemHeight = this.sidebar.height / sidebarLength
        let 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
      },
      // èŽ·å–å½“å‰çš„é”šç‚¹åºå·
      getActiveAnchorIndex() {
        const {
          childrens,
          sticky
        } = this
        for (let i = this.childrens.length - 1; i >= 0; i--) {
          const preAnchorHeight = i > 0 ? childrens[i - 1].height : 0
          const reachTop = sticky ? preAnchorHeight : 0
          if (reachTop >= childrens[i].top) {
            return i
          }
        }
        return -1
      },
      // æ»šåŠ¨åˆ°å¯¹åº”çš„é”šç‚¹
      scrollToAnchor(index) {
        if (this.scrollToAnchorIndex === index) {
          return
        }
        this.scrollToAnchorIndex = index
        const anchor = this.childrens.find(item => item.index === this.indexList[index])
        if (anchor) {
          const scrollTop = anchor.top + this.scrollTop
          this.$emit('select', {
            index: anchor.index,
            scrollTop: scrollTop
          })
          uni.pageScrollTo({
            duration:0,
            scrollTop: scrollTop
          })
        }
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-index-list {
    position: relative;
    &__sidebar {
      display: flex;
      flex-direction: column;
      position: fixed;
      top: 50%;
      right: 0;
      text-align: center;
      transform: translateY(-50%);
      user-select: none;
      z-index: 99;
      &__item {
        font-weight: 500;
        padding: 8rpx 18rpx;
        font-size: 22rpx;
        line-height: 1;
      }
    }
    &__alert {
      display: flex;
      flex-direction: row;
      position: fixed;
      width: 120rpx;
      height: 120rpx;
      top: 50%;
      right: 90rpx;
      align-items: center;
      justify-content: center;
      margin-top: -60rpx;
      border-radius: 24rpx;
      font-size: 50rpx;
      color: #FFFFFF;
      background-color: $tn-font-sub-color;
      padding: 0;
      z-index: 9999999;
      text {
        line-height: 50rpx;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-input/tn-input.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,427 @@
<template>
  <view
    class="tn-input-class tn-input"
    :class="{
      'tn-input--border': border,
      'tn-input--error': validateState
    }"
    :style="{
      padding: `0 ${border ? 20 : 0}rpx`,
      borderColor: borderColor,
      textAlign: inputAlign
    }"
    @tap.stop="inputClick"
  >
    <textarea
      v-if="type === 'textarea'"
      class="tn-input__input tn-input__textarea"
      :style="[inputStyle]"
      :value="defaultValue"
      :placeholder="placeholder"
      :placeholderStyle="placeholderStyle"
      :disabled="disabled || type === 'select'"
      :maxlength="maxLength"
      :fixed="fixed"
      :focus="focus"
      :autoHeight="autoHeight"
      :selectionStart="elSelectionStart"
      :selectionEnd="elSelectionEnd"
      :cursorSpacing="cursorSpacing"
      :showConfirmBar="showConfirmBar"
      @input="handleInput"
      @blur="handleBlur"
      @focus="onFocus"
      @confirm="onConfirm"
    />
    <input
      v-else
      class="tn-input__input"
      :type="type === 'password' ? 'text' : type"
      :style="[inputStyle]"
      :value="defaultValue"
      :password="type === 'password' && !showPassword"
      :placeholder="placeholder"
      :placeholderStyle="placeholderStyle"
      :disabled="disabled || type === 'select'"
      :maxlength="maxLength"
      :focus="focus"
      :confirmType="confirmType"
      :selectionStart="elSelectionStart"
      :selectionEnd="elSelectionEnd"
      :cursorSpacing="cursorSpacing"
      :showConfirmBar="showConfirmBar"
      @input="handleInput"
      @blur="handleBlur"
      @focus="onFocus"
      @confirm="onConfirm"
    />
    <!-- å³è¾¹çš„icon -->
    <view class="tn-input__right-icon tn-flex tn-flex-col-center">
      <!-- æ¸…除按钮 -->
      <view
        v-if="clearable && value !== '' && focused"
        class="tn-input__right-icon__item tn-input__right-icon__clear"
        @tap="onClear"
      >
        <view class="icon tn-icon-close"></view>
      </view>
      <view
        v-else-if="type === 'text' && !focused && showRightIcon && rightIcon !== ''"
        class="tn-input__right-icon__item tn-input__right-icon__clear"
      >
        <view class="icon" :class="[`tn-icon-${rightIcon}`]"></view>
      </view>
      <!-- æ˜¾ç¤ºå¯†ç æŒ‰é’® -->
      <view
        v-if="passwordIcon && type === 'password'"
        class="tn-input__right-icon__item tn-input__right-icon__clear"
        @tap="showPassword = !showPassword"
      >
        <view v-if="!showPassword" class="tn-icon-eye-hide"></view>
        <view v-else class="icon tn-icon-eye"></view>
      </view>
      <!-- å¯é€‰é¡¹ç®­å¤´ -->
      <view
        v-if="type === 'select'"
        class="tn-input__right-icon__item tn-input__right-icon__select"
        :class="{
          'tn-input__right-icon__select--reverse': selectOpen
        }"
      >
        <view class="icon tn-icon-up-triangle"></view>
      </view>
    </view>
  </view>
</template>
<script>
  import Emitter from '../../libs/utils/emitter.js'
  export default {
    mixins: [Emitter],
    name: 'tn-input',
    props: {
      value: {
        type: [String, Number],
        default: ''
      },
      // è¾“入框的类型
      type: {
        type: String,
        default: 'text'
      },
      // è¾“入框文字对齐方式
      inputAlign: {
        type: String,
        default: 'left'
      },
      // æ–‡æœ¬æ¡†ä¸ºç©ºæ—¶æ˜¾ç¤ºçš„信息
      placeholder: {
        type: String,
        default: ''
      },
      placeholderStyle: {
        type: String,
        default: 'color: #AAAAAA'
      },
      // æ˜¯å¦ç¦ç”¨è¾“入框
      disabled: {
        type: Boolean,
        default: false
      },
      // å¯è¾“入文字的最大长度
      maxLength: {
        type: Number,
        default: 255
      },
      // è¾“入框高度
      height: {
        type: Number,
        default: 0
      },
      // æ ¹æ®å†…容自动调整高度
      autoHeight: {
        type: Boolean,
        default: true
      },
      // é”®ç›˜å³ä¸‹è§’显示的文字,仅在text时生效
      confirmType: {
        type: String,
        default: 'done'
      },
      // è¾“入框自定义样式
      customStyle: {
        type: Object,
        default() {
          return {}
        }
      },
      // æ˜¯å¦å›ºå®šè¾“入框
      fixed: {
        type: Boolean,
        default: false
      },
      // æ˜¯å¦è‡ªåŠ¨èŽ·å–ç„¦ç‚¹
      focus: {
        type: Boolean,
        default: false
      },
      // å½“type为password时,是否显示右侧密码图标
      passwordIcon: {
        type: Boolean,
        default: true
      },
      // å½“type为 input或者textarea时是否显示边框
      border: {
        type: Boolean,
        default: false
      },
      // è¾¹æ¡†çš„颜色
      borderColor: {
        type: String,
        default: '#dcdfe6'
      },
      // å½“type为select时,旋转右侧图标,标记当时select是打开还是关闭
      selectOpen: {
        type: Boolean,
        default: false
      },
      // æ˜¯å¦å¯æ¸…空
      clearable: {
        type: Boolean,
        default: true
      },
      // å…‰æ ‡ä¸Žé”®ç›˜çš„距离
      cursorSpacing: {
        type: Number,
        default: 0
      },
      // selectionStart和selectionEnd需要搭配使用,自动聚焦时生效
      // å…‰æ ‡èµ·å§‹ä½ç½®
      selectionStart: {
        type: Number,
        default: -1
      },
      // å…‰æ ‡ç»“束位置
      selectionEnd: {
        type: Number,
        default: -1
      },
      // è‡ªåŠ¨åŽ»é™¤ä¸¤ç«¯ç©ºæ ¼
      trim: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦æ˜¾ç¤ºé”®ç›˜ä¸Šæ–¹çš„完成按钮
      showConfirmBar: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦åœ¨è¾“入框内最右边显示图标
      showRightIcon: {
        type: Boolean,
        default: false
      },
      // æœ€å³è¾¹å›¾æ ‡çš„名称
      rightIcon: {
        type: String,
        default: ''
      }
    },
    computed: {
      // è¾“入框样式
      inputStyle() {
        let style = {}
        // å¦‚果没有设置高度,根据不同的类型设置一个默认值
        style.minHeight = this.height ? this.height + 'rpx' :
          this.type === 'textarea' ? this.textareaHeight + 'rpx' : this.inputHeight + 'rpx'
        style = Object.assign(style, this.customStyle)
        return style
      },
      // å…‰æ ‡èµ·å§‹ä½ç½®
      elSelectionStart() {
        return String(this.selectionStart)
      },
      // å…‰æ ‡ç»“束位置
      elSelectionEnd() {
        return String(this.selectionEnd)
      }
    },
    data() {
      return {
        // é»˜è®¤å€¼
        defaultValue: this.value,
        // è¾“入框高度
        inputHeight: 70,
        // textarea的高度
        textareaHeight: 100,
        // æ ‡è®°éªŒè¯çš„状态
        validateState: false,
        // æ ‡è®°æ˜¯å¦èŽ·å–åˆ°ç„¦ç‚¹
        focused: false,
        // æ˜¯å¦é¢„览密码
        showPassword: false,
        // ç”¨äºŽå¤´æ¡å°ç¨‹åºï¼Œåˆ¤æ–­@input中,前后的值是否发生了变化,因为头条中文下,按下键没有输入内容,也会触发@input事件
        lastValue: '',
      }
    },
    watch: {
      value(newVal, oldVal) {
        this.defaultValue = newVal
        // å½“值发生变化时,并且type为select时,不会触发input事件
        // æ¨¡æ‹Ÿinput事件
        if (newVal !== oldVal && this.type === 'select') {
          this.handleInput({
            detail: {
              value: newVal
            }
          })
        }
      }
    },
    created() {
      // ç›‘听form-item发出的错误事件,将输入框变成红色
      this.$on("on-form-item-error", this.onFormItemError)
    },
    methods: {
      /**
       * input事件
       */
      handleInput(event) {
        let value = event.detail.value
        // æ˜¯å¦éœ€è¦åŽ»æŽ‰ç©ºæ ¼
        if (this.trim) value = this.$t.string.trim(value)
        // åŽŸç”Ÿäº‹ä»¶
        this.$emit('input', value)
        // model赋值
        this.defaultValue = value
        // è¿‡ä¸€ä¸ªç”Ÿå‘½å‘¨æœŸå†å‘送事件给tn-form-item,否则this.$emit('input')更新了父组件的值,但是微信小程序上
        // å°šæœªæ›´æ–°åˆ°tn-form-item,导致获取的值为空,从而校验混论
        // è¿™é‡Œä¸èƒ½å»¶æ—¶æ—¶é—´å¤ªçŸ­ï¼Œæˆ–者使用this.$nextTick,否则在头条上,会造成混乱
        setTimeout(() => {
          // å¤´æ¡å°ç¨‹åºç”±äºŽè‡ªèº«bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
          // #ifdef MP-TOUTIAO
          if (this.$t.string.trim(value) === this.lastValue) return
          this.lastValue = value
          // #endif
          // å‘送当前的值到form-item进行校验
          this.dispatch('tn-form-item','on-form-change', value)
        }, 40)
      },
      /**
       * blur事件
       */
      handleBlur(event) {
        let value = event.detail.value
        // ç”±äºŽç‚¹å‡»æ¸…除图标也会触发blur事件,导致图标消失从而无法点击
        setTimeout(() => {
          this.focused = false
        }, 100)
        // åŽŸç”Ÿäº‹ä»¶
        this.$emit('blur', value)
        // è¿‡ä¸€ä¸ªç”Ÿå‘½å‘¨æœŸå†å‘送事件给tn-form-item,否则this.$emit('blur')更新了父组件的值,但是微信小程序上
        // å°šæœªæ›´æ–°åˆ°tn-form-item,导致获取的值为空,从而校验混论
        // è¿™é‡Œä¸èƒ½å»¶æ—¶æ—¶é—´å¤ªçŸ­ï¼Œæˆ–者使用this.$nextTick,否则在头条上,会造成混乱
        setTimeout(() => {
          // å¤´æ¡å°ç¨‹åºç”±äºŽè‡ªèº«bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
          // #ifdef MP-TOUTIAO
          if (this.$t.string.trim(value) === this.lastValue) return
          this.lastValue = value
          // #endif
          // å‘送当前的值到form-item进行校验
          this.dispatch('tn-form-item','on-form-blur', value)
        }, 40)
      },
      // å¤„理校验错误
      onFormItemError(status) {
        this.validateState = status
      },
      // èšç„¦äº‹ä»¶
      onFocus(event) {
        this.focused = true
        this.$emit('focus')
      },
      // ç‚¹å‡»ç¡®è®¤æŒ‰é’®äº‹ä»¶
      onConfirm(event) {
        this.$emit('confirm', event.detail.value)
      },
      // æ¸…除事件
      onClear(event) {
        this.$emit('input', '')
      },
      // ç‚¹å‡»äº‹ä»¶
      inputClick() {
        this.$emit('click')
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-input {
    display: flex;
    flex-direction: row;
    position: relative;
    flex: 1;
    &__input {
      font-size: 28rpx;
      color: $tn-font-color;
      flex: 1;
    }
    &__textarea {
      width: auto;
      font-size: 28rpx;
      color: $tn-font-color;
      padding: 10rpx 0;
      line-height: normal;
      flex: 1;
    }
    &--border {
      border-radius: 6rpx;
      border: 2rpx solid $tn-border-solid-color;
    }
    &--error {
      border-color: $tn-color-red !important;
    }
    &__right-icon {
      line-height: 1;
      .icon {
        color: $tn-font-sub-color;
      }
      &__item {
        margin-left: 10rpx;
      }
      &__clear {
        .icon {
          font-size: 32rpx;
        }
      }
      &__select {
        transition: transform .4s;
        .icon {
          font-size: 26rpx;
        }
        &--reverse {
          transform: rotate(-180deg);
        }
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-keyboard/tn-keyboard.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,220 @@
<template>
  <view v-if="value" class="tn-keyboard-class tn-keyboard">
    <tn-popup
      v-model="value"
      mode="bottom"
      :popup="false"
      length="auto"
      :mask="mask"
      :maskCloseable="maskCloseable"
      :safeAreaInsetBottom="safeAreaInsetBottom"
      :zIndex="elZIndex"
      @close="popupClose"
    >
      <view>
        <slot></slot>
      </view>
      <!-- æç¤ºä¿¡æ¯ -->
      <view v-if="tooltip" class="tn-keyboard__tooltip">
        <view
          v-if="cancelBtn"
          class="tn-keyboard__tooltip__item tn-keyboard__tooltip__cancel"
          hover-class="tn-keyboard__tooltip__cancel--hover"
          :hover-stay-time="150"
          @tap="onCancel"
        >
          {{ cancelBtn ? cancelText : ''}}
        </view>
        <view v-if="showTips" class="tn-keyboard__tooltip__item tn-keyboard__tooltip__tips">
          {{ tips ? tips : mode === 'number' ? '数字键盘' : mode === 'card' ? '身份证键盘' : '车牌号码键盘'}}
        </view>
        <view
          v-if="confirmBtn"
          class="tn-keyboard__tooltip__item tn-keyboard__tooltip__confirm"
          hover-class="tn-keybord__tooltip__confirm--hover"
          :hover-stay-time="150"
          @tap="onConfirm"
        >
          {{ confirmBtn ? confirmText : ''}}
        </view>
      </view>
      <!-- é”®ç›˜å†…容 -->
      <block v-if="mode === 'number' || mode === 'card'">
        <tn-number-keyboard :mode="mode" :dotEnabled="dotEnabled" :randomEnabled="randomEnabled" @change="change" @backspace="backspaceClick"></tn-number-keyboard>
      </block>
      <block v-if="mode === 'car'">
        <tn-car-keyboard :randomEnabled="randomEnabled" :switchEnMode="switchEnMode" @change="change" @backspace="backspaceClick"></tn-car-keyboard>
      </block>
    </tn-popup>
  </view>
</template>
<script>
  export default {
    name: 'tn-keyboard',
    props: {
      // æŽ§åˆ¶é”®ç›˜å¼¹å‡ºæ”¶å›ž
      value: {
        type: Boolean,
        default: false
      },
      // é”®ç›˜ç±»åž‹
      // number - æ•°å­—键盘 card - èº«ä»½è¯é”®ç›˜ car - è½¦ç‰Œå·ç 
      mode: {
        type: String,
        default: 'number'
      },
      // å½“mode = number时,是否显示'.'符号
      dotEnabled: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦æ‰“乱顺序
      randomEnabled: {
        type: Boolean,
        default: false
      },
      // å½“mode = car,设置键盘中英文状态
      switchEnMode: {
        type: Boolean,
        default: false
      },
      // æ˜¾ç¤ºé¡¶éƒ¨å·¥å…·æ¡
      tooltip: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦æ˜¾ç¤ºæç¤ºä¿¡æ¯
      showTips: {
        type: Boolean,
        default: true
      },
      // æç¤ºæ–‡å­—
      tips: {
        type: String,
        default: ''
      },
      // æ˜¯å¦æ˜¾ç¤ºå–消按钮
      cancelBtn: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦æ˜¾ç¤ºç¡®è®¤æŒ‰é’®
      confirmBtn: {
        type: Boolean,
        default: true
      },
      // å–消按钮文字
      cancelText: {
        type: String,
        default: '取消'
      },
      // ç¡®è®¤æŒ‰é’®æ–‡å­—
      confirmText: {
        type: String,
        default: '确认'
      },
      // æ˜¯å¦å¼€å¯åº•部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
      safeAreaInsetBottom: {
          type: Boolean,
          default: false
      },
      // æ˜¯å¦å¯ä»¥é€šè¿‡ç‚¹å‡»é®ç½©è¿›è¡Œå…³é—­
      maskCloseable: {
          type: Boolean,
          default: true
      },
      // æ˜¯å¦æ˜¾ç¤ºé®ç½©
      mask: {
        type: Boolean,
        default: true
      },
      // z-index
      zIndex: {
        type: Number,
        default: 0
      }
    },
    computed: {
      elZIndex() {
        return this.zIndex ? this.zIndex : this.$t.zIndex.popup
      }
    },
    data() {
      return {
      }
    },
    methods: {
      change(e) {
        this.$emit('change', e)
      },
      // å…³é—­é”®ç›˜
      popupClose() {
        // ä¿®æ”¹value的值
        this.$emit('input', false)
      },
      // è¾“入完成
      onConfirm() {
        this.popupClose()
        this.$emit('confirm')
      },
      // è¾“入取消
      onCancel() {
        this.popupClose()
        this.$emit('cancel')
      },
      // ç‚¹å‡»é€€æ ¼
      backspaceClick() {
        this.$emit('backspace')
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-keyboard {
    position: relative;
    &__tooltip {
      display: flex;
      flex-direction: row;
      justify-content: space-between;
      &__item {
        color: $tn-font-color;
        flex: 0 0 33.3333333333%;
        text-align: center;
        font-size: 28rpx;
        padding: 20rpx 10rpx;
      }
      &__cancel {
        text-align: left;
        flex-grow: 1;
        flex-wrap: 0;
        padding-left: 40rpx;
        color: $tn-content-color;
        &--hover {
          color: $tn-font-color;
        }
      }
      &__confirm {
        text-align: right;
        flex-grow: 1;
        flex-wrap: 0;
        padding-right: 40rpx;
        color: $tn-main-color;
        &--hover {
          color: $tn-color-blue;
        }
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-landscape/tn-landscape.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,225 @@
<template>
  <view class="tn-landscape-class tn-landscape">
    <view v-if="showValue" class="tn-landscape__container" :style="[containerStyle]">
      <slot></slot>
      <view
        v-if="closeBtn"
        class="tn-landscape__icon tn-icon-close-fill"
        :class="[{
          'tn-landscape__icon--left-top': closePosition === 'leftTop',
          'tn-landscape__icon--right-top': closePosition === 'rightTop',
          'tn-landscape__icon--bottom': closePosition === 'bottom'
        }]"
        :style="[closeBtnStyle]"
        @tap="close"
      ></view>
    </view>
    <view
      v-if="mask"
      class="tn-landscape__mask"
      :class="[{'tn-landscape__mask--show': showValue}]"
      :style="[maskStyle]"
      @tap="closeMask"
    ></view>
  </view>
</template>
<script>
  export default {
    name: 'tn-landscape',
    props: {
      // æ˜¾ç¤º
      show: {
        type: Boolean,
        default: false
      },
      // æ˜¾ç¤ºå…³é—­å›¾æ ‡
      closeBtn: {
        type: Boolean,
        default: true
      },
      // å…³é—­å›¾æ ‡é¢œè‰²
      closeColor: {
        type: String,
        default: ''
      },
      // å…³é—­å›¾æ ‡å¤§å°ï¼Œå•位rpx
      closeSize: {
        type: Number,
        default: 0
      },
      // å…³é—­å›¾æ ‡ä½ç½®
      // leftTop -> å·¦ä¸Šè§’ rightTop -> å³ä¸Šè§’ bottom -> åº•部
      closePosition: {
        type: String,
        default: 'rightTop'
      },
      // å…³é—­å›¾æ ‡top值,单位rpx
      // å½“关闭图标在leftTop或者rightTop时生效
      closeTop: {
        type: Number,
        default: 0
      },
      // å…³é—­å›¾æ ‡right值,单位rpx
      // å½“关闭图标在RightTop时生效
      closeRight: {
        type: Number,
        default: 0
      },
      // å…³é—­å›¾æ ‡bottom值,单位rpx
      // å½“关闭图标在bottom时生效
      closeBottom: {
        type: Number,
        default: 0
      },
      // å…³é—­å›¾æ ‡left值,单位rpx
      // å½“关闭图标在leftTop时生效
      closeLeft: {
        type: Number,
        default: 0
      },
      // æ˜¾ç¤ºé®ç½©
      mask: {
        type: Boolean,
        default: true
      },
      // ç‚¹å‡»é®ç½©å¯ä»¥å…³é—­
      maskCloseable: {
        type: Boolean,
        default: true
      },
      // zIndex
      zIndex: {
        type: Number,
        default: 0
      }
    },
    computed: {
      containerStyle() {
        let style = {}
        style.zIndex = this.zIndex ? this.zIndex : this.$t.zIndex.landsacpe
        return style
      },
      closeBtnStyle() {
        let style = {}
        if (this.closePosition === 'leftTop') {
          if (this.closeTop) {
            style.top = this.closeTop + 'rpx'
          }
          if (this.closeLeft) {
            style.left = this.closeLeft + 'rpx'
          }
        } else if (this.closePosition === 'rightTop') {
          if (this.closeTop) {
            style.top = this.closeTop + 'rpx'
          }
          if (this.closeRight) {
            style.right = this.closeRight + 'rpx'
          }
        } else if (this.closePosition === 'bottom') {
          if (this.closeBottom) {
            style.bottom = this.closeBottom + 'rpx'
          }
        }
        if (this.closeSize) {
          style.fontSize = this.closeSize + 'rpx'
        }
        if (this.closeColor) {
          style.color = this.closeColor
        }
        return style
      },
      maskStyle() {
        let style = {}
        style.zIndex = this.zIndex ? this.zIndex - 1 : this.$t.zIndex.landsacpe - 1
        return style
      }
    },
    watch: {
      show: {
        handler(val) {
          this.showValue = val
        },
        immediate: true
      }
    },
    data() {
      return {
        showValue: false
      }
    },
    methods: {
      // å…³é—­åŽ‹å±çª—
      close() {
        this.showValue = false
        this.$emit('close')
      },
      // ç‚¹å‡»é®ç½©å…³é—­
      closeMask() {
        if (!this.maskCloseable) return
        this.close()
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-landscape {
    width: 100%;
    overflow: hidden;
    &__container {
      max-width: 100%;
      position: fixed;
      display: inline-flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
    }
    &__icon {
      position: absolute;
      text-align: center;
      font-size: 50rpx;
      color: #FFFFFF;
      &--left-top {
        top: -40rpx;
        left: 20rpx;
      }
      &--right-top {
        top: -40rpx;
        right: 40rpx;
      }
      &--bottom {
        left: 50%;
        bottom: -40rpx;
        transform: translateX(-50%);
      }
    }
    &__mask {
      position: fixed;
      width: 100%;
      height: 100%;
      background-color: $tn-mask-bg-color;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      opacity: 0;
      transform: scale3d(1, 1, 0);
      transition: all 0.25s ease-in;
      &--show {
        opacity: 1 !important;
        transform: scale3d(1, 1, 1);
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-lazy-load/tn-lazy-load.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,254 @@
<template>
  <view class="tn-lazy-load-class tn-lazy-load">
    <view
      class="tn-lazy-load__item"
      :class="[`tn-lazy-load__item--${elIndex}`]"
      :style="[lazyLoadItemStyle]"
    >
      <view class="tn-lazy-load__item__content">
        <image
          v-if="!error"
          class="tn-lazy-load__item__image"
          :style="[imageStyle]"
          :src="show ? image : loadingImg"
          :mode="imgMode"
          @load="handleImgLoaded"
          @error="handleImgError"
          @tap="handleImgClick"
        ></image>
        <image
          v-else
          class="tn-lazy-load__item__image tn-lazy-load__item__image--error"
          :style="[imageStyle]"
          :src="errorImg"
          :mode="imgMode"
          @load="handleErrorImgLoaded"
          @tap="handleImgClick"
        ></image>
      </view>
    </view>
  </view>
</template>
<script>
  export default {
    name: 'tn-lazy-load',
    props: {
      // ç»„件标识
      index: {
        type: [String, Number],
        default: ''
      },
      // å¾…显示的图片地址
      image: {
        type: String,
        default: ''
      },
      // å›¾ç‰‡è£å‰ªæ¨¡å¼
      imgMode: {
        type: String,
        default: 'scaleToFill'
      },
      // å ä½å›¾ç‰‡è·¯å¾„
      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
      },
      // æ˜¯å¦å¼€å¯è¿‡æ¸¡æ•ˆæžœ
      isEffect: {
        type: Boolean,
        default: true
      },
      // åŠ¨ç”»è¿‡æ¸¡æ—¶é—´
      duration: {
        type: [String, Number],
        default: 500
      },
      // æ¸¡æ•ˆæžœçš„速度曲线,各个之间差别不大,因为这是淡入淡出,且时间很短,不是那些变形或者移动的情况,会明显
      // linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);
      effect: {
          type: String,
          default: 'ease-in-out'
      },
      // å›¾ç‰‡é«˜åº¦ï¼Œå•位rpx
      height: {
        type: [String, Number],
        default: 450
      },
      // å›¾ç‰‡åœ†è§’
      borderRadius: {
        type: String,
        default: ''
      }
    },
    computed: {
      thresholdValue() {
        // å…ˆå–绝对值,因为threshold可能是负数,最后根据this.threshold是正数或者负数,重新还原
        let threshold = uni.upx2px(Math.abs(this.threshold))
        return this.threshold < 0 ? -threshold : threshold
      },
      lazyLoadItemStyle() {
        let style = {}
        style.opacity = Number(this.opacity)
        if (this.borderRadius) {
          style.borderRadius = this.borderRadius
        }
        // å› ä¸ºtime值需要改变,所以不直接用duration值(不能改变父组件prop传过来的值)
        style.transition = `opacity ${this.time / 1000}s ${this.effect}`
        style.height = this.$t.string.getLengthUnitValue(this.height)
        return style
      },
      imageStyle() {
        let style = {}
        if (typeof this.height === 'string' && this.height.indexOf('%') === -1) {
          style.height = this.$t.string.getLengthUnitValue(this.height)
        }
        return style
      }
    },
    watch: {
      show(val) {
        // å¦‚果不开启过渡效果直接返回
        if (!this.effect) return
        this.time = 0
        // åŽŸæ¥opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的白色),再改成1,是为了获得过渡效果
        this.opacity = 0
        setTimeout(() => {
          this.time = this.duration
          this.opacity = 1
        }, 30)
      },
      image(val) {
        // ä¿®æ”¹å›¾ç‰‡åŽé‡ç½®éƒ¨åˆ†å˜é‡
        if (!val) {
          // å¦‚果传入null或者'',或者undefined,标记为错误状态
          this.error = true
        } else {
          this.init()
          this.error = false
        }
      }
    },
    data() {
      return {
        elIndex: this.$t.uuid(),
        // æ˜¾ç¤ºå›¾ç‰‡
        show: false,
        // å›¾ç‰‡é€æ˜Žåº¦
        opacity: 1,
        // åŠ¨ç”»æ—¶é—´
        time: this.duration,
        // æ‡’加载状态
        // loadlazy-懒加载中状态,loading-图片正在加载,loaded-图片加加载完成
        loadStatus: '',
        // å›¾ç‰‡åŠ è½½å¤±è´¥
        error: false
      }
    },
    created() {
      // ç”±äºŽä¸€äº›ç‰¹æ®ŠåŽŸå› ï¼Œä¸èƒ½å°†æ­¤å˜é‡æ”¾åˆ°data中定义
      this.observer = {}
      this.observerName = 'lazyLoadContentObserver'
    },
    mounted() {
      // åœ¨éœ€è¦ç”¨åˆ°æ‡’加载的页面,在触发底部的时候触发tOnLazyLoadReachBottom事件,保证所有图片进行加载
      this.$nextTick(() => {
        uni.$once('tOnLazyLoadReachBottom', () => {
          if (!this.show) this.show = true
        })
      })
      // mounted的时候,不一定挂载了这个元素,延时30ms,否则会报错或者不报错,但是也没有效果
      setTimeout(() => {
        this.disconnectObserver(this.observerName)
        const contentObserver = uni.createIntersectionObserver(this)
        contentObserver.relativeToViewport({
          bottom: this.thresholdValue
        }).observe(`.tn-lazy-load__item--${this.elIndex}`, (res) => {
          if (res.intersectionRatio > 0) {
            // æ‡’加载状态改变
            this.show = true
            // å¦‚果图片已经加载,去掉监听,减少性能消耗
            this.disconnectObserver(this.observerName)
          }
        })
        this[this.observerName] = contentObserver
      }, 50)
    },
    methods: {
      // åˆå§‹åŒ–
      init() {
        this.error = false
        this.loadStatus = ''
      },
      // å¤„理图片点击事件
      handleImgClick() {
        let whichImg = ''
        // å¦‚æžœshow为false,则表示图片还没有开始加载,点击的是最开始占位图
        if (this.show === false) whichImg = 'lazyImg'
        // å¦‚æžœerror为true,则表示图片加载失败,点击的是错误占位图
        else if (this.error === true) whichImg = 'errorImg'
        // ç‚¹å‡»äº†æ­£å¸¸çš„图片
        else whichImg = 'realImg'
        this.$emit('click', {
          index: this.index,
          whichImg: whichImg
        })
      },
      // å¤„理图片加载完成事件,通过show来区分是占位图触发还是加载真正的图片触发
      handleImgLoaded() {
        if (this.loadStatus = '') {
          // å ä½å›¾åŠ è½½å®Œæˆ
          this.loadStatus = 'lazyed'
        }
        else if (this.loadStatus == 'lazyed') {
          // çœŸæ­£çš„图片加载完成
          this.loadStatus = 'loaded'
          this.$emit('loaded', this.index)
        }
      },
      // å¤„理错误图片加载完成
      handleErrorImgLoaded() {
        this.$emit('error', this.index)
      },
      // å¤„理图片加载失败
      handleImgError() {
        this.error = true
      },
      disconnectObserver(observerName) {
        const observer = this[observerName]
        observer && observer.disconnect()
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-lazy-load {
    &__item {
      background-color: $tn-bg-gray-color;
      overflow: hidden;
      &__image {
        // è§£å†³çˆ¶å®¹å™¨ä¼šå¤šå‡º3px的问题
        display: block;
        width: 100%;
        // éª—系统开启硬件加速
        transform: transition3d(0, 0, 0);
        // é˜²æ­¢å›¾ç‰‡åŠ è½½â€œé—ªä¸€ä¸‹â€
        will-change: transform;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-line-progress/tn-line-progress.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,143 @@
<template>
  <view
    class="tn-line-progress-class tn-line-progress"
    :style="[progressStyle]"
  >
    <view
      class="tn-line-progress--active"
      :class="[
        $t.color.getBackgroundColorInternalClass(activeColor),
        striped ? stripedAnimation ? 'tn-line-progress__striped tn-line-progress__striped--active' : 'tn-line-progress__striped' : '',
      ]"
      :style="[progressActiveStyle]"
    >
      <slot v-if="$slots.default || $slots.$default"></slot>
      <block v-else-if="showPercent">{{ percent + '%' }}</block>
    </view>
  </view>
</template>
<script>
  export default {
    name: 'tn-line-progress',
    props: {
      // è¿›åº¦ï¼ˆç™¾åˆ†æ¯”)
      percent: {
        type: Number,
        default: 0,
        validator: val => {
          return val >= 0 && val <= 100
        }
      },
      // é«˜åº¦
      height: {
        type: Number,
        default: 0
      },
      // æ˜¯å¦æ˜¾ç¤ºä¸ºåœ†è§’
      round: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦æ˜¾ç¤ºæ¡çº¹
      striped: {
        type: Boolean,
        default: false
      },
      // æ¡çº¹æ˜¯å¦è¿åЍ
      stripedAnimation: {
        type: Boolean,
        default: true
      },
      // æ¿€æ´»éƒ¨åˆ†é¢œè‰²
      activeColor: {
        type: String,
        default: ''
      },
      // éžæ¿€æ´»éƒ¨åˆ†é¢œè‰²
      inactiveColor: {
        type: String,
        default: ''
      },
      // æ˜¯å¦æ˜¾ç¤ºè¿›åº¦æ¡å†…部百分比值
      showPercent: {
        type: Boolean,
        default: false
      }
    },
    computed: {
      progressStyle() {
        let style = {}
        style.borderRadius = this.round ? '100rpx' : 0
        if (this.height) {
          style.height = this.$t.string.getLengthUnitValue(this.height)
        }
        if (this.inactiveColor) {
          style.backgroundColor = this.inactiveColor
        }
        return style
      },
      progressActiveStyle() {
        let style = {}
        style.width = this.percent + '%'
        if (this.$t.color.getBackgroundColorStyle(this.activeColor)) {
          style.backgroundColor = this.$t.color.getBackgroundColorStyle(this.activeColor)
        }
        return style
      }
    },
    data() {
      return {
      }
    },
  }
</script>
<style lang="scss" scoped>
  .tn-line-progress {
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
    align-items: center;
    width: 100%;
    height: 28rpx;
    overflow: hidden;
    border-radius: 100rpx;
    background-color: $tn-progress-bg-color;
    &--active {
      display: flex;
      flex-direction: row;
      align-items: center;
      justify-items: flex-end;
      justify-content: space-around;
      width: 0;
      height: 100%;
      font-size: 20rpx;
      color: #FFFFFF;
      background-color: $tn-main-color;
      transition: all 0.3s ease;
    }
    &__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: 80rpx 80rpx;
      &--active {
        animation: progress-striped 2s linear infinite;
      }
    }
  }
  @keyframes progress-striped {
    0% {
      background-position: 0 0;
    }
    100% {
      background-position: 80rpx 0;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-list-cell/tn-list-cell.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,209 @@
<template>
  <view
    class="tn-list-cell-class tn-list-cell"
    :class="[
      backgroundColorClass,
      fontColorClass,
      cellClass
    ]"
    :hover-class="hover ? 'tn-hover' : ''"
    :hover-stay-time="150"
    :style="[cellStyle]"
    @tap="handleClick"
  >
    <slot></slot>
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    mixins: [ componentsColorMixin ],
    name: 'tn-list-cell',
    props: {
      // åˆ—表序号
      index: {
        type: [Number, String],
        default: '0'
      },
      // å†…边距
      padding: {
        type: String,
        default: ''
      },
      // æ˜¯å¦æœ‰ç®­å¤´
      arrow: {
        type: Boolean,
        default: false
      },
      //箭头是否有偏移距离
      arrowRight: {
          type: Boolean,
          default: true
      },
      // æ˜¯å¦æœ‰ç‚¹å‡»æ•ˆæžœ
      hover: {
        type: Boolean,
        default: false
      },
      // éšè—çº¿æ¡
      unlined: {
        type: Boolean,
        default: false
      },
      //线条是否有左偏移距离
      lineLeft: {
          type: Boolean,
          default: true
      },
      //线条是否有右偏移距离
      lineRight: {
          type: Boolean,
          default: true
      },
      //是否加圆角
      radius: {
          type: Boolean,
          default: false
      }
    },
    computed: {
      cellClass() {
        let clazz = ''
        if (this.arrow) {
          clazz += ' tn-list-cell--arrow'
          if (!this.arrowRight) {
            clazz += ' tn-list-cell--arrow--none-right'
          }
        }
        if (this.unlined) {
          clazz += ' tn-list-cell--unlined'
        } else {
          if (this.lineLeft) {
            clazz += ' tn-list-cell--line-left'
          }
          if (this.lineRight) {
            clazz += ' tn-list-cell--line-right'
          }
        }
        if (this.radius) {
          clazz += ' tn-list-cell--radius'
        }
        return clazz
      },
      cellStyle() {
        let style = {}
        if (this.backgroundColorStyle) {
          style.backgroundColor = this.backgroundColorStyle
        }
        if (this.fontColorStyle) {
          style.color = this.fontColorStyle
        }
        if (this.fontSize) {
          style.fontSize = this.fontSize + this.fontUnit
        }
        if (this.padding) {
          style.padding = this.padding
        }
        return style
      },
    },
    data() {
      return {
      }
    },
    methods: {
      // å¤„理点击事件
      handleClick() {
        this.$emit("click", {
          index: Number(this.index)
        })
        this.$emit("tap", {
          index: Number(this.index)
        })
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-list-cell {
    position: relative;
    width: 100%;
    box-sizing: border-box;
    background-color: #FFFFFF;
    color: $tn-font-color;
    font-size: 28rpx;
    padding: 26rpx 30rpx;
    &--radius {
      border-radius: 12rpx;
      overflow: hidden;
    }
    &--arrow {
      &::before {
        content: " ";
        position: absolute;
        top: 50%;
        right: 30rpx;
        width: 20rpx;
        height: 20rpx;
        margin-top: -12rpx;
        border-width: 4rpx 4rpx 0 0;
        border-color: $tn-font-holder-color;
        border-style: solid;
        transform: matrix(0.5, 0.5, -0.5, 0.5, 0, 0);
      }
      &--none-right {
        &::before {
          right: 0 !important;
        }
      }
    }
    &::after {
      content: " ";
      position: absolute;
      bottom: 0;
      right: 0;
      left: 0;
      pointer-events: none;
      border-bottom: 1px solid $tn-border-solid-color;
      transform: scaleY(0.5) translateZ(0);
      transform-origin: 0 100%;
    }
    &--line-left {
      &::after {
        left: 30rpx !important;
      }
    }
    &--line-right {
      &::after {
        right: 30rpx !important;
      }
    }
    &--unlined {
      &::after {
        border-bottom: 0 !important;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-list-view/tn-list-view.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,184 @@
<template>
  <view
    class="tn-list-view-class tn-list-view"
    :class="[
      backgroundColorClass,
      viewClass
    ]"
    :style="[viewStyle]"
  >
    <view
      v-if="showTitle"
      class="tn-list-view__title"
      :class="[
        fontColorClass
      ]"
      :style="[titleStyle]"
      @tap="handleClickTitle"
    >{{ title }}</view>
    <view
      v-else
      :class="[{'tn-list-view__title--card': card}]"
      @tap="handleClickTitle"
    >
      <slot name="title"></slot>
    </view>
    <view
      class="tn-list-view__content tn-border-solid-top tn-border-solid-bottom"
      :class="[contentClass]"
    >
      <slot></slot>
    </view>
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    mixins: [ componentsColorMixin ],
    name: 'tn-list-view',
    props: {
      // æ ‡é¢˜
      title: {
        type: String,
        default: ''
      },
      // åŽ»æŽ‰è¾¹æ¡† ä¸Šè¾¹æ¡† top, ä¸‹è¾¹æ¡† bottom, æ‰€æœ‰è¾¹æ¡† all
      unlined: {
        type: String,
        default: 'all'
      },
      // ä¸Šå¤–边距
      marginTop: {
        type: String,
        default: ''
      },
      // å†…容是否显示为卡片模式
      card: {
        type: Boolean,
        default: false
      },
      // æ˜¯å¦è‡ªå®šä¹‰æ ‡é¢˜
      customTitle: {
        type: Boolean,
        default: false
      }
    },
    computed: {
      showTitle() {
        return !this.customTitle && this.title
      },
      viewClass() {
        let clazz = ''
        if (this.card) {
          clazz += ' tn-list-view--card'
        }
        return clazz
      },
      viewStyle() {
        let style = {}
        if (this.backgroundColorStyle) {
          style.backgroundColor = this.backgroundColorStyle
        }
        if (this.marginTop) {
          style.marginTop = this.marginTop
        }
        return style
      },
      titleStyle() {
        let style = {}
        if (this.fontColorStyle) {
          style.color = this.fontColorStyle
        }
        if (this.fontSize) {
          style.fontSize = this.fontSize + this.fontUnit
        }
        return style
      },
      contentClass() {
        let clazz = ''
        if (this.card) {
          clazz += ' tn-list-view__content--card'
        }
        switch(this.unlined) {
          case 'top':
            clazz += ' tn-none-border-top'
            break
          case 'bottom':
            clazz += ' tn-none-border-bottom'
            break
          case 'all':
            clazz += ' tn-none-border'
            break
        }
        return clazz
      }
    },
    data () {
      return {
        kindShowFlag: this.showKind
      }
    },
    methods: {
      // å¤„理标题点击事件
      handleClickTitle() {
        if (!this.kindList) return
        this.kindShowFlag = !this.kindShowFlag
        this.$emit("clickTitle", {})
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-list-view {
    background-color: #FFFFFF;
    &__title {
      width: 100%;
      padding: 30rpx;
      font-size: 30rpx;
      line-height: 30rpx;
      box-sizing: border-box;
      &--card {
        // margin: 0rpx 30rpx;
      }
    }
    &__content {
      width: 100%;
      position: relative;
      border-radius: 0;
      &--card {
        // width: auto;
        // overflow: hidden;
        // margin-right: 30rpx;
        // margin-left: 30rpx;
        // border-radius: 20rpx
      }
    }
    &--card {
      // padding-bottom: 30rpx;
      border-radius: 20rpx;
      overflow: hidden;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-load-more/tn-load-more.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,188 @@
<template>
  <view class="tn-load-more-class tn-load-more">
    <view
      class="tn-load-more__wrap"
      :class="[backgroundColorClass]"
      :style="[loadStyle]"
    >
      <view class="tn-load-more__line"></view>
      <view
        class="tn-load-more__content"
        :class="[{'tn-load-more__content--more': (status === 'loadmore' || status === 'nomore')}]"
      >
        <view class="tn-load-more__loading">
          <tn-loading
            class="tn-load-more__loading__icon"
            :mode="loadingIconType"
            :show="status === 'loading' && loadingIcon"
            :color="loadingIconColor"
          ></tn-loading>
        </view>
        <view
          class="tn-load-more__text"
          :class="[fontColorClass, {'tn-load-more__text--dot': (status === 'nomore' && dot)}]"
          :style="[loadTextStyle]"
        >{{ showText }}</view>
      </view>
      <view class="tn-load-more__line"></view>
    </view>
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    name: 'tn-load-more',
    mixins: [componentsColorMixin],
    props: {
      // åŠ è½½çŠ¶æ€
      // loadmore -> åŠ è½½æ›´å¤š
      // loading -> åŠ è½½ä¸­
      // nomore -> æ²¡æœ‰æ›´å¤š
      status: {
        type: String,
        default: 'loadmore'
      },
      // æ˜¾ç¤ºåŠ è½½å›¾æ ‡
      loadingIcon: {
        type: Boolean,
        default: true
      },
      // åŠ è½½å›¾æ ‡æ ·å¼ï¼Œå‚è€ƒtn-loading组件的加载类型
      loadingIconType: {
        type: String,
        default: 'circle'
      },
      // åœ¨åœ†åœˆåŠ è½½çŠ¶æ€ä¸‹ï¼Œåœ†åœˆçš„é¢œè‰²
      loadingIconColor: {
        type: String,
        default: ''
      },
      // æ˜¾ç¤ºçš„æ–‡å­—
      loadText: {
        type: Object,
        default() {
          return {
            loadmore: '加载更多',
            loading: '正在加载...',
            nomore: '没有更多了'
          }
        }
      },
      // æ˜¯å¦æ˜¾ç¤ºç²—点,在nomore状态下生效
      dot: {
        type: Boolean,
        default: false
      },
      // è‡ªå®šä¹‰ç»„件样式
      customStyle: {
        type: Object,
        default() {
          return {}
        }
      }
    },
    computed: {
      loadStyle() {
        let style = {}
        if (this.backgroundColorStyle) {
          style.backgroundColor = this.backgroundColorStyle
        }
        // åˆå¹¶ç”¨æˆ·è‡ªå®šä¹‰æ ·å¼
        if (Object.keys(this.customStyle).length > 0) {
          Object.assign(style, this.customStyle)
        }
        return style
      },
      loadTextStyle() {
        let style = {}
        if (this.fontColorStyle) {
          style.color = this.fontColorStyle
        }
        if (this.fontSizeStyle) {
          style.fontSize = this.fontSizeStyle
          style.lineHeight = this.$t.string.getLengthUnitValue(this.fontSize + 2, this.fontUnit)
        }
        return style
      },
      // æ˜¾ç¤ºçš„æç¤ºæ–‡å­—
      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.dot) text = this.dotText
        else text = this.loadText.nomore || '没有更多了'
        return text
      }
    },
    data() {
      return {
        // ç²—点
        dotText: '●'
      }
    },
    methods: {
      // å¤„理加载更多事件
      loadMore() {
        // åªæœ‰åœ¨ loadmore çŠ¶æ€ä¸‹ç‚¹å‡»æ‰ä¼šå‘é€ç‚¹å‡»äº‹ä»¶ï¼Œå†…å®¹ä¸æ»¡ä¸€å±æ—¶æ— æ³•è§¦å‘åº•éƒ¨ä¸Šæ‹‰äº‹ä»¶ï¼Œæ‰€ä»¥éœ€è¦ç‚¹å‡»æ¥è§¦å‘
        if (this.status === 'loadmore') this.$emit('loadmore')
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-load-more {
    &__wrap {
      background-color: transparent;
      display: flex;
      flex-direction: row;
      justify-content: center;
      align-items: center;
      color: $tn-content-color;
    }
    &__line {
      vertical-align: middle;
      border: 1px solid $tn-content-color;
      width: 50rpx;
      transform: scaleY(0.5);
    }
    &__content {
      display: flex;
      flex-direction: row;
      justify-content: center;
      align-items: center;
      padding: 0 12rpx;
      &--more {
        position: relative;
      }
    }
    &__loading {
      margin-right: 8rpx;
      &__icon {
        display: flex;
        flex-direction: row;
        justify-content: center;
        align-items: center;
      }
    }
    &__text {
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
      line-height: 30rpx;
      &--dot {
        font-size: 28rpx;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-loading/tn-loading.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,114 @@
<template>
  <view
    v-if="show"
    class="tn-loading-class tn-loading"
    :class="[
      `tn-loading-${mode}`,
      animation ? `tn-loading-${mode}--animation` : ''
    ]"
    :style="[loadStyle]"
  ></view>
</template>
<script>
  export default {
    name: 'tn-loading',
    props: {
      // åŠ¨ç”»ç±»åž‹
      // circle åœ†åœˆ flower èŠ±æœµå½¢çŠ¶
      mode: {
        type: String,
        default: 'circle'
      },
      // æ˜¯å¦æ˜¾ç¤º
      show: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦æ˜¾ç¤ºåŠ è½½åŠ¨ç”»
      animation: {
        type: Boolean,
        default: true
      },
      // åœ†åœˆé¢œè‰²
      color: {
        type: String,
        default: ''
      },
      // å›¾æ ‡å¤§å°
      size: {
        type: Number,
        default: 34
      }
    },
    computed: {
      // åŠ è½½åŠ¨ç”»åœ†åœˆçš„æ ·å¼
      loadStyle() {
        let style = {}
        style.width = this.size + 'rpx'
        style.height = style.width
        if (this.mode === 'circle') style.borderColor = `#E6E6E6 #E6E6E6 #E6E6E6 ${this.color ? this.color : '#AAAAAA'}`
        return style
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-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: #E6E6E6 #E6E6E6 #E6E6E6 #AAAAAA;
    &--animation {
      animation: tn-circle 1s linear infinite;
      -webkit-animation: tn-circle 1s linear infinite;
    }
  }
  .tn-loading-flower {
    display: inline-block;
    vertical-align: middle;
    width: 28rpx;
    height: 28rpx;
    background: transparent url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iMTIwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgxMDB2MTAwSDB6Ii8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTlFOUU5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTMwKSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iIzk4OTY5NyIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgzMCAxMDUuOTggNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjOUI5OTlBIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDYwIDc1Ljk4IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0EzQTFBMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NSA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNBQkE5QUEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDU4LjY2IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0IyQjJCMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgxNTAgNTQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjQkFCOEI5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA1MCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDMkMwQzEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTE1MCA0NS45OCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDQkNCQ0IiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTEyMCA0MS4zNCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNEMkQyRDIiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDM1IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0RBREFEQSIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgtNjAgMjQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTJFMkUyIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKC0zMCAtNS45OCA2NSkiLz48L3N2Zz4=) no-repeat;;
    background-size: 100%;
    &--animation {
      animation: tn-flower 1s steps(12) infinite;
      -webkit-animation: tn-flower 1s steps(12) infinite;
    }
  }
  @keyframes tn-flower {
      0% {
          transform: rotate(0deg);
          -webkit-transform: rotate(0deg);
      }
      to {
          transform: rotate(360deg);
          -webkit-transform: rotate(360deg);
      }
  }
  @keyframes tn-circle {
      0% {
          transform: rotate(0);
          -webkit-transform: rotate(0);
      }
      100% {
          transform: rotate(360deg);
          -webkit-transform: rotate(360deg);
      }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-modal/tn-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,246 @@
<template>
  <view v-if="value" class="tn-modal-class tn-modal">
    <tn-popup
      v-model="value"
      mode="center"
      :popup="false"
      :borderRadius="radius"
      :width="width"
      :zoom="zoom"
      :safeAreaInsetBottom="safeAreaInsetBottom"
      :maskCloseable="maskCloseable"
      :zIndex="zIndex"
      :closeBtn="showCloseBtn"
      @close="close"
    >
      <!-- å†…容 -->
      <view
        class="tn-modal__box"
        :class="[
          backgroundColorClass
        ]"
        :style="[boxStyle]"
      >
        <!-- ä¸æ˜¯è‡ªå®šä¹‰å¼¹æ¡†å†…容 -->
        <view v-if="!custom">
          <view class="tn-modal__box__title" v-if="title && title !== ''">{{ title }}</view>
          <view
            class="tn-modal__box__content"
            :class="[
              fontColorClass,
              contentClass
            ]"
            :style="contentStyle"
          >{{ content }}</view>
          <view v-if="button && button.length" class="tn-modal__box__btn-box" :class="[button.length != 2 ? 'tn-flex-direction-column' : '']">
            <block v-for="(item, index) in button" :key="index">
              <tn-button
                width="100%"
                height="68rpx"
                :fontSize="26"
                :backgroundColor="item.backgroundColor || ''"
                :fontColor="item.fontColor || ''"
                :plain="item.plain || false"
                :shape="item.shape || 'round'"
                :class="[
                  button.length > 2 ? 'tn-margin-bottom' : ''
                ]"
                @click="handleClick(index)"
                :style="{
                  width: button.length != 2 ? '80%' : '46%'
                }"
              >
                {{ item.text }}
              </tn-button>
            </block>
          </view>
        </view>
        <view v-else>
          <slot></slot>
        </view>
      </view>
    </tn-popup>
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    mixins: [componentsColorMixin],
    name: 'tn-modal',
    props: {
      // æ˜¾ç¤ºæŽ§åˆ¶
      value: {
        type: Boolean,
        default: false
      },
      // å¼¹æ¡†å®½åº¦
      width: {
        type: String,
        default: '84%'
      },
      // å†…边距
      padding: {
        type: String,
        default: ''
      },
      // åœ†è§’
      radius: {
        type: Number,
        default: 12
      },
      // æ ‡é¢˜
      title: {
        type: String,
        default: ''
      },
      // å†…容
      content: {
        type: String,
        default: ''
      },
      // æŒ‰é’®å†…容 è®¾ç½®å‚数与button组件的参数一致
      // {
      //   text: '确定',
      //   backgroundColor: 'red',
      //   fontColor: 'white',
      //   plain: true,
      //   shape: ''
      // }
      button: {
        type: Array,
        default: () => {
          return []
        }
      },
      safeAreaInsetBottom: {
          type: Boolean,
          default: false
      },
      // ç‚¹å‡»é®ç½©æ˜¯å¦å¯ä»¥å…³é—­
      maskCloseable: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦æ˜¾ç¤ºå³ä¸Šè§’关闭按钮
      showCloseBtn: {
        type: Boolean,
        default: false
      },
      // æ”¾å¤§åŠ¨ç”»
      zoom: {
        type: Boolean,
        default: true
      },
      // è‡ªå®šä¹‰å¼¹æ¡†å†…容
      custom: {
        type: Boolean,
        default: false
      },
      // å¼¹æ¡†çš„z-index
      zIndex: {
        type: Number,
        default: 0
      }
    },
    computed: {
      boxStyle() {
        let style = {}
        if (this.padding) {
          style.padding = this.padding
        }
        if (this.backgroundColorStyle) {
          style.backgroundColor = this.backgroundColorStyle
        }
        return style
      },
      contentClass() {
        let clazz = ''
        if (this.title) {
          clazz += ' tn-margin-top'
        } else {
          clazz += ' tn-modal__box__content--no-title'
        }
        return clazz
      },
      contentStyle() {
        let style = {}
        if (this.fontSize) {
          style.fontSize = this.fontSize + this.fontUnit
        }
        if (this.fontColorStyle) {
          style.color = this.fontColorStyle
        }
        return style
      },
    },
    data() {
      return {
      }
    },
    methods: {
      // å¤„理按钮点击事件
      handleClick(index) {
        if (!this.value) return
        this.$emit("click", {
          index: Number(index)
        })
      },
      // å¤„理关闭事件
      close() {
        this.$emit("cancel")
        this.$emit('input', false)
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-modal {
    &__box {
      position: relative;
      box-sizing: border-box;
      background-color: #FFFFFF;
      padding: 40rpx 64rpx;
      &__title {
        text-align: center;
        font-size: 34rpx;
        color: #333;
        padding-top: 20rpx;
        font-weight: bold;
      }
      &__content {
        text-align: center;
        padding-bottom: 30rpx;
        color: $tn-font-color;
        font-size: 28rpx;
        &--no-title {
          padding-bottom: 0rpx !important;
        }
      }
      &__btn-box {
        width: 100%;
        display: flex;
        align-items: center;
        justify-content: space-between;
      }
      &__content ~ &__btn-box {
        margin-top: 30rpx;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-nav-bar/tn-nav-bar.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,355 @@
<template>
  <view
    class="tn-custom-nav-bar-class tn-custom-nav-bar"
    :style="[navBarStyle]"
  >
      <view
      class="tn-custom-nav-bar__bar"
      :class="[barClass]"
      :style="[barStyle]"
    >
      <view v-if="isBack">
        <view v-if="customBack">
          <view
            :style="{
              width: customBackStyleInfo.width + 'px',
              height: customBackStyleInfo.height + 'px',
              marginLeft: customBackStyleInfo.left + 'px'
            }"
          >
            <slot name="back"></slot>
          </view>
        </view>
        <view v-else class="tn-custom-nav-bar__bar__action" @tap="handlerBack">
            <text class="tn-custom-nav-bar__bar__action--nav-back" :class="[`tn-icon-${backIcon}`]"></text>
            <text class="tn-custom-nav-bar__bar__action--nav-back-text" v-if="backTitle">{{ backTitle }}</text>
        </view>
      </view>
          <view class="tn-custom-nav-bar__bar__content" :style="[contentStyle]">
              <slot></slot>
          </view>
          <view>
        <slot name="right"></slot>
      </view>
      </view>
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    name: 'tn-nav-bar',
    mixins: [componentsColorMixin],
    props: {
      // å±‚级
      zIndex: {
        type: Number,
        default: 0
      },
      // å¯¼èˆªæ çš„高度
      height: {
        type: Number,
        default: 0
      },
      // é«˜åº¦å•位
      unit: {
        type: String,
        default: 'px'
      },
      // æ˜¯å¦æ˜¾ç¤ºè¿”回按钮
      isBack: {
        type: Boolean,
        default: true
      },
      // è¿”回按钮的图标
      backIcon: {
        type: String,
        default: 'left'
      },
      // è¿”回按钮旁显示的文字
      backTitle: {
        type: String,
        default: '返回'
      },
      // é€æ˜ŽçŠ¶æ€æ 
      alpha: {
        type: Boolean,
        default: false
      },
      // æ˜¯å¦å›ºå®šåœ¨é¡¶éƒ¨
      fixed: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦æ˜¾ç¤ºåº•部阴影
      bottomShadow: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦è‡ªå®šä¹‰è¿”回按钮
      customBack: {
        type: Boolean,
        default: false
      },
      // è¿”回前回调
      beforeBack: {
        type: Function,
        default: null
      }
    },
    computed: {
      navBarStyle() {
        let style = {}
        style.height = this.height === 0 ? this.customBarHeight + this.unit : this.height + this.unit
        if (this.fixed) {
          style.position = 'fixed'
        }
        style.zIndex = this.elZIndex
        return style
      },
      barClass() {
        let clazz = ''
        if (this.backgroundColorClass) {
          clazz += ` ${this.backgroundColorClass}`
        }
        if (this.fontColorClass) {
          clazz += `${this.fontColorClass}`
        }
        if (this.fixed) {
          clazz += ' tn-custom-nav-bar__bar--fixed'
        }
        if (this.alpha) {
          clazz += ' tn-custom-nav-bar__bar--alpha'
        }
        if (this.bottomShadow) {
          clazz += ' tn-custom-nav-bar__bar--bottom-shadow'
        }
        return clazz
      },
      barStyle() {
        let style = {}
        style.height = this.height === 0 ? this.customBarHeight + this.unit : this.height + this.unit
        if (this.fixed) {
          style.paddingTop = this.statusBarHeight + 'px'
        }
        if(!this.backgroundColorClass) {
          style.backgroundColor = this.backgroundColor !== '' ? this.backgroundColor : '#FFFFFF'
        }
        if (!this.fontColorClass && this.fontColor) {
          style.color= this.fontColor
        }
        style.zIndex = this.elZIndex
        return style
      },
      contentStyle() {
        let style = {}
        style.top = this.fixed ? this.statusBarHeight + 'px' : '0px'
        style.height = this.height === 0 ? (this.customBarHeight - this.statusBarHeight) + this.unit : this.height + this.unit
        style.lineHeight = style.height
        if (this.isBack) {
          if (this.customBack) {
            const width = (this.customBackStyleInfo.width + this.customBackStyleInfo.left) * 2
            style.width = `calc(100% - ${width}px)`
          } else {
            style.width = 'calc(100% - 340rpx)'
          }
        } else {
          style.width = '100%'
        }
        return style
      },
      elZIndex() {
        return this.zIndex ? this.zIndex : this.$t.zIndex.navbar
      }
    },
    data() {
      return {
        // çŠ¶æ€æ çš„é«˜åº¦
        statusBarHeight: 0,
        // è‡ªå®šä¹‰å¯¼èˆªæ çš„高度
        customBarHeight: 0,
        // è‡ªå®šä¹‰è¿”回按钮时,返回容器的宽高边距信息
        customBackStyleInfo: {
          width: 86,
          height: 32,
          left: 15
        }
      }
    },
    mounted() {
      // èŽ·å–vuex中的自定义顶栏的高度
      this.updateNavBarInfo()
    },
    created() {
      // èŽ·å–èƒ¶å›Šä¿¡æ¯
      // #ifdef MP-WEIXIN
      let custom = wx.getMenuButtonBoundingClientRect()
      this.customBackStyleInfo.width = custom.width
      this.customBackStyleInfo.height = custom.height
      this.customBackStyleInfo.left = uni.upx2px(750) - custom.right
      // #endif
    },
    methods: {
      // æ›´æ–°å¯¼èˆªæ çš„高度
      async updateNavBarInfo() {
        // èŽ·å–vuex中的自定义顶栏的高度
        let customBarHeight = this.vuex_custom_bar_height
        let statusBarHeight = this.vuex_status_bar_height
        // å¦‚果获取失败则重新获取
        if (!customBarHeight) {
          try {
            const navBarInfo = await this.$t.updateCustomBar()
            customBarHeight = navBarInfo.customBarHeight
            statusBarHeight = navBarInfo.statusBarHeight
          } catch(e) {
            setTimeout(() => {
              this.updateNavBarInfo()
            }, 10)
            return
          }
        }
        // æ›´æ–°vuex中的导航栏信息
        this && this.$t.vuex('vuex_status_bar_height', statusBarHeight)
        this && this.$t.vuex('vuex_custom_bar_height', customBarHeight)
        this.customBarHeight = customBarHeight
        this.statusBarHeight = statusBarHeight
      },
      // å¤„理返回事件
      async handlerBack() {
        if (this.beforeBack && typeof(this.beforeBack) === 'function') {
          // æ‰§è¡Œå›žè°ƒï¼ŒåŒæ—¶ä¼ å…¥ç´¢å¼•当作参数
          // åœ¨å¾®ä¿¡ï¼Œæ”¯ä»˜å®ç­‰çŽ¯å¢ƒ(H5正常),会导致父组件定义的函数体中的this变成子组件的this
          // é€šè¿‡bind()方法,绑定父组件的this,让this的this为父组件的上下文
          let beforeBack = this.beforeBack.bind(this.$t.$parent.call(this))()
          // åˆ¤æ–­æ˜¯å¦è¿”回了Promise
          if (!!beforeBack && typeof beforeBack.then === 'function') {
            await beforeBack.then(res => {
              // Promise返回成功
              this.navBack()
            }).catch(err => {})
          } else if (beforeBack === true) {
            this.navBack()
          }
        } else {
          this.navBack()
        }
      },
      // è¿”回上一页
      navBack() {
        // é€šè¿‡åˆ¤æ–­å½“前页面的页面栈信息,是否有上一页进行返回,如果没有则跳转到首页
        const pages = getCurrentPages()
        if (pages && pages.length > 0) {
          const firstPage = pages[0]
          if (pages.length == 1 && (!firstPage.route || firstPage.route != 'pages/index/index')) {
            uni.reLaunch({
              url: '/pages/index/index'
            })
          } else {
            uni.navigateBack({
              delta: 1
            })
          }
        } else {
          uni.reLaunch({
            url: '/pages/index/index'
          })
        }
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-custom-nav-bar {
    display: block;
    position: relative;
    &__bar {
      display: flex;
      position: relative;
      align-items: center;
      min-height: 100rpx;
      justify-content: space-between;
      min-height: 0px;
      /* #ifdef MP-WEIXIN */
      padding-right: 220rpx;
      /* #endif */
      /* #ifdef MP-ALIPAY */
      padding-right: 150rpx;
      /* #endif */
      box-shadow: 0rpx 0rpx 0rpx;
      z-index: 9999;
      &--fixed {
        position: fixed;
        width: 100%;
        top: 0;
      }
      &--alpha {
        background: transparent !important;
        box-shadow: none !important;
      }
      &--bottom-shadow {
        box-shadow: 0rpx 0rpx 80rpx 0rpx rgba(0, 0, 0, 0.05);
      }
      &__action {
        display: flex;
        align-items: center;
        height: 100%;
        justify-content: center;
        max-width: 100%;
        &--nav-back {
          /* position: absolute; */
          /* top: 50%; */
          /* left: 20rpx; */
          /* margin-top: -15rpx; */
          // width: 25rpx;
          // height: 25rpx;
          padding: 20rpx;
          font-size: 38rpx;
          line-height: 100%;
          // border-width: 0 0 4rpx 4rpx;
          // border-color: #000000;
          // border-style: solid;
          // transform: matrix(0.5, 0.5, -0.5, 0.5, 0, 0);
        }
        &--nav-back-text {
          padding: 20rpx 20rpx 20rpx 0rpx;
        }
      }
      &__content {
        position: absolute;
        text-align: center;
        left: 0;
        right: 0;
        bottom: 0;
        margin: auto;
        font-size: 32rpx;
        cursor: none;
        // pointer-events: none;
        text-overflow: ellipsis;
        white-space: nowrap;
        overflow: hidden;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-notice-bar/tn-notice-bar.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,209 @@
<template>
  <view
    v-if="showNotice"
    class="tn-notice-bar-class tn-notice-bar"
    :style="{
      borderRadius: radius + 'rpx'
    }"
  >
    <block v-if="mode === 'horizontal' && circular">
      <tn-row-notice
        :backgroundColor="backgroundColor"
        :fontColor="fontColor"
        :fontSize="fontSize"
        :fontUnit="fontUnit"
        :list="list"
        :show="show"
        :playStatus="playStatus"
        :leftIcon="leftIcon"
        :leftIconName="leftIconName"
        :leftIconSize="leftIconSize"
        :rightIcon="rightIcon"
        :rightIconName="rightIconName"
        :rightIconSize="rightIconSize"
        :closeBtn="closeBtn"
        :autoplay="autoplay"
        :radius="radius"
        :padding="padding"
        :speed="speed"
        @click="click"
        @close="close"
        @clickLeft="clickLeftIcon"
        @clickRight="clickRightIcon"
      ></tn-row-notice>
    </block>
    <block v-if="mode === 'vertical' || (mode === 'horizontal' && !circular)">
      <tn-column-notice
        :backgroundColor="backgroundColor"
        :fontColor="fontColor"
        :fontSize="fontSize"
        :fontUnit="fontUnit"
        :list="list"
        :show="show"
        :mode="mode"
        :playStatus="playStatus"
        :leftIcon="leftIcon"
        :leftIconName="leftIconName"
        :leftIconSize="leftIconSize"
        :rightIcon="rightIcon"
        :rightIconName="rightIconName"
        :rightIconSize="rightIconSize"
        :closeBtn="closeBtn"
        :autoplay="autoplay"
        :radius="radius"
        :padding="padding"
        :duration="duration"
        @click="click"
        @close="close"
        @clickLeft="clickLeftIcon"
        @clickRight="clickRightIcon"
        @end="end"
      ></tn-column-notice>
    </block>
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    name: 'tn-notice-bar',
    mixins: [componentsColorMixin],
    props: {
      // æ˜¾ç¤ºçš„内容
      list: {
        type: Array,
        default() {
          return []
        }
      },
      // æ˜¯å¦æ˜¾ç¤º
      show: {
        type: Boolean,
        default: true
      },
      // æ’­æ”¾çŠ¶æ€
      // play -> æ’­æ”¾ paused -> æš‚停
      playStatus: {
        type: String,
        default: 'play'
      },
      // æ»šåŠ¨æ–¹å‘
      // horizontal -> æ°´å¹³æ»šåЍ vertical -> åž‚直滚动
      mode: {
        type: String,
        default: 'horizontal'
      },
      // æ˜¯å¦æ˜¾ç¤ºå·¦è¾¹å›¾æ ‡
      leftIcon: {
        type: Boolean,
        default: true
      },
      // å·¦è¾¹å›¾æ ‡çš„名称
      leftIconName: {
        type: String,
        default: 'sound'
      },
      // å·¦è¾¹å›¾æ ‡çš„大小
      leftIconSize: {
        type: Number,
        default: 34
      },
      // æ˜¯å¦æ˜¾ç¤ºå³è¾¹çš„图标
      rightIcon: {
        type: Boolean,
        default: false
      },
      // å³è¾¹å›¾æ ‡çš„名称
      rightIconName: {
        type: String,
        default: 'right'
      },
      // å³è¾¹å›¾æ ‡çš„大小
      rightIconSize: {
        type: Number,
        default: 26
      },
      // æ˜¯å¦æ˜¾ç¤ºå…³é—­æŒ‰é’®
      closeBtn: {
        type: Boolean,
        default: false
      },
      // åœ†è§’
      radius: {
        type: Number,
        default: 0
      },
      // å†…边距
      padding: {
        type: String,
        default: '18rpx 24rpx'
      },
      // è‡ªåŠ¨æ’­æ”¾
      autoplay: {
        type: Boolean,
        default: true
      },
      // æ»šåŠ¨å‘¨æœŸ
      duration: {
        type: Number,
        default: 2000
      },
      // æ°´å¹³æ»šåŠ¨æ—¶çš„é€Ÿåº¦ï¼Œå³æ¯ç§’æ»šåŠ¨å¤šå°‘rpx
      speed: {
        type: Number,
        default: 160
      },
      // æ°´å¹³æ»šåŠ¨çš„æ—¶å€™æ˜¯å¦é‡‡ç”¨è¡”æŽ¥çš„æ¨¡å¼
      circular: {
        type: Boolean,
        default: true
      },
      // æ²¡æœ‰æ•°æ®æ—¶æ˜¯å¦æ˜¾ç¤ºé€šçŸ¥
      autoHidden: {
        type: Boolean,
        default: true
      }
    },
    computed: {
      // å½“设置了show为false,或者autoHidden为true且list为空时,不显示通知
      showNotice() {
        if (this.show === false || (this.autoHidden && this.list.length === 0)) return false
        else return true
      }
    },
    data() {
      return {
      }
    },
    methods: {
      // ç‚¹å‡»äº†é€šçŸ¥æ 
      click(index) {
        this.$emit('click', index)
      },
      // ç‚¹å‡»äº†å…³é—­æŒ‰é’®
      close() {
        this.$emit('close')
      },
      // ç‚¹å‡»äº†å·¦è¾¹å›¾æ ‡
      clickLeftIcon() {
        this.$emit('clickLeft')
      },
      // ç‚¹å‡»äº†å³è¾¹å›¾æ ‡
      clickRightIcon() {
        this.$emit('clickRight')
      },
      // ä¸€ä¸ªå‘¨æœŸæ»šåŠ¨ç»“æŸ
      end() {
        this.$emit('end')
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-notice-bar {
    overflow: hidden;
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-number-box/tn-number-box.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,401 @@
<template>
  <view class="tn-number-box-class tn-number-box">
    <!-- å‡ -->
    <view
      class="tn-number-box__btn__minus"
      :class="[
        backgroundColorClass,
        fontColorClass,
        {'tn-number-box__btn--disabled': disabled || inputValue <= min}
      ]"
      :style="{
        backgroundColor: backgroundColorStyle,
        height: $t.string.getLengthUnitValue(inputHeight),
        color: fontColorStyle,
        fontSize: fontSizeStyle
      }"
      @touchstart.stop.prevent="touchStart('minus')"
      @touchend.stop.prevent="clearTimer"
    >
      <view class="tn-icon-reduce"></view>
    </view>
    <!-- è¾“入框 -->
    <input
      v-model="inputValue"
      :disabled="disabledInput || disabled"
      :cursor-spacing="getCursorSpacing"
      class="tn-number-box__input"
      :class="[
        fontColorClass,
        {'tn-number-box__input--disabled': disabledInput || disabled}
      ]"
      :style="{
        width: $t.string.getLengthUnitValue(inputWidth),
        height: $t.string.getLengthUnitValue(inputHeight),
        color: fontColorStyle,
        fontSize: fontSizeStyle,
        backgroundColor: backgroundColorStyle
      }"
      @blur="blurInput"
      @focus="focusInput"
    />
    <!-- åŠ  -->
    <view
      class="tn-number-box__btn__plus"
      :class="[
        backgroundColorClass,
        fontColorClass,
        {'tn-number-box__btn--disabled': disabled || inputValue >= max}
      ]"
      :style="{
        backgroundColor: backgroundColorStyle,
        height: $t.string.getLengthUnitValue(inputHeight),
        color: fontColorStyle,
        fontSize: fontSizeStyle
      }"
      @touchstart.stop.prevent="touchStart('plus')"
      @touchend.stop.prevent="clearTimer"
    >
      <view class="tn-icon-add"></view>
    </view>
  </view>
</template>
<script>
  import componentsColor from '../../libs/mixin/components_color.js'
  export default {
    mixins: [componentsColor],
    name: 'tn-number-box',
    props: {
      value: {
        type: Number,
        default: 1
      },
      // ç´¢å¼•
      index: {
        type: [Number, String],
        default: ''
      },
      // æœ€å°å€¼
      min: {
        type: Number,
        default: 0
      },
      // æœ€å¤§å€¼
      max: {
        type: Number,
        default: 99999
      },
      // æ­¥è¿›å€¼
      step: {
        type: Number,
        default: 1
      },
      // ç¦ç”¨
      disabled: {
        type: Boolean,
        default: false
      },
      // æ˜¯å¦ç¦ç”¨è¾“å…¥
      disabledInput: {
        type: Boolean,
        default: false
      },
      // è¾“入框的宽度
      inputWidth: {
        type: Number,
        default: 88
      },
      // è¾“入框的高度
      inputHeight: {
        type: Number,
        default: 50
      },
      // è¾“入框和键盘之间的距离
      cursorSpacing: {
        type: Number,
        default: 100
      },
      // æ˜¯å¦å¼€å¯é•¿æŒ‰è¿›è¡Œè¿žç»­é€’增减
      longPress: {
        type: Boolean,
        default: true
      },
      // é•¿æŒ‰è§¦å‘é—´éš”
      longPressTime: {
        type: Number,
        default: 250
      },
      // æ˜¯å¦åªèƒ½è¾“入正整数
      positiveInteger: {
        type: Boolean,
        default: true
      }
    },
    computed: {
      getCursorSpacing() {
        return Number(uni.upx2px(this.cursorSpacing))
      }
    },
    data() {
      return {
        // è¾“入框的值
        inputValue: 1,
        // é•¿æŒ‰å®šæ—¶å™¨
        longPressTimer: null,
        // æ ‡è®°å€¼çš„æ”¹å˜æ˜¯æ¥è‡ªå¤–部还是内部
        changeFromInner: false,
        // å†…部定时器
        innerChangeTimer: null
      }
    },
    watch: {
      value(val) {
        // åªæœ‰value的改变是来自外部的时候,才去同步inputValue的值,否则会造成循环错误
        if (!this.changeFromInner) {
          this.updateInputValue()
          // å› ä¸ºinputValue变化后,会触发this.handleChange(),在其中changeFromInner会再次被设置为true,
          // é€ æˆå¤–面修改值,也导致被认为是内部修改的混乱,这里进行this.$nextTick延时,保证在运行周期的最后处
          // å°†changeFromInner设置为false
          this.$nextTick(() => {
              this.changeFromInner = false
          })
        }
      },
      inputValue(newVal, oldVal) {
        // ä¸ºäº†è®©ç”¨æˆ·èƒ½å¤Ÿåˆ é™¤æ‰€æœ‰è¾“入值,重新输入内容,删除所有值后,内容为空字符串
        if (newVal === '') return
        let value = 0
        // é¦–先判断是否数值,并且在min和max之间,如果不是,使用原来值
        let isNumber = this.$t.test.number(newVal)
        if (isNumber && newVal >= this.min && newVal <= this.max) value = newVal
        else value = oldVal
        // åˆ¤æ–­æ˜¯å¦åªèƒ½è¾“入大于等于0的整数
        if (this.positiveInteger) {
          // å°äºŽ0或者带有小数点
          if (newVal < 0 || String(newVal).indexOf('.') !== -1) {
            value = Math.floor(newVal)
            // åŒå‘绑定input的值,必须要使用$nextTick修改显示的值
            this.$nextTick(() => {
                this.inputValue = value
            })
          }
        }
        this.handleChange(value, 'change')
      },
      min() {
        this.updateInputValue()
      },
      max() {
        this.updateInputValue()
      }
    },
    created() {
      this.updateInputValue()
    },
    methods: {
      // å¼€å§‹ç‚¹å‡»æŒ‰é’®
      touchStart(func) {
        // å…ˆæ‰§è¡Œä¸€éæ–¹æ³•,否则会造成松开手时,就执行了clearTimer,导致无法实现功能
        this[func]()
        // å¦‚果没有开启长按功能,直接返回
        if (!this.longPress) return
        // æ¸…空长按定时器,防止重复注册
        if (this.longPressTimer) {
          clearInterval(this.longPressTimer)
          this.longPressTimer = null
        }
        this.longPressTimer = setInterval(() => {
          // æ‰§è¡ŒåŠ å‡æ“ä½œ
          this[func]()
        }, this.longPressTime)
      },
      // æ¸…除定时器
      clearTimer() {
        this.$nextTick(() => {
          if (this.longPressTimer) {
            clearInterval(this.longPressTimer)
            this.longPressTimer = null
          }
        })
      },
      // å‡
      minus() {
        this.computeValue('minus')
      },
      // åŠ 
      plus() {
        this.computeValue('plus')
      },
      // å¤„理小数相加减出现溢出问题
      calcPlus(num1, num2) {
        let baseNum = 0, baseNum1 = 0, baseNum2 = 0
        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 = 0, baseNum1 = 0, baseNum2 = 0
        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)
      },
      // å¤„理操作后的值
      computeValue(type) {
        uni.hideKeyboard()
        if (this.disabled) return
        let value = 0
        if (type === 'minus') {
          // å‡
          value = this.calcMinus(this.inputValue, this.step)
        } else if (type === 'plus') {
          // åŠ 
          value = this.calcPlus(this.inputValue, this.step)
        }
        // åˆ¤æ–­æ˜¯å¦æ¯”最小值小和操作最大值
        if (value < this.min || value > this.max) return
        this.inputValue = value
        this.handleChange(value, type)
      },
      // å¤„理用户手动输入
      blurInput(event) {
        let val = 0,
            value = event.detail.value
        // å¦‚果为非0-9数字组成,或者其第一位数值为0,直接让其等于min值
        // è¿™é‡Œä¸ç›´æŽ¥åˆ¤æ–­æ˜¯å¦æ­£æ•´æ•°ï¼Œæ˜¯å› ä¸ºç”¨æˆ·ä¼ é€’çš„props min值可能为0
        if (!/(^\d+$)/.test(value) || value[0] == 0) {
          val = this.min
        } else {
          val = +value
        }
        if (val > this.max) {
          val = this.max
        } else if (val < this.min) {
          val = this.min
        }
        this.$nextTick(() => {
          this.inputValue = val
        })
        this.handleChange(val, 'blur')
      },
      // èŽ·å–ç„¦ç‚¹
      focusInput() {
        this.$emit('focus')
      },
      // åˆå§‹åŒ–inputValue
      updateInputValue() {
        let value = this.value
        if (value <= this.min) {
          value = this.min
        } else if (value >= this.max) {
          value = this.max
        }
        this.inputValue = Number(value)
      },
      // å¤„理值改变状态
      handleChange(value, type) {
        if (this.disabled) return
        // æ¸…除定时器,防止混乱
        if (this.innerChangeTimer) {
          clearTimeout(this.innerChangeTimer)
          this.innerChangeTimer = null
        }
        // å†…部修改值
        this.changeFromInner = true
        // ä¸€å®šæ—¶é—´å†…,清除changeFromInner标记,否则内部值改变后
        // å¤–部通过程序修改value值,将会无效
        this.innerChangeTimer = setTimeout(() => {
          this.changeFromInner = false
        }, 150)
        this.$emit('input', Number(value))
        this.$emit(type, {
          value: Number(value),
          index: this.index
        })
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-number-box {
    display: inline-flex;
    align-items: center;
    &__btn {
      &__plus,&__minus {
        width: 60rpx;
        display: flex;
        flex-direction: row;
        justify-content: center;
        align-items: center;
        background-color: $tn-font-holder-color;
      }
      &__plus {
        border-radius: 0 8rpx 8rpx 0;
      }
      &__minus {
        border-radius: 8rpx 0 0 8rpx;
      }
      &--disabled {
        color: $tn-font-sub-color !important;
        background: $tn-font-holder-color !important;
      }
    }
    &__input {
      display: flex;
      flex-direction: row;
      align-items: center;
      justify-content: center;
      position: relative;
      text-align: center;
      box-sizing: border-box;
      padding: 0 4rpx;
      margin: 0 6rpx;
      background-color: $tn-font-holder-color;
      &--disabled {
        color: $tn-font-sub-color !important;
        background: $tn-font-holder-color !important;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-number-keyboard/tn-number-keyboard.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,182 @@
<template>
  <view class="tn-number-keyboard-class tn-number-keyboard" @touchmove.stop.prevent="() => {}">
    <view class="tn-number-keyboard__grids">
      <view
        v-for="(item, index) in dataList"
        :key="index"
        class="tn-number-keyboard__grids__item"
        :class="{
          'tn-bg-gray--light': showGaryBg(index),
          'tn-border-solid-top': index <= 2,
          'tn-border-solid-bottom': index < 9,
          'tn-border-solid-right': (index + 1) % 3 != 0
        }"
        :hover-class="hoverClass(index)"
        :hover-stay-time="150"
        @tap="keyboardClick(item)"
      >
        <view class="tn-number-keyboard__grids__btn">{{ item }}</view>
      </view>
      <view
        class="tn-number-keyboard__grids__item tn-bg-gray--light"
        hover-class="tn-hover"
        :hover-stay-time="150"
        @touchstart.stop="backspaceClick"
        @touchend="clearTimer"
      >
        <view class="tn-number-keyboard__grids__btn tn-number-keyboard__back">
          <view class="tn-icon-left-arrow tn-number-keyboard__back__icon"></view>
        </view>
      </view>
    </view>
  </view>
</template>
<script>
  export default {
    name: 'tn-number-keyboard',
    props: {
      // é”®ç›˜ç±»åž‹
      // number -> æ•°å­—键盘 card -> èº«ä»½è¯é”®ç›˜
      mode: {
        type: String,
        default: 'number'
      },
      // æ˜¯å¦æ˜¾ç¤ºé”®ç›˜çš„'.'符号
      dotEnabled: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦ä¸ºä¹±åºé”®ç›˜
      randomEnabled: {
        type: Boolean,
        default: false
      }
    },
    computed: {
      // é”®ç›˜æ˜¾ç¤ºçš„内容
      dataList() {
        let tmp = []
        if (!this.dotEnabled && this.mode === 'number') {
          if (!this.randomEnabled) {
            return [1, 2, 3, 4, 5, 6, 7, 8, 9, '', 0]
          } else {
            let data = this.$t.array.random([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
            data.splice(-1, 0, '')
            return data
          }
        } else if (this.dotEnabled && this.mode === 'number') {
          if (!this.randomEnabled) {
            return [1, 2, 3, 4, 5, 6, 7, 8, 9, this.dot, 0]
          } else {
            let data = this.$t.array.random([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
            data.splice(-1, 0, this.dot)
            return data
          }
        } else if (this.mode === 'card') {
          if (!this.randomEnabled) {
            return [1, 2, 3, 4, 5, 6, 7, 8, 9, this.cardX, 0]
          } else {
            let data = this.$t.array.random([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
            data.splice(-1, 0, this.cardX)
            return data
          }
        }
      },
      // æŒ‰é”®çš„æ ·å¼
      keyStyle() {
        return index => {
          let style = {}
          if (this.mode === 'number' && !this.dotEnabled && index === 9) style.flex = '0 0 66.6666666666%'
          return style
        }
      },
      // æ˜¯å¦è®©æŒ‰é”®æ˜¾ç¤ºç°è‰²ï¼Œåªåœ¨æ•°å­—键盘和非乱序且在点击时
      showGaryBg() {
        return index => {
          if (!this.randomEnabled && index === 9 && (this.mode !== 'number' || (this.mode === 'number' && this.dotEnabled))) return true
          else return false
        }
      },
      // æ‰‹æŒ‡åœç•™çš„class
      hoverClass() {
        return index => {
          if (this.mode === 'number' && !this.dotEnabled && index === 9) return ''
          if (!this.randomEnabled && index === 9 && (this.mode === 'number' && this.dotEnabled || this.mode === 'card')) return 'tn-hover'
          else return 'tn-number-keyboard--hover'
        }
      }
    },
    data() {
      return {
        // é€€æ ¼é”®å†…容
        backspace: 'backspace',
        // ç‚¹å†…容
        dot: '.',
        // é•¿æŒ‰å¤šæ¬¡åˆ é™¤äº‹ä»¶ç›‘听
        longPressDeleteTimer: null,
        // èº«ä»½è¯çš„X符号
        cardX: 'X'
      }
    },
    methods: {
      // ç‚¹å‡»é€€æ ¼é”®
      backspaceClick() {
        this.$emit('backspace')
        this.clearTimer()
        this.longPressDeleteTimer = setInterval(() => {
          this.$emit('backspace')
        }, 250)
      },
      // èŽ·å–é”®ç›˜æ˜¾ç¤ºçš„å†…å®¹
      keyboardClick(value) {
        if (this.mode === 'number' && !this.dotEnabled && value === '') return
        // å…è®¸é”®ç›˜æ˜¾ç¤ºç‚¹æ¨¡å¼å’Œè§¦å‘非点按键时,将内容转换为数字类型
        if (this.dotEnabled && value != this.dot && value != this.cardX) value = Number(value)
        this.$emit('change', value)
      },
      // æ¸…除定时器
      clearTimer() {
        if (this.longPressDeleteTimer) {
          clearInterval(this.longPressDeleteTimer)
          this.longPressDeleteTimer = null
        }
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-number-keyboard {
    position: relative;
    &__grids {
      display: flex;
      flex-direction: row;
      flex-wrap: wrap;
      justify-content: flex-end;
      &__item {
        display: flex;
        flex-direction: row;
        flex: 0 0 33.3333333333%;
        align-items: center;
        justify-content: center;
        height: 110rpx;
        text-align: center;
        font-size: 50rpx;
        color: $tn-font-color;
        font-weight: 500;
      }
    }
    &__back {
      font-size: 38rpx;
    }
    &--hover {
      background-color: $tn-font-holder-color;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-picker/tn-picker.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,723 @@
<template>
  <view v-if="value" class="tn-picker-class tn-picker">
    <tn-popup
      v-model="value"
      mode="bottom"
      :popup="false"
      length="auto"
      :safeAreaInsetBottom="safeAreaInsetBottom"
      :maskCloseable="maskCloseable"
      :zIndex="elZIndex"
      @close="close"
    >
      <view class="tn-picker__content" :style="{ zIndex: elZIndex }">
        <!-- é¡¶éƒ¨ -->
        <view class="tn-picker__content__header tn-border-solid-bottom" @touchmove.stop.prevent>
          <!-- å–消按钮 -->
          <view
            class="tn-picker__content__header__btn tn-picker__content__header--cancel"
            :style="{ color: cancelColor }"
            hover-class="tn-hover-class"
            :hover-stay-time="150"
            @tap="getResult('cancel')"
          >{{cancelText}}</view>
          <!-- æ ‡é¢˜ -->
          <view class="tn-picker__content__header__title">{{ title }}</view>
          <!-- ç¡®è®¤æŒ‰é’® -->
          <view
            class="tn-picker__content__header__btn tn-picker__content__header--confirm"
            :style="{ color: moving ? cancelColor : confirmColor}"
            hover-class="tn-hover-class"
            :hover-stay-time="150"
            @touchmove.stop
            @tap.stop="getResult('confirm')"
          >{{confirmText}}</view>
        </view>
        <!-- ä¸»ä½“ -->
        <view class="tn-picker__content__body">
          <!-- åœ°åŒºé€‰æ‹© -->
          <picker-view
            v-if="mode === 'region'"
            class="tn-picker__content__body__view"
            :value="valueArr"
            @change="change"
            @pickstart="pickStart"
            @pickend="pickEnd"
          >
            <picker-view-column v-if="!reset && params.province">
              <view class="tn-picker__content__body__item" v-for="(item, index) in provinces" :key="index">
                <view class="tn-text-ellipsis">{{ item.label }}</view>
              </view>
            </picker-view-column>
            <picker-view-column v-if="!reset && params.city">
              <view class="tn-picker__content__body__item" v-for="(item, index) in citys" :key="index">
                <view class="tn-text-ellipsis">{{ item.label }}</view>
              </view>
            </picker-view-column>
            <picker-view-column v-if="!reset && params.area">
              <view class="tn-picker__content__body__item" v-for="(item, index) in areas" :key="index">
                <view class="tn-text-ellipsis">{{ item.label }}</view>
              </view>
            </picker-view-column>
          </picker-view>
          <!-- æ—¶é—´é€‰æ‹© -->
          <picker-view
            v-else-if="mode === 'time'"
            class="tn-picker__content__body__view"
            :value="valueArr"
            @change="change"
            @pickstart="pickStart"
            @pickend="pickEnd"
          >
            <picker-view-column v-if="!reset && params.year">
              <view class="tn-picker__content__body__item" v-for="(item, index) in years" :key="index">
                {{ item }}
                <text v-if="showTimeTag" class="tn-picker__content__body__item__text">å¹´</text>
              </view>
            </picker-view-column>
            <picker-view-column v-if="!reset && params.month">
              <view class="tn-picker__content__body__item" v-for="(item, index) in months" :key="index">
                {{ formatNumber(item) }}
                <text v-if="showTimeTag" class="tn-picker__content__body__item--text">月</text>
              </view>
            </picker-view-column>
            <picker-view-column v-if="!reset && params.day">
              <view class="tn-picker__content__body__item" v-for="(item, index) in days" :key="index">
                {{ formatNumber(item) }}
                <text v-if="showTimeTag" class="tn-picker__content__body__item--text">日</text>
              </view>
            </picker-view-column>
            <picker-view-column v-if="!reset && params.hour">
              <view class="tn-picker__content__body__item" v-for="(item, index) in hours" :key="index">
                {{ formatNumber(item) }}
                <text v-if="showTimeTag" class="tn-picker__content__body__item--text">时</text>
              </view>
            </picker-view-column>
            <picker-view-column v-if="!reset && params.minute">
              <view class="tn-picker__content__body__item" v-for="(item, index) in minutes" :key="index">
                {{ formatNumber(item) }}
                <text v-if="showTimeTag" class="tn-picker__content__body__item--text">分</text>
              </view>
            </picker-view-column>
            <picker-view-column v-if="!reset && params.second">
              <view class="tn-picker__content__body__item" v-for="(item, index) in seconds" :key="index">
                {{ formatNumber(item) }}
                <text v-if="showTimeTag" class="tn-picker__content__body__item--text">秒</text>
              </view>
            </picker-view-column>
          </picker-view>
          <!-- å•列选择 -->
          <picker-view
            v-else-if="mode === 'selector'"
            class="tn-picker__content__body__view"
            :value="valueArr"
            @change="change"
            @pickstart="pickStart"
            @pickend="pickEnd"
          >
            <picker-view-column v-if="!reset">
              <view class="tn-picker__content__body__item" v-for="(item, index) in range" :key="index">
                <view class="tn-text-ellipsis">{{ getItemValue(item, 'selector') }}</view>
              </view>
            </picker-view-column>
          </picker-view>
          <!-- å¤šåˆ—选择 -->
          <picker-view
            v-else-if="mode === 'multiSelector'"
            class="tn-picker__content__body__view"
            :value="valueArr"
            @change="change"
            @pickstart="pickStart"
            @pickend="pickEnd"
          >
            <picker-view-column v-if="!reset" v-for="(item, index) in range" :key="index">
              <view class="tn-picker__content__body__item" v-for="(sub_item, sub_index) in item" :key="sub_index">
                <view class="tn-text-ellipsis">{{ getItemValue(sub_item, 'multiSelector') }}</view>
              </view>
            </picker-view-column>
          </picker-view>
        </view>
      </view>
    </tn-popup>
  </view>
</template>
<script>
  import provinces from '../../libs/utils/province.js'
  import citys from '../../libs/utils/city.js'
  import areas from '../../libs/utils/area.js'
  export default {
    name: 'tn-picker',
    props: {
      value: {
        type: Boolean,
        default: false,
      },
      // é¡¶éƒ¨æ ‡é¢˜
      title: {
        type: String,
        default: ''
      },
      // 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
          }
        }
      },
      // æ¨¡å¼é€‰æ‹©ï¼Œregion-地区类型,time-时间类型,selector-单列模式,multiSelector-多列模式
      mode: {
        type: String,
        default: 'time'
      },
      // å½“mode=selector或者mode=multiSelector时,提供的数组
      range: {
        type: Array,
        default() {
          return []
        }
      },
      // å½“mode=selector或者mode=multiSelector时,提供的默认项下标
      defaultSelector: {
        type: Array,
        default() {
          return [0]
        }
      },
      // å½“range是一个Array<Object>时,通过rangeKey来指定Object中key的值作为显示的内容
      rangeKey: {
        type: String,
        default: ''
      },
      // æ—¶é—´æ¨¡å¼ ï¼Œæ˜¯å¦æ˜¾ç¤ºæ—¶é—´åŽçš„单位
      showTimeTag: {
        type: Boolean,
        default: true
      },
      // å¼€å§‹å¹´ä»½
      startYear: {
        type: [String, Number],
        default: 1950
      },
      // ç»“束年份
      endYear: {
        type: [String, Number],
        default: 2050
      },
      // é»˜è®¤æ˜¾ç¤ºçš„æ—¶é—´
      // 2021-09-01 || 2021-09-01 13:00:23 || 2021/09/01
      defaultTime: {
        type: String,
         default: ''
      },
      // é»˜è®¤æ˜¾ç¤ºçš„地区
      // å¯ä¼ ç±»ä¼¼["广东省", "广州市", "天河区"]
      defaultRegin: {
        type: Array,
        default() {
          return []
        }
      },
      // é»˜è®¤æ˜¾ç¤ºçš„地区编码
      // å¯ä¼ ç±»ä¼¼["11", "1101", "110101"]
      // å¦‚æžœdefaultRegin和areaCode同时存在,优先使用areaCode的值
      areaCode: {
        type: Array,
        default() {
          return []
        }
      },
      // å–消按钮的文字
      cancelText: {
        type: String,
        default: '取消'
      },
      // å–消按钮的颜色
      cancelColor: {
        type: String,
        default: ''
      },
      // ç¡®è®¤æŒ‰é’®çš„æ–‡å­—
      confirmText: {
        type: String,
        default: '确认'
      },
      // ç¡®è®¤æŒ‰é’®çš„æ¼”示
      confirmColor: {
        type: String,
        default: ''
      },
      safeAreaInsetBottom: {
        type: Boolean,
        default: false
      },
      // æ˜¯å¦å…è®¸é€šè¿‡ç‚¹å‡»é®ç½©å…³é—­
      maskCloseable: {
        type: Boolean,
        default: true
      },
      zIndex: {
        type: Number,
        default: 0
      }
    },
    computed: {
      // ç›‘听参数变化
      propsChange() {
        return [this.mode, this.defaultTime, this.startYear, this.endYear, this.defaultRegin, this.areaCode]
      },
      // ç›‘听地区发生变化
      regionChange() {
        return [this.province, this.city]
      },
      // ç›‘听年月发生变化
      yearAndMonth() {
        return [this.year, this.month]
      },
      elZIndex() {
        return this.zIndex ? this.zIndex : this.$t.zIndex.popup
      }
    },
    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
      }
    },
    watch: {
      propsChange() {
        this.reset = true
        setTimeout(() => this.init(), 10)
      },
      regionChange() {
        // å¦‚果地区发生变化,为了让picker联动起来,必须重置this.citys和this.areas
        this.citys = citys[this.province]
        this.areas = areas[this.province][this.city]
      },
      yearAndMonth() {
        // æœˆä»½çš„变化,实时变更日的天数,因为不同月份,天数不一样
        // ä¸€ä¸ªæœˆå¯èƒ½æœ‰30,31天,甚至闰年2月的29天,平年2月28天
        if (this.params.year) this.setDays()
      },
      value(val) {
        // å¾®ä¿¡å’ŒQQ小程序由于一些奇怪的原因(故同时对所有平台均初始化一遍),需要重新初始化才能显示正确的值
        if (val) {
          this.reset = true
          setTimeout(() => this.init(), 10)
        }
      }
    },
    mounted() {
      this.init()
    },
    methods: {
      // è®°å½•开始滑动
      pickStart() {
        // #ifdef MP-WEIXIN
        this.moving = true
        // #endif
      },
      // è®°å½•滚动结束
      pickEnd() {
        // #ifdef MP-WEIXIN
        this.moving = false
        // #endif
      },
      // æ ¹æ®ä¼ é€’的列表的数据获取显示的数据
      getItemValue(item, mode) {
        // å•列模式或者多列模式中的getItemValue同时被执行,故在这里再加一层判断
        if (this.mode === mode) {
          return typeof item === 'object' ? item[this.rangeKey] : item
        }
      },
      // å¾€æ•°å­—前面补0
      formatNumber(num) {
        return this.$t.number.formatNumberAddZero(num)
      },
      // ç”Ÿæˆé€’进的数组
      generateArray(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(arr, val) {
        let index = arr.indexOf(val)
        // å¦‚æžœindex为-1着找不到元素
        // ~(-1)=(-1)-1=0
        return ~index ? index : 0
      },
      // æ—¥æœŸæ—¶é—´å¤„理
      initTimeValue() {
        // æ ¼å¼åŒ–时间,在IE浏览器(uni不存在此情况),无法识别日期间的"-"间隔符号
        let fdate = this.defaultTime.replace(/\-/g, '/')
        fdate = fdate && fdate.indexOf('/') == -1 ? `2021/01/01 ${fdate}` : fdate
        let time = null
        if (fdate) time = new Date(fdate)
        else time = new Date()
        // èŽ·å–å¹´æœˆæ—¥æ—¶åˆ†ç§’
        this.year = time.getFullYear()
        this.month = 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
        // é¿å…å¤šæ¬¡è§¦å‘导致值数组计算错误
        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() {
        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.defaultRegin.length) {
          tmp = this.defaultRegin[0]
        } else {
          tmp = 0
        }
        // éåŽ†çœä»½æ•°ç»„
        provinces.map((v, k) => {
          if (useCode ? v.value == tmp : v.label == tmp) {
            this.province = k
            return
          }
        })
        this.provinces = provinces
        this.valueArr.splice(0, 1, this.province)
      },
      setCitys() {
        if (!this.params.city) return
        let tmp = ''
        let useCode = false
        // å¦‚果同时配置了defaultRegion和areaCode,优先使用areaCode参数
        if (this.areaCode.length) {
          tmp = this.areaCode[1]
          useCode = true
        } else if (this.defaultRegin.length) {
          tmp = this.defaultRegin[1]
        } else {
          tmp = 0
        }
        // éåŽ†çœä»½æ•°ç»„
        citys[this.province].map((v, k) => {
          if (useCode ? v.value == tmp : v.label == tmp) {
            this.city = k
            return
          }
        })
        this.citys = citys[this.province]
        this.valueArr.splice(1, 1, this.city)
      },
      setAreas() {
        if (!this.params.area) return
        let tmp = ''
        let useCode = false
        // å¦‚果同时配置了defaultRegion和areaCode,优先使用areaCode参数
        if (this.areaCode.length) {
          tmp = this.areaCode[2]
          useCode = true
        } else if (this.defaultRegin.length) {
          tmp = this.defaultRegin[2]
        } else {
          tmp = 0
        }
        // éåŽ†çœä»½æ•°ç»„
        areas[this.province][this.city].map((v, k) => {
          if (useCode ? v.value == tmp : v.label == tmp) {
            this.area = k
            return
          }
        })
        this.areas = areas[this.province][this.city]
        this.valueArr.splice(2, 1, this.area)
      },
      close() {
        this.$emit('input', false)
      },
      // ç›‘听用户修改了picker列选项
      change(event) {
        this.valueArr = event.detail.value
        let i = 0
        if (this.mode === 'time') {
          // ä½¿ç”¨i++是因为不知道数组的长度
          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') {
          // æ ‡è®°çœå¸‚是否发生了变化
          let provinceChange = false,
              cityChange = false
          if (this.params.province) {
            let value = this.valueArr[i++]
            if (this.province != value) {
              // å¦‚果省份发生了变化,则重置市区的索引为0
              this.city = 0
              this.area = 0
              provinceChange = true
            }
            this.province = value
          }
          if (this.params.city && !provinceChange) {
            let value = this.valueArr[i++]
            if (this.city != value) {
              // å¦‚果市发生了变化,则重置区的索引为0
              this.area = 0
              cityChange = true
            }
            this.city = value
          }
          if (this.params.area && !provinceChange && !cityChange) this.area = this.valueArr[i++]
          // å¦‚果有省市进行了改变,重新设置列表的值
          if (provinceChange || cityChange) {
            this.valueArr = [this.province, this.city, this.area]
          }
        } else if (this.mode === 'multiSelector') {
          let index = null
          // å¯¹æ¯”前后两个数组,寻找变更的是哪一列,如果某一个元素不同,即可判定该列发生了变化
          this.defaultSelector.map((v, idx) => {
            if (v != event.detail.value[idx]) index = idx
          })
          // ä¸ºäº†è®©ç”¨æˆ·å¯¹å¤šåˆ—变化时,动态设置其他列
          if (index != null) {
            this.$emit('columnchange', {
              column: index,
              index: event.detail.value[index]
            })
          }
        }
      },
      // ç”¨æˆ·ç‚¹å‡»ç¡®å®šæŒ‰é’®
      getResult(event = null) {
        // #ifdef MP-WEIXIN
        if (this.moving) return
        // #endif
        let result = {}
        // åªè¿”回用户需要的数据
        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 === 'multiSelector') {
          result = this.valueArr
        } else if (this.mode === 'selector') {
          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>
  .tn-picker {
    &__content {
      position: relative;
      &__header {
        position: relative;
        display: flex;
        flex-direction: row;
        width: 100%;
        height: 90rpx;
        padding: 0 40rpx;
        align-items: center;
        justify-content: space-between;
        box-sizing: border-box;
        font-size: 30rpx;
        background-color: #FFFFFF;
        &__btn {
          padding: 16rpx;
          box-sizing: border-box;
          text-align: center;
          text-decoration: none;
        }
        &__title {
          color: $tn-font-color;
        }
        &--cancel {
          color: $tn-font-sub-color;
        }
        &--confirm {
          color: $tn-main-color;
        }
      }
      &__body {
        width: 100%;
        height: 500rpx;
        overflow: hidden;
        background-color: #FFFFFF;
        &__view {
          height: 100%;
          box-sizing: border-box;
        }
        &__item {
          display: flex;
          flex-direction: row;
          align-items: center;
          justify-content: center;
          font-size: 32rpx;
          color: $tn-font-color;
          padding: 0 8rpx;
          &--text {
            font-size: 24rpx;
            padding-left: 8rpx;
          }
        }
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-popup/tn-popup.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,491 @@
<template>
  <view
    v-if="visibleSync"
    class="tn-popup-class tn-popup"
    :style="[customStyle, popupStyle, { zIndex: elZIndex - 1}]"
    hover-stop-propagation
  >
    <!-- mask -->
    <view
      class="tn-popup__mask"
      :class="[{'tn-popup__mask--show': showPopup && mask}]"
      :style="{zIndex: elZIndex - 2}"
      @tap="maskClick"
      @touchmove.stop.prevent = "() => {}"
      hover-stop-propagation
    ></view>
    <!-- å¼¹æ¡†å†…容 -->
    <view
      class="tn-popup__content"
      :class="[
        mode !== 'center' ? backgroundColorClass : '',
        safeAreaInsetBottom ? 'tn-safe-area-inset-bottom' : '',
        'tn-popup--' + mode,
        showPopup ? 'tn-popup__content--visible' : '',
        zoom && mode === 'center' ? 'tn-popup__content__center--animation-zoom' : ''
      ]"
      :style="[contentStyle]"
      @tap="modeCenterClose"
      @touchmove.stop.prevent
      @tap.stop.prevent
    >
      <!-- å±…中时候的内容 -->
      <view
        v-if="mode === 'center'"
        class="tn-popup__content__center_box"
        :class="[backgroundColorClass]"
        :style="[centerStyle]"
        @touchmove.stop.prevent
        @tap.stop.prevent
      >
        <!-- å…³é—­æŒ‰é’® -->
        <view
          v-if="closeBtn"
          class="tn-popup__close"
          :class="[`tn-icon-${closeBtnIcon}`, `tn-popup__close--${closeBtnPosition}`]"
          :style="[closeBtnStyle, {zIndex: elZIndex}]"
          @tap="close"
        ></view>
        <scroll-view class="tn-popup__content__scroll-view">
          <slot></slot>
        </scroll-view>
      </view>
      <!-- é™¤å±…中外的其他情况 -->
      <scroll-view v-else class="tn-popup__content__scroll-view">
        <slot></slot>
      </scroll-view>
      <!-- å…³é—­æŒ‰é’® -->
      <view
        v-if="mode !== 'center' && closeBtn"
        class="tn-popup__close"
        :class="[`tn-popup__close--${closeBtnPosition}`]"
        :style="{zIndex: elZIndex}"
        @tap="close"
      >
        <view :class="[`tn-icon-${closeBtnIcon}`]" :style="[closeBtnStyle]"></view>
      </view>
    </view>
  </view>
</template>
<script>
  import componentsColorMixin from '../../libs/mixin/components_color.js'
  export default {
    mixins: [componentsColorMixin],
    name: 'tn-popup',
    props: {
      value: {
          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)
      length: {
        type: [Number, String],
        default: 'auto'
      },
      // å®½åº¦ï¼Œåªå¯¹å·¦ï¼Œå³ï¼Œä¸­éƒ¨å¼¹å‡ºæ—¶èµ·ä½œç”¨ï¼Œå•位rpx,或者"auto"
      // æˆ–者百分比"50%",表示由内容撑开高度或者宽度,优先级高于length参数
      width: {
          type: String,
          default: ''
      },
      // é«˜åº¦ï¼Œåªå¯¹ä¸Šï¼Œä¸‹ï¼Œä¸­éƒ¨å¼¹å‡ºæ—¶èµ·ä½œç”¨ï¼Œå•位rpx,或者"auto"
      // æˆ–者百分比"50%",表示由内容撑开高度或者宽度,优先级高于length参数
      height: {
          type: String,
          default: ''
      },
      // æ˜¯å¦å¼€å¯åŠ¨ç”»ï¼Œåªåœ¨mode=center有效
      zoom: {
        type: Boolean,
        default: true
      },
      // æ˜¯å¦å¼€å¯åº•部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
      safeAreaInsetBottom: {
          type: Boolean,
          default: false
      },
      // æ˜¯å¦å¯ä»¥é€šè¿‡ç‚¹å‡»é®ç½©è¿›è¡Œå…³é—­
      maskCloseable: {
          type: Boolean,
          default: true
      },
      // ç”¨æˆ·è‡ªå®šä¹‰æ ·å¼
      customStyle: {
          type: Object,
          default() {
              return {}
          }
      },
      // æ˜¾ç¤ºåœ†è§’的大小
      borderRadius: {
        type: Number,
        default: 0
      },
      // zIndex
      zIndex: {
        type: Number,
        default: 0
      },
      // æ˜¯å¦æ˜¾ç¤ºå…³é—­æŒ‰é’®
      closeBtn: {
        type: Boolean,
        default: false
      },
      // å…³é—­æŒ‰é’®çš„图标
      closeBtnIcon: {
        type: String,
        default: 'close'
      },
      // å…³é—­æŒ‰é’®æ˜¾ç¤ºçš„位置
      // top-left/top-right/bottom-left/bottom-right
      closeBtnPosition: {
        type: String,
        default: 'top-right'
      },
      // å…³é—­æŒ‰é’®å›¾æ ‡é¢œè‰²
      closeIconColor: {
        type: String,
        default: '#AAAAAA'
      },
      // å…³é—­æŒ‰é’®å›¾æ ‡çš„大小
      closeIconSize: {
        type: Number,
        default: 30
      },
      // ç»™ä¸€ä¸ªè´Ÿçš„margin-top,往上偏移,避免和键盘重合的情况,仅在mode=center时有效
      negativeTop: {
          type: Number,
          default: 0
      },
      // marginTop,在mode = top,left,right时生效,避免用户使用了自定义导航栏,组件把导航栏遮挡了
      marginTop: {
        type: Number,
        default: 0
      },
      // æ­¤ä¸ºå†…部参数,不在文档对外使用,为了解决Picker和keyboard等融合了弹窗的组件
      // å¯¹v-model双向绑定多层调用造成报错不能修改props值的问题
      popup: {
          type: Boolean,
          default: true
      },
    },
    computed: {
      // å¤„理使用了自定义导航栏时被遮挡的问题
      popupStyle() {
        let style = {}
        if ((this.mode === 'top' || this.mode === 'left' || this.mode === 'right') && this.marginTop) {
          style.marginTop = this.$t.string.getLengthUnitValue(this.marginTop, 'px')
        }
        return style
      },
      // æ ¹æ®mode的位置,设定其弹窗的宽度(mode = left|right),或者高度(mode = top|bottom)
      contentStyle() {
        let style = {}
        // å¦‚果是左边或者上边弹出时,需要给translate设置为负值,用于隐藏
        if (this.mode === 'left' || this.mode === 'right') {
          style = {
            width: this.width ? this.$t.string.getLengthUnitValue(this.width) : this.$t.string.getLengthUnitValue(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.$t.string.getLengthUnitValue(this.height) : this.$t.string.getLengthUnitValue(this.length),
            transform: `translate3D(0px, ${this.mode === 'top' ? '-100%': '100%'}, 0px)`
          }
        }
        style.zIndex = this.elZIndex
        // å¦‚果设置了圆角的值,添加弹窗的圆角
        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
          }
          style.overflow = 'hidden'
        }
        if (this.backgroundColorStyle && this.mode !== 'center') {
          style.backgroundColor = this.backgroundColorStyle
        }
        return style
      },
      // ä¸­éƒ¨å¼¹çª—的样式
      centerStyle() {
        let style = {}
        style.width = this.width ? this.$t.string.getLengthUnitValue(this.width) : this.$t.string.getLengthUnitValue(this.length)
        // ä¸­éƒ¨å¼¹å‡ºçš„æ¨¡å¼ï¼Œå¦‚果没有设置高度,就用auto值,由内容撑开
        style.height = this.height ? this.$t.string.getLengthUnitValue(this.height) : 'auto'
        style.zIndex = this.elZIndex
        if (this.negativeTop) {
          style.marginTop = `-${this.$t.string.getLengthUnitValue(this.negativeTop)}`
        }
        if (this.borderRadius) {
          style.borderRadius = `${this.borderRadius}rpx`
          style.overflow='hidden'
        }
        if (this.backgroundColorStyle) {
          style.backgroundColor = this.backgroundColorStyle
        }
        return style
      },
      // å…³é—­æŒ‰é’®æ ·å¼
      closeBtnStyle() {
        let style = {}
        if (this.closeIconColor) {
          style.color = this.closeIconColor
        }
        if (this.closeIconSize) {
          style.fontSize = this.closeIconSize + 'rpx'
        }
        return style
      },
      elZIndex() {
        return this.zIndex ? this.zIndex : this.$t.zIndex.popup
      }
    },
    data() {
      return {
        timer: null,
        visibleSync: false,
        showPopup: false,
        closeFromInner: false
      }
    },
    watch: {
      value(val) {
        if (val) {
          // console.log(this.visibleSync);
          if (this.visibleSync) {
            this.visibleSync = false
            return
          }
          this.open()
        } else if (!this.closeFromInner) {
          this.close()
        }
        this.closeFromInner = false
      }
    },
    mounted() {
      // ç»„件渲染完成时,检查value是否为true,如果是,弹出popup
      this.value && this.open()
    },
    methods: {
      // ç‚¹å‡»é®ç½©
      maskClick() {
        if (!this.maskCloseable) return
        this.close()
      },
      open() {
        this.change('visibleSync', 'showPopup', true)
      },
      // å…³é—­å¼¹æ¡†
      close() {
        // æ ‡è®°å…³é—­æ˜¯å†…部发生的,否则修改了value值,导致watch中对value检测,导致再执行一遍close
        // é€ æˆ@close事件触发两次
        this.closeFromInner = true
        this.change('showPopup', 'visibleSync', false)
      },
      // ä¸­éƒ¨å¼¹å‡ºæ—¶ï¼Œéœ€è¦.tn-drawer-content将内容居中,此元素会铺满屏幕,点击需要关闭弹窗
      // è®©å…¶åªåœ¨mode=center时起作用
      modeCenterClose() {
        if (this.mode != 'center' || !this.maskCloseable) return
        this.close()
      },
      // å…³é—­æ—¶å…ˆé€šè¿‡åŠ¨ç”»éšè—å¼¹çª—å’Œé®ç½©ï¼Œå†ç§»é™¤æ•´ä¸ªç»„ä»¶
      // æ‰“开时,先渲染组件,延时一定时间再让遮罩和弹窗的动画起作用
      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')
            clearTimeout(this.timer)
          }, 10)
          // #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')
            clearTimeout(this.timer)
          }, 250)
        }
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-popup {
    /* #ifndef APP-NVUE */
    display: block;
    /* #endif */
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    overflow: hidden;
    &__content {
      /* #ifndef APP-NVUE */
      display: block;
      /* #endif */
      position: absolute;
      transition: all 0.25s linear;
      &--visible {
        transform: translate3D(0px, 0px, 0px) !important;
        &.tn-popup--center {
          transform: scale(1);
          opacity: 1;
        }
      }
      &__center_box {
        min-width: 100rpx;
        min-height: 100rpx;
        /* #ifndef APP-NVUE */
        display: block;
        /* #endif */
        position: relative;
        background-color: #FFFFFF;
      }
      &__scroll-view {
        width: 100%;
        height: 100%;
      }
      &__center--animation-zoom {
        transform: scale(1.15);
      }
    }
    &__scroll_view {
      width: 100%;
      height: 100%;
    }
    &--left {
      top: 0;
      bottom: 0;
      left: 0;
      background-color: #FFFFFF;
    }
    &--right {
      top: 0;
      bottom: 0;
      right: 0;
      background-color: #FFFFFF;
    }
    &--top {
      left: 0;
      right: 0;
      top: 0;
      background-color: #FFFFFF;
    }
    &--bottom {
      left: 0;
      right: 0;
      bottom: 0;
      background-color: #FFFFFF;
    }
    &--center {
      display: flex;
      flex-direction: column;
      bottom: 0;
      top: 0;
      left: 0;
      right: 0;
      justify-content: center;
      align-items: center;
      opacity: 0;
    }
    &__close {
      position: absolute;
      &--top-left {
        top: 30rpx;
        left: 30rpx;
      }
      &--top-right {
        top: 30rpx;
        right: 30rpx;
      }
      &--bottom-left {
        bottom: 30rpx;
        left: 30rpx;
      }
      &--bottom-right {
        bottom: 30rpx;
        right: 30rpx;
      }
    }
    &__mask {
      width: 100%;
      height: 100%;
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      border: 0;
      background-color: $tn-mask-bg-color;
      transition: 0.25s linear;
      transition-property: opacity;
      opacity: 0;
      &--show {
        opacity: 1;
      }
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-radio-group/tn-radio-group.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,124 @@
<template>
  <view class="tn-radio-group-class tn-radio-group">
    <slot></slot>
  </view>
</template>
<script>
  import Emitter from '../../libs/utils/emitter.js'
  export default {
    mixins: [ Emitter ],
    name: 'tn-radio-group',
    props: {
      // å•选组的值,会选中值相同的选项
      value: {
        type: String,
        default: ''
      },
      // æ˜¯å¦ç¦ç”¨
      disabled: {
        type: Boolean,
        default: false
      },
      // ç¦ç”¨ç‚¹å‡»æ ‡ç­¾è¿›è¡Œé€‰æ‹©
      disabledLabel: {
        type: Boolean,
        default: false
      },
      // é€‰æ‹©æ¡†çš„形状 square æ–¹å½¢ circle åœ†å½¢
      shape: {
        type: String,
        default: 'circle'
      },
      // é€‰ä¸­æ—¶çš„颜色
      activeColor: {
        type: String,
        default: '#01BEFF'
      },
      // ç»„件大小
      size: {
        type: Number,
        default: 34
      },
      // æ¯ä¸ªradio占的宽度
      width: {
        type: String,
        default: 'auto'
      },
      // æ˜¯å¦æ¢è¡Œ
      wrap: {
        type: Boolean,
        default: false
      },
      // å›¾æ ‡å¤§å°
      iconSize: {
        type: Number,
        default: 20
      }
    },
    computed: {
      // è¿™é‡Œcomputed的变量,都是子组件tn-radio需要用到的,由于头条小程序的兼容性差异,子组件无法实时监听父组件参数的变化
      // æ‰€ä»¥éœ€è¦æ‰‹åŠ¨é€šçŸ¥å­ç»„ä»¶ï¼Œè¿™é‡Œè¿”å›žä¸€ä¸ªparentData变量,供watch监听,在其中去通知每一个子组件重新从父组件(tn-radio-group)
      // æ‹‰å–父组件新的变化后的参数
      parentData() {
        return [this.value, this.disabled, this.activeColor, this.size, this.disabledLabel, this.shape, this.iconSize, this.width, this.wrap]
      }
    },
    data() {
      return {
      }
    },
    watch: {
      // å½“父组件中的子组件需要共享的参数发生了变化,手动通知子组件
      parentData() {
        if (this.children.length) {
          this.children.map(child => {
            // åˆ¤æ–­å­ç»„ä»¶(tn-radio)如果有updateParentData方法的话,子组件重新从父组件拉取了最新的值
            typeof(child.updateParentData) === 'function' && child.updateParentData()
          })
        }
      }
    },
    created() {
      // å¦‚果将children定义在data中,在微信小程序会造成循环引用而报错
      this.children = []
    },
    methods: {
      // æ”¹æ–¹æ³•由子组件tn-radio调用,当一个tn-radio被选中的时候,给父组件设置value值(通知其他tn-radio组件)
      setValue(value) {
        // é€šè¿‡å­ç»„件传递过来的value值(此被选中的子组件内部已将parentValue设置等于value的值),将其他tn-radio设置未选中的状态
        this.children.map(child => {
          if (child.parentData.value !== value) child.parentData.value = ''
        })
        // é€šè¿‡emit事件,设置父组件通过v-model双向绑定的值
        this.$emit('input', value)
        this.$emit('change', value)
        // ç­‰å¾…下一个周期再执行,因为this.$emit('input')作用于父组件,再反馈到子组件内部,需要时间
        // ç”±äºŽå¤´æ¡å°ç¨‹åºæ‰§è¡Œè¿Ÿé’ï¼Œæ•…需要用几十毫秒的延时
        setTimeout(() => {
            // å°†å½“前的值发送到 tn-form-item è¿›è¡Œæ ¡éªŒ
            this.dispatch('tn-form-item', 'on-form-change', value);
        }, 60)
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-radio-group {
    /* #ifndef MP || APP-NVUE */
    display: inline-flex;
    flex-wrap: wrap;
    /* #endif */
    &::after {
      content: " ";
      display: table;
      clear: both;
    }
  }
</style>
´úÂë¹ÜÀí/PDA/tuniao-ui/components/tn-radio/tn-radio.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,265 @@
<template>
  <view class="tn-radio-class tn-radio" :style="[radioStyle]">
    <view
      class="tn-radio__icon-wrap"
      :class="[iconClass]"
      :style=[iconStyle]
      @tap="toggle"
    >
      <view class="tn-icon-success tn-radio__icon-wrap__icon"></view>
    </view>
    <view
      class="tn-radio__label"
      :style="{
        fontSize: labelSize ? labelSize + 'rpx' : ''
      }"
      @tap="onClickLabel"
    >
      <slot></slot>
    </view>
  </view>
</template>
<script>
  export default {
    name: 'tn-radio',
    props: {
      // radio名称
      name: {
        type: [String, Number],
        default: ''
      },
      // æ˜¯å¦ç¦ç”¨
      disabled: {
        type: Boolean,
        default: false
      },
      // ç¦ç”¨ç‚¹å‡»æ ‡ç­¾è¿›è¡Œé€‰æ‹©
      disabledLabel: {
        type: Boolean,
        default: false
      },
      // é€‰æ‹©æ¡†çš„形状 square æ–¹å½¢ circle åœ†å½¢
      shape: {
        type: String,
        default: ''
      },
      // é€‰ä¸­æ—¶çš„颜色
      activeColor: {
        type: String,
        default: ''
      },
      // ç»„件尺寸
      size: {
        type: Number,
        default: 0
      },
      // å›¾æ ‡å¤§å°
      iconSize: {
        type: Number,
        default: 0
      },
      // label字体大小
      labelSize: {
        type: Number,
        default: 0
      }
    },
    computed: {
      // ç¦ç”¨ï¼Œçˆ¶ç»„件会覆盖子组件的状态
      isDisabled() {
        return this.disabled ? this.disabled : (this.parentData.disabled ? this.parentData.disabled : false)
      },
      // ç¦ç”¨label点击,父组件会覆盖子组件的状态
      isDisabledLabel() {
        return this.disabledLabel ? this.disabledLabel : (this.parentData.disabledLabel ? this.parentData.disabledLabel : false)
      },
      // ç»„件尺寸
      elSize() {
        return this.size ? this.size : (this.parentData.size ? this.parentData.size : 34)
      },
      // ç»„件选中时的颜色
      elActiveColor() {
        return this.activeColor ? this.activeColor : (this.parentData.activeColor ? this.parentData.activeColor : '#01BEFF')
      },
      // ç»„件形状
      elShape() {
        return this.shape ? this.shape : (this.parentData.shape ? this.parentData.shape : 'circle')
      },
      iconClass() {
        let clazz = ''
        clazz += (' tn-radio__icon-wrap--' + this.elShape)
        if (this.parentData.value === this.name) clazz += ' tn-radio__icon-wrap--checked'
        if (this.isDisabled) clazz += ' tn-radio__icon-wrap--disabled'
        if (this.parentData.value === this.name && this.isDisabled) clazz += ' tn-radio__icon-wrap--disabled--checked'
        return clazz
      },
      iconStyle() {
        // å½“前radio的name等于parent的value才认为时选中
        let style = {}
        if (this.elActiveColor && this.parentData.value === this.name && !this.isDisabled) {
          style.borderColor = this.elActiveColor
          style.backgroundColor = this.elActiveColor
        }
        style.color = this.parentData.value === this.name ? '#FFFFFF' : 'transparent'
        style.width = this.elSize + 'rpx'
        style.height = style.width
        style.fontSize = (this.iconSize ? this.iconSize : (this.parentData.iconSize ? this.parentData.iconSize : 20)) + 'rpx'
        return style
      },
      radioStyle() {
        let style = {}
        if (this.parent && this.parentData.width) {
          // #ifdef MP
          // å„家小程序因为它们特殊的编译结构,使用float布局
          style.float = 'left';
          // #endif
          // #ifndef MP
          // H5和APP使用flex布局
          style.flex = `0 0 ${this.parentData.width}`;
          // #endif
        }
        if(this.parent && this.parentData.wrap) {
            style.width = '100%';
            // #ifndef MP
            // H5和APP使用flex布局,将宽度设置100%,即可自动换行
            style.flex = '0 0 100%';
            // #endif
        }
        return style
      }
    },
    data() {
      return {
        // çˆ¶ç»„件的默认值,因为头条小程序不支持在computed中使用this.parent.xxx的形式
        // æ•…只能使用如此方法
        parentData: {
          value: null,
          disabled: null,
          disabledLabel: null,
          shape: null,
          activeColor: null,
          size: null,
          width: null,
          wrap: null,
          iconSize: null,
        }
      }
    },
    created() {
      // æ”¯ä»˜å®å°ç¨‹åºä¸æ”¯æŒprovide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环引用
      this.updateParentData()
      this.parent.children.push(this)
    },
    methods: {
      updateParentData() {
        this.getParentData('tn-radio-group')
      },
      onClickLabel() {
        if (!this.isDisabledLabel && !this.isDisabled) {
          this.setRadioCheckedStatus()
        }
      },
      toggle() {
        if (!this.isDisabled) {
          this.setRadioCheckedStatus()
        }
      },
      emitEvent() {
        // tn-radio的name不等于父组件的v-model的值时(意味着未选中),才发出事件,避免多次点击触发事件
        if (this.parentData.value !== this.name) this.$emit('change', this.name)
      },
      // æ”¹å˜é€‰ä¸­çš„状态
      // æ›´æ”¹æœ¬ç»„ä»¶çš„parentData.value的值为本组件的name值,同时通过父组件遍历所有的radio实例
      // å°†æœ¬ç»„件外的其他radio的parentData.value都设置为空
      setRadioCheckedStatus() {
        this.emitEvent()
        if (this.parent) {
          this.parent.setValue(this.name)
          this.parentData.value = this.name
        }
      }
    }
  }
</script>
<style lang="scss" scoped>
  .tn-radio {
    /* #ifndef APP-NVUE */
    display: inline-flex;
    /* #endif */
    align-items: center;
    overflow: hidden;
    user-select: none;
    line-height: 1.8;
    &__icon-wrap {
      color: $tn-font-color;
      display: flex;
      flex-direction: row;
      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: 20rpx;
      border: 1rpx solid $tn-font-sub-color;
      transition-duration: 0.2s;
      /* #ifdef MP-TOUTIAO */
      // å¤´æ¡å°ç¨‹åºå…¼å®¹æ€§é—®é¢˜ï¼Œéœ€è¦è®¾ç½®è¡Œé«˜ä¸º0,否则图标偏下
      &__icon {
          line-height: 0;
      }
      /* #endif */
      &--circle {
        border-radius: 100%;
      }
      &--square {
        border-radius: 6rpx;
      }
      &--checked {
        color: #FFFFFF;
        background-color: $tn-main-color;
        border-color: $tn-main-color;
      }
      &--disabled {
        background-color: $tn-font-holder-color;
        border-color: $tn-font-sub-color;
      }
      &--disabled--checked {
        color: $tn-font-sub-color !important;
      }
    }
    &__label {
      word-wrap: break-word;
      margin-left: 10rpx;
      margin-right: 24rpx;
      color: $tn-font-color;
      font-size: 30rpx;
      &--disabled {
        color: $tn-font-sub-color;
      }
    }
  }
</style>
在上述文件截断后对比
代码管理/PDA/tuniao-ui/components/tn-rate/tn-rate.vue 代码管理/PDA/tuniao-ui/components/tn-read-more/tn-read-more.vue 代码管理/PDA/tuniao-ui/components/tn-row-notice/tn-row-notice.vue 代码管理/PDA/tuniao-ui/components/tn-scroll-list/tn-scroll-list.vue 代码管理/PDA/tuniao-ui/components/tn-scroll-view/tn-scroll-view.vue 代码管理/PDA/tuniao-ui/components/tn-select/tn-select.vue 代码管理/PDA/tuniao-ui/components/tn-sign-board/tn-sign-board.vue 代码管理/PDA/tuniao-ui/components/tn-skeleton/tn-skeleton.vue 代码管理/PDA/tuniao-ui/components/tn-slider/tn-slider.vue 代码管理/PDA/tuniao-ui/components/tn-stack-swiper/index-h5.wxs 代码管理/PDA/tuniao-ui/components/tn-stack-swiper/index.wxs 代码管理/PDA/tuniao-ui/components/tn-stack-swiper/tn-stack-swiper.vue 代码管理/PDA/tuniao-ui/components/tn-steps/tn-steps.vue 代码管理/PDA/tuniao-ui/components/tn-sticky/tn-sticky.vue 代码管理/PDA/tuniao-ui/components/tn-subsection/tn-subsection.vue 代码管理/PDA/tuniao-ui/components/tn-swipe-action-item/index.wxs 代码管理/PDA/tuniao-ui/components/tn-swipe-action-item/tn-swipe-action-item.vue 代码管理/PDA/tuniao-ui/components/tn-swipe-action/tn-swipe-action.vue 代码管理/PDA/tuniao-ui/components/tn-swiper/tn-swiper.vue 代码管理/PDA/tuniao-ui/components/tn-switch/tn-switch.vue 代码管理/PDA/tuniao-ui/components/tn-tabbar/tn-tabbar.vue 代码管理/PDA/tuniao-ui/components/tn-table/tn-table.vue 代码管理/PDA/tuniao-ui/components/tn-tabs-swiper/tn-tabs-swiper.vue 代码管理/PDA/tuniao-ui/components/tn-tabs/tn-tabs.vue 代码管理/PDA/tuniao-ui/components/tn-tag/tn-tag.vue 代码管理/PDA/tuniao-ui/components/tn-td/tn-td.vue 代码管理/PDA/tuniao-ui/components/tn-time-line-item/tn-time-line-item.vue 代码管理/PDA/tuniao-ui/components/tn-time-line-item/tn-time-line-item.vue_bk 代码管理/PDA/tuniao-ui/components/tn-time-line/tn-time-line.vue 代码管理/PDA/tuniao-ui/components/tn-time-line/tn-time-line.vue_bk 代码管理/PDA/tuniao-ui/components/tn-tips/tn-tips.vue 代码管理/PDA/tuniao-ui/components/tn-toast/tn-toast.vue 代码管理/PDA/tuniao-ui/components/tn-tr/tn-tr.vue 代码管理/PDA/tuniao-ui/components/tn-tree-node/tn-tree-node.vue 代码管理/PDA/tuniao-ui/components/tn-tree-view/tn-tree-view.vue 代码管理/PDA/tuniao-ui/components/tn-verification-code-input/tn-verification-code-input.vue 代码管理/PDA/tuniao-ui/components/tn-verification-code/tn-verification-code.vue 代码管理/PDA/tuniao-ui/components/tn-waterfall/tn-waterfall.vue 代码管理/PDA/tuniao-ui/iconfont.css 代码管理/PDA/tuniao-ui/index.js 代码管理/PDA/tuniao-ui/index.scss 代码管理/PDA/tuniao-ui/libs/config/color.js 代码管理/PDA/tuniao-ui/libs/config/zIndex.js 代码管理/PDA/tuniao-ui/libs/css/color.scss 代码管理/PDA/tuniao-ui/libs/css/main.scss 代码管理/PDA/tuniao-ui/libs/css/style.h5.scss 代码管理/PDA/tuniao-ui/libs/css/style.mp.scss 代码管理/PDA/tuniao-ui/libs/function/$parent.js 代码管理/PDA/tuniao-ui/libs/function/array.js 代码管理/PDA/tuniao-ui/libs/function/color.js 代码管理/PDA/tuniao-ui/libs/function/colorUtils.js 代码管理/PDA/tuniao-ui/libs/function/deepClone.js 代码管理/PDA/tuniao-ui/libs/function/message.js 代码管理/PDA/tuniao-ui/libs/function/messageUtils.js 代码管理/PDA/tuniao-ui/libs/function/number.js 代码管理/PDA/tuniao-ui/libs/function/string.js 代码管理/PDA/tuniao-ui/libs/function/test.js 代码管理/PDA/tuniao-ui/libs/function/updateCustomBarInfo.js 代码管理/PDA/tuniao-ui/libs/function/uuid.js 代码管理/PDA/tuniao-ui/libs/mixin/components_color.js 代码管理/PDA/tuniao-ui/libs/mixin/mixin.js 代码管理/PDA/tuniao-ui/libs/mixin/mpShare.js 代码管理/PDA/tuniao-ui/libs/mixin/touch.js 代码管理/PDA/tuniao-ui/libs/utils/area.js 代码管理/PDA/tuniao-ui/libs/utils/async-validator.js 代码管理/PDA/tuniao-ui/libs/utils/calendar.js 代码管理/PDA/tuniao-ui/libs/utils/city.js 代码管理/PDA/tuniao-ui/libs/utils/emitter.js 代码管理/PDA/tuniao-ui/libs/utils/province.js 代码管理/PDA/tuniao-ui/theme.scss 代码管理/PDA/uni.scss 代码管理/PDA/unpackage/cache/apk/__UNI__3B0DA10_cm.apk 代码管理/PDA/unpackage/cache/apk/__UNI__4120374_cm.apk 代码管理/PDA/unpackage/cache/apk/__UNI__F8536D6_cm.apk 代码管理/PDA/unpackage/cache/apk/apkurl 代码管理/PDA/unpackage/cache/certdata 代码管理/PDA/unpackage/cache/cloudcertificate/certini 代码管理/PDA/unpackage/cache/cloudcertificate/package.keystore 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappchooselocation.js 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniapperror.png 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappes6.js 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappopenlocation.js 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniapppicker.js 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappquill.js 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappquillimageresize.js 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappscan.js 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappsuccess.png 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/__uniappview.html 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/app-config-service.js 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/app-config.js 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/app-service.js 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/app-view.js 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/center-selected.png 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/center.png 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/favicon.ico 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/index-selected.png 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/index.png 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/login_bottom_bg.jpg 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/login_top2.jpg 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/login_top3.png 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/logo.png 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/myIcon/1.png 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/myIcon/2.png 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/static/myIcon/qiu.png 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/view.css 代码管理/PDA/unpackage/cache/wgt/__UNI__3B0DA10/view.umd.min.js 代码管理/PDA/unpackage/dist/build/app-plus/__uniappchooselocation.js 代码管理/PDA/unpackage/dist/build/app-plus/__uniapperror.png 代码管理/PDA/unpackage/dist/build/app-plus/__uniappes6.js 代码管理/PDA/unpackage/dist/build/app-plus/__uniappopenlocation.js 代码管理/PDA/unpackage/dist/build/app-plus/__uniapppicker.js 代码管理/PDA/unpackage/dist/build/app-plus/__uniappquill.js 代码管理/PDA/unpackage/dist/build/app-plus/__uniappquillimageresize.js 代码管理/PDA/unpackage/dist/build/app-plus/__uniappscan.js 代码管理/PDA/unpackage/dist/build/app-plus/__uniappsuccess.png 代码管理/PDA/unpackage/dist/build/app-plus/__uniappview.html 代码管理/PDA/unpackage/dist/build/app-plus/app-config-service.js 代码管理/PDA/unpackage/dist/build/app-plus/app-config.js 代码管理/PDA/unpackage/dist/build/app-plus/app-service.js 代码管理/PDA/unpackage/dist/build/app-plus/app-view.js 代码管理/PDA/unpackage/dist/build/app-plus/static/center-selected.png 代码管理/PDA/unpackage/dist/build/app-plus/static/center.png 代码管理/PDA/unpackage/dist/build/app-plus/static/favicon.ico 代码管理/PDA/unpackage/dist/build/app-plus/static/index-selected.png 代码管理/PDA/unpackage/dist/build/app-plus/static/index.png 代码管理/PDA/unpackage/dist/build/app-plus/static/login_bottom_bg.jpg 代码管理/PDA/unpackage/dist/build/app-plus/static/login_top2.jpg 代码管理/PDA/unpackage/dist/build/app-plus/static/login_top3.png 代码管理/PDA/unpackage/dist/build/app-plus/static/logo.png 代码管理/PDA/unpackage/dist/build/app-plus/static/myIcon/1.png 代码管理/PDA/unpackage/dist/build/app-plus/static/myIcon/2.png 代码管理/PDA/unpackage/dist/build/app-plus/static/myIcon/qiu.png 代码管理/PDA/unpackage/dist/build/app-plus/view.css 代码管理/PDA/unpackage/dist/build/app-plus/view.umd.min.js 代码管理/PDA/unpackage/dist/dev/app-plus/__uniappchooselocation.js 代码管理/PDA/unpackage/dist/dev/app-plus/__uniapperror.png 代码管理/PDA/unpackage/dist/dev/app-plus/__uniappes6.js 代码管理/PDA/unpackage/dist/dev/app-plus/__uniappopenlocation.js 代码管理/PDA/unpackage/dist/dev/app-plus/__uniapppicker.js 代码管理/PDA/unpackage/dist/dev/app-plus/__uniappquill.js 代码管理/PDA/unpackage/dist/dev/app-plus/__uniappquillimageresize.js 代码管理/PDA/unpackage/dist/dev/app-plus/__uniappscan.js 代码管理/PDA/unpackage/dist/dev/app-plus/__uniappsuccess.png 代码管理/PDA/unpackage/dist/dev/app-plus/__uniappview.html 代码管理/PDA/unpackage/dist/dev/app-plus/app-config-service.js 代码管理/PDA/unpackage/dist/dev/app-plus/app-config.js 代码管理/PDA/unpackage/dist/dev/app-plus/app-service.js 代码管理/PDA/unpackage/dist/dev/app-plus/app-view.js 代码管理/PDA/unpackage/dist/dev/app-plus/static/center-selected.png 代码管理/PDA/unpackage/dist/dev/app-plus/static/center.png 代码管理/PDA/unpackage/dist/dev/app-plus/static/favicon.ico 代码管理/PDA/unpackage/dist/dev/app-plus/static/index-selected.png 代码管理/PDA/unpackage/dist/dev/app-plus/static/index.png 代码管理/PDA/unpackage/dist/dev/app-plus/static/login_bottom_bg.jpg 代码管理/PDA/unpackage/dist/dev/app-plus/static/login_top2.jpg 代码管理/PDA/unpackage/dist/dev/app-plus/static/login_top3.png 代码管理/PDA/unpackage/dist/dev/app-plus/static/logo.png 代码管理/PDA/unpackage/dist/dev/app-plus/view.css 代码管理/PDA/unpackage/dist/dev/app-plus/view.umd.min.js 代码管理/PDA/unpackage/release/apk/__UNI__3B0DA10__20240304182646.apk 代码管理/PDA/unpackage/release/apk/__UNI__3B0DA10__20240312142901.apk 代码管理/PDA/unpackage/uni_modules/uni-icons/changelog.md 代码管理/PDA/unpackage/uni_modules/uni-icons/components/uni-icons/icons.js 代码管理/PDA/unpackage/uni_modules/uni-icons/components/uni-icons/uni-icons.vue 代码管理/PDA/unpackage/uni_modules/uni-icons/components/uni-icons/uni.ttf 代码管理/PDA/unpackage/uni_modules/uni-icons/readme.md 代码管理/PDA/uview-ui/LICENSE 代码管理/PDA/uview-ui/README.md 代码管理/PDA/uview-ui/components/u-action-sheet/u-action-sheet.vue 代码管理/PDA/uview-ui/components/u-alert-tips/u-alert-tips.vue 代码管理/PDA/uview-ui/components/u-avatar-cropper/u-avatar-cropper.vue 代码管理/PDA/uview-ui/components/u-avatar-cropper/weCropper.js 代码管理/PDA/uview-ui/components/u-avatar/u-avatar.vue 代码管理/PDA/uview-ui/components/u-back-top/u-back-top.vue 代码管理/PDA/uview-ui/components/u-badge/u-badge.vue 代码管理/PDA/uview-ui/components/u-button/u-button.vue 代码管理/PDA/uview-ui/components/u-calendar/u-calendar.vue 代码管理/PDA/uview-ui/components/u-car-keyboard/u-car-keyboard.vue 代码管理/PDA/uview-ui/components/u-card/u-card.vue 代码管理/PDA/uview-ui/components/u-cell-group/u-cell-group.vue 代码管理/PDA/uview-ui/components/u-cell-item/u-cell-item.vue 代码管理/PDA/uview-ui/components/u-checkbox-group/u-checkbox-group.vue 代码管理/PDA/uview-ui/components/u-checkbox/u-checkbox.vue 代码管理/PDA/uview-ui/components/u-circle-progress/u-circle-progress.vue 代码管理/PDA/uview-ui/components/u-col/u-col.vue 代码管理/PDA/uview-ui/components/u-collapse-item/u-collapse-item.vue 代码管理/PDA/uview-ui/components/u-collapse/u-collapse.vue 代码管理/PDA/uview-ui/components/u-column-notice/u-column-notice.vue 代码管理/PDA/uview-ui/components/u-count-down/u-count-down.vue 代码管理/PDA/uview-ui/components/u-count-to/u-count-to.vue 代码管理/PDA/uview-ui/components/u-divider/u-divider.vue 代码管理/PDA/uview-ui/components/u-dropdown-item/u-dropdown-item.vue 代码管理/PDA/uview-ui/components/u-dropdown/u-dropdown.vue 代码管理/PDA/uview-ui/components/u-empty/u-empty.vue 代码管理/PDA/uview-ui/components/u-field/u-field.vue 代码管理/PDA/uview-ui/components/u-form-item/u-form-item.vue 代码管理/PDA/uview-ui/components/u-form/u-form.vue 代码管理/PDA/uview-ui/components/u-full-screen/u-full-screen.vue 代码管理/PDA/uview-ui/components/u-gap/u-gap.vue 代码管理/PDA/uview-ui/components/u-grid-item/u-grid-item.vue 代码管理/PDA/uview-ui/components/u-grid/u-grid.vue 代码管理/PDA/uview-ui/components/u-icon/u-icon.vue 代码管理/PDA/uview-ui/components/u-image/u-image.vue 代码管理/PDA/uview-ui/components/u-index-anchor/u-index-anchor.vue 代码管理/PDA/uview-ui/components/u-index-list/u-index-list.vue 代码管理/PDA/uview-ui/components/u-input/u-input.vue 代码管理/PDA/uview-ui/components/u-keyboard/u-keyboard.vue 代码管理/PDA/uview-ui/components/u-lazy-load/u-lazy-load.vue 代码管理/PDA/uview-ui/components/u-line-progress/u-line-progress.vue 代码管理/PDA/uview-ui/components/u-line/u-line.vue 代码管理/PDA/uview-ui/components/u-link/u-link.vue 代码管理/PDA/uview-ui/components/u-loading-page/u-loading-page.vue 代码管理/PDA/uview-ui/components/u-loading/u-loading.vue 代码管理/PDA/uview-ui/components/u-loadmore/u-loadmore.vue 代码管理/PDA/uview-ui/components/u-mask/u-mask.vue 代码管理/PDA/uview-ui/components/u-message-input/u-message-input.vue 代码管理/PDA/uview-ui/components/u-modal/u-modal.vue 代码管理/PDA/uview-ui/components/u-navbar/u-navbar.vue 代码管理/PDA/uview-ui/components/u-no-network/u-no-network.vue 代码管理/PDA/uview-ui/components/u-notice-bar/u-notice-bar.vue 代码管理/PDA/uview-ui/components/u-number-box/u-number-box.vue 代码管理/PDA/uview-ui/components/u-number-keyboard/u-number-keyboard.vue 代码管理/PDA/uview-ui/components/u-parse/libs/CssHandler.js 代码管理/PDA/uview-ui/components/u-parse/libs/MpHtmlParser.js 代码管理/PDA/uview-ui/components/u-parse/libs/config.js 代码管理/PDA/uview-ui/components/u-parse/libs/handler.wxs 代码管理/PDA/uview-ui/components/u-parse/libs/trees.vue 代码管理/PDA/uview-ui/components/u-parse/u-parse.vue 代码管理/PDA/uview-ui/components/u-picker/u-picker.vue 代码管理/PDA/uview-ui/components/u-popup/u-popup.vue 代码管理/PDA/uview-ui/components/u-radio-group/u-radio-group.vue 代码管理/PDA/uview-ui/components/u-radio/u-radio.vue 代码管理/PDA/uview-ui/components/u-rate/u-rate.vue 代码管理/PDA/uview-ui/components/u-read-more/u-read-more.vue 代码管理/PDA/uview-ui/components/u-row-notice/u-row-notice.vue 代码管理/PDA/uview-ui/components/u-row/u-row.vue 代码管理/PDA/uview-ui/components/u-search/u-search.vue 代码管理/PDA/uview-ui/components/u-section/u-section.vue 代码管理/PDA/uview-ui/components/u-select/u-select.vue 代码管理/PDA/uview-ui/components/u-skeleton/u-skeleton.vue 代码管理/PDA/uview-ui/components/u-slider/u-slider.vue 代码管理/PDA/uview-ui/components/u-steps/u-steps.vue 代码管理/PDA/uview-ui/components/u-sticky/u-sticky.vue 代码管理/PDA/uview-ui/components/u-subsection/u-subsection.vue 代码管理/PDA/uview-ui/components/u-swipe-action/u-swipe-action.vue 代码管理/PDA/uview-ui/components/u-swiper/u-swiper.vue 代码管理/PDA/uview-ui/components/u-switch/u-switch.vue 代码管理/PDA/uview-ui/components/u-tabbar/u-tabbar.vue 代码管理/PDA/uview-ui/components/u-table/u-table.vue 代码管理/PDA/uview-ui/components/u-tabs-swiper/u-tabs-swiper.vue 代码管理/PDA/uview-ui/components/u-tabs/u-tabs.vue 代码管理/PDA/uview-ui/components/u-tag/u-tag.vue 代码管理/PDA/uview-ui/components/u-td/u-td.vue 代码管理/PDA/uview-ui/components/u-th/u-th.vue 代码管理/PDA/uview-ui/components/u-time-line-item/u-time-line-item.vue 代码管理/PDA/uview-ui/components/u-time-line/u-time-line.vue 代码管理/PDA/uview-ui/components/u-toast/u-toast.vue 代码管理/PDA/uview-ui/components/u-top-tips/u-top-tips.vue 代码管理/PDA/uview-ui/components/u-tr/u-tr.vue 代码管理/PDA/uview-ui/components/u-upload/u-upload.vue 代码管理/PDA/uview-ui/components/u-verification-code/u-verification-code.vue 代码管理/PDA/uview-ui/components/u-waterfall/u-waterfall.vue 代码管理/PDA/uview-ui/iconfont.css 代码管理/PDA/uview-ui/index.js 代码管理/PDA/uview-ui/index.scss 代码管理/PDA/uview-ui/libs/config/config.js 代码管理/PDA/uview-ui/libs/config/zIndex.js 代码管理/PDA/uview-ui/libs/css/color.scss 代码管理/PDA/uview-ui/libs/css/common.scss 代码管理/PDA/uview-ui/libs/css/style.components.scss 代码管理/PDA/uview-ui/libs/css/style.h5.scss 代码管理/PDA/uview-ui/libs/css/style.mp.scss 代码管理/PDA/uview-ui/libs/css/style.nvue.scss 代码管理/PDA/uview-ui/libs/css/style.vue.scss 代码管理/PDA/uview-ui/libs/function/$parent.js 代码管理/PDA/uview-ui/libs/function/addUnit.js 代码管理/PDA/uview-ui/libs/function/bem.js 代码管理/PDA/uview-ui/libs/function/color.js 代码管理/PDA/uview-ui/libs/function/colorGradient.js 代码管理/PDA/uview-ui/libs/function/debounce.js 代码管理/PDA/uview-ui/libs/function/deepClone.js 代码管理/PDA/uview-ui/libs/function/deepMerge.js 代码管理/PDA/uview-ui/libs/function/getParent.js 代码管理/PDA/uview-ui/libs/function/guid.js 代码管理/PDA/uview-ui/libs/function/md5.js 代码管理/PDA/uview-ui/libs/function/queryParams.js 代码管理/PDA/uview-ui/libs/function/random.js 代码管理/PDA/uview-ui/libs/function/randomArray.js 代码管理/PDA/uview-ui/libs/function/route.js 代码管理/PDA/uview-ui/libs/function/sys.js 代码管理/PDA/uview-ui/libs/function/test.js 代码管理/PDA/uview-ui/libs/function/throttle.js 代码管理/PDA/uview-ui/libs/function/timeFormat.js 代码管理/PDA/uview-ui/libs/function/timeFrom.js 代码管理/PDA/uview-ui/libs/function/toast.js 代码管理/PDA/uview-ui/libs/function/trim.js 代码管理/PDA/uview-ui/libs/function/type2icon.js 代码管理/PDA/uview-ui/libs/mixin/mixin.js 代码管理/PDA/uview-ui/libs/mixin/mpShare.js 代码管理/PDA/uview-ui/libs/request/index.js 代码管理/PDA/uview-ui/libs/store/index.js 代码管理/PDA/uview-ui/libs/util/area.js 代码管理/PDA/uview-ui/libs/util/async-validator.js 代码管理/PDA/uview-ui/libs/util/city.js 代码管理/PDA/uview-ui/libs/util/emitter.js 代码管理/PDA/uview-ui/libs/util/province.js 代码管理/PDA/uview-ui/theme.scss 代码管理/PDA/vue.config.js