Explorar o código

20230807初始化

15143018065 hai 1 ano
achega
90a66b9100
Modificáronse 100 ficheiros con 7257 adicións e 0 borrados
  1. 1 0
      .gitignore
  2. 20 0
      .hbuilderx/launch.json
  3. 75 0
      App.vue
  4. 191 0
      LICENSE
  5. 69 0
      README.md
  6. 14 0
      androidPrivacy.json
  7. 47 0
      api/address.js
  8. 11 0
      api/article/category.js
  9. 17 0
      api/article/index.js
  10. 11 0
      api/balance.js
  11. 11 0
      api/balance/log.js
  12. 17 0
      api/captcha.js
  13. 35 0
      api/cart.js
  14. 11 0
      api/category/index.js
  15. 19 0
      api/checkout.js
  16. 23 0
      api/comment.js
  17. 16 0
      api/coupon.js
  18. 11 0
      api/express.js
  19. 23 0
      api/goods.js
  20. 11 0
      api/goods/service.js
  21. 11 0
      api/help.js
  22. 23 0
      api/login/index.js
  23. 22 0
      api/myCoupon.js
  24. 47 0
      api/order.js
  25. 17 0
      api/order/comment.js
  26. 13 0
      api/page.js
  27. 11 0
      api/points/log.js
  28. 11 0
      api/recharge.js
  29. 11 0
      api/recharge/order.js
  30. 11 0
      api/recharge/plan.js
  31. 35 0
      api/refund.js
  32. 17 0
      api/region.js
  33. 11 0
      api/setting.js
  34. 18 0
      api/upload.js
  35. 34 0
      api/user.js
  36. 11 0
      api/user/coupon.js
  37. 44 0
      app.scss
  38. 3 0
      common/constant/index.js
  39. 7 0
      common/constant/paginate.js
  40. 10 0
      common/enum/coupon/ApplyRange.js
  41. 10 0
      common/enum/coupon/CouponType.js
  42. 10 0
      common/enum/coupon/ExpireType.js
  43. 5 0
      common/enum/coupon/index.js
  44. 85 0
      common/enum/enum.js
  45. 10 0
      common/enum/goods/SpecType.js
  46. 3 0
      common/enum/goods/index.js
  47. 10 0
      common/enum/order/DeliveryStatus.js
  48. 9 0
      common/enum/order/DeliveryType.js
  49. 11 0
      common/enum/order/OrderSource.js
  50. 12 0
      common/enum/order/OrderStatus.js
  51. 10 0
      common/enum/order/PayStatus.js
  52. 10 0
      common/enum/order/PayType.js
  53. 10 0
      common/enum/order/ReceiptStatus.js
  54. 17 0
      common/enum/order/index.js
  55. 11 0
      common/enum/order/refund/AuditStatus.js
  56. 12 0
      common/enum/order/refund/RefundStatus.js
  57. 10 0
      common/enum/order/refund/RefundType.js
  58. 9 0
      common/enum/order/refund/index.js
  59. 27 0
      common/enum/setting/Key.js
  60. 12 0
      common/enum/store/page/category/Style.js
  61. 3 0
      common/enum/store/page/category/index.js
  62. 57 0
      common/model/Region.js
  63. 77 0
      common/model/Setting.js
  64. 36 0
      components/add-cart-btn/index.vue
  65. 173 0
      components/add-cart-popup/index.vue
  66. 57 0
      components/avatar-image/index.vue
  67. 66 0
      components/empty/index.vue
  68. 1364 0
      components/goods-sku-popup/index.vue
  69. 450 0
      components/goods-sku-popup/number-box/index.vue
  70. 55 0
      components/mescroll-uni/components/mescroll-down.css
  71. 47 0
      components/mescroll-uni/components/mescroll-down.vue
  72. 90 0
      components/mescroll-uni/components/mescroll-empty.vue
  73. 83 0
      components/mescroll-uni/components/mescroll-top.vue
  74. 47 0
      components/mescroll-uni/components/mescroll-up.css
  75. 39 0
      components/mescroll-uni/components/mescroll-up.vue
  76. 19 0
      components/mescroll-uni/mescroll-body.css
  77. 352 0
      components/mescroll-uni/mescroll-body.vue
  78. 65 0
      components/mescroll-uni/mescroll-mixins.js
  79. 37 0
      components/mescroll-uni/mescroll-uni-option.js
  80. 36 0
      components/mescroll-uni/mescroll-uni.css
  81. 799 0
      components/mescroll-uni/mescroll-uni.js
  82. 424 0
      components/mescroll-uni/mescroll-uni.vue
  83. 48 0
      components/mescroll-uni/mixins/mescroll-comp.js
  84. 59 0
      components/mescroll-uni/mixins/mescroll-more-item.js
  85. 74 0
      components/mescroll-uni/mixins/mescroll-more.js
  86. 109 0
      components/mescroll-uni/wxs/mixins.js
  87. 92 0
      components/mescroll-uni/wxs/renderjs.js
  88. 268 0
      components/mescroll-uni/wxs/wxs.wxs
  89. 123 0
      components/page/article/index.vue
  90. 154 0
      components/page/banner/index.vue
  91. 31 0
      components/page/blank/index.vue
  92. 256 0
      components/page/goods/index.vue
  93. 36 0
      components/page/guide/index.vue
  94. 47 0
      components/page/image/index.vue
  95. 108 0
      components/page/index.vue
  96. 23 0
      components/page/mixin.js
  97. 87 0
      components/page/navBar/index.vue
  98. 40 0
      components/page/notice/index.vue
  99. 33 0
      components/page/richText/index.vue
  100. 0 0
      components/page/search/index.vue

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+/unpackage

+ 20 - 0
.hbuilderx/launch.json

@@ -0,0 +1,20 @@
+{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
+  // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
+    "version": "0.0",
+    "configurations": [{
+     	"default" : 
+     	{
+     		"launchtype" : "remote"
+     	},
+     	"h5" : 
+     	{
+     		"launchtype" : "remote"
+     	},
+     	"mp-weixin" : 
+     	{
+     		"launchtype" : "remote"
+     	},
+     	"type" : "uniCloud"
+     }
+    ]
+}

+ 75 - 0
App.vue

@@ -0,0 +1,75 @@
+<script>
+  export default {
+
+    /**
+     * 全局变量
+     */
+    globalData: {
+
+    },
+
+    /**
+     * 初始化完成时触发
+     */
+    onLaunch() {
+      // 小程序主动更新
+      this.updateManager()
+    },
+
+    methods: {
+
+      /**
+       * 小程序主动更新
+       */
+      updateManager() {
+        const updateManager = uni.getUpdateManager();
+        updateManager.onCheckForUpdate(res => {
+          // 请求完新版本信息的回调
+          // console.log(res.hasUpdate)
+        })
+        updateManager.onUpdateReady(() => {
+          uni.showModal({
+            title: '更新提示',
+            content: '新版本已经准备好,即将重启应用',
+            showCancel: false,
+            success(res) {
+              if (res.confirm) {
+                // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
+                updateManager.applyUpdate()
+              }
+            }
+          })
+        })
+        updateManager.onUpdateFailed(() => {
+          // 新的版本下载失败
+          uni.showModal({
+            title: '更新提示',
+            content: '新版本下载失败',
+            showCancel: false
+          })
+        })
+      }
+
+    }
+
+  }
+</script>
+
+<style lang="scss">
+  /* 引入uView库样式 */
+  @import "uview-ui/index.scss";
+</style>
+
+<style>
+  /* 项目基础样式 */
+  @import "./app.scss";
+
+  .uni-app--showlayout+uni-tabbar.uni-tabbar-bottom,
+  .uni-app--showlayout+uni-tabbar.uni-tabbar-bottom .uni-tabbar,
+  .uni-app--showlayout+uni-tabbar.uni-tabbar-top,
+  .uni-app--showlayout+uni-tabbar.uni-tabbar-top .uni-tabbar {
+    left: var(--window-left);
+    right: var(--window-right);
+  }
+
+</style>

+ 191 - 0
LICENSE

@@ -0,0 +1,191 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "{}" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification within
+third-party archives.
+
+   Copyright 2018 萤火科技
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 69 - 0
README.md

@@ -0,0 +1,69 @@
+# 萤火商城V2.0开源版 [uni-app端]
+
+#### 项目介绍
+萤火商城V2.0,是2021年全新推出的一款轻量级、高性能、前后端分离的电商系统,支持微信小程序 + H5+ 公众号 + APP,前后端源码完全开源,看见及所得,完美支持二次开发,可学习可商用,让您快速搭建个性化独立商城。
+
+    如果对您有帮助,您可以点右上角 “Star” 收藏一下 ,获取第一时间更新,谢谢!
+
+#### 源码下载
+1. 主商城端(又称后端、服务端,PHP开发 用于管理后台和提供api接口)
+
+    下载地址:[https://gitee.com/xany/yoshop2.0](https://gitee.com/xany/yoshop2.0)
+
+2. 用户端(也叫客户端、前端,uniapp开发 用于生成H5和微信小程序)
+
+    下载地址:[https://gitee.com/xany/yoshop2.0-uniapp](https://gitee.com/xany/yoshop2.0-uniapp)
+
+#### 如何使用uni-app端
+
+##### 一、导入uniapp项目
+
+    1. 首先下载HBuilderX并安装,地址:https://www.dcloud.io/hbuilderx.html
+    2. 打开HBuilderX -> 顶部菜单栏 -> 文件 -> 导入 -> 从本地目录导入 -> 选择uniapp端项目目录
+    3. 找到config.js文件,找到里面的apiUrl项,填入已搭建的后端url地址
+    4. 打开manifest.json文件,选择微信小程序配置,填写小程序的appid
+
+##### 二、本地调试
+
+    1. 打开HBuilderX -> 顶部菜单栏 -> 运行 -> 运行到浏览器 -> Chrome
+    2. 如果请求后端api时 提示跨域错误,可安装Chrome插件:【Allow CORS: Access-Control-Allow-Origin】,地址:https://chrome.google.com/webstore/detail/allow-cors-access-control/lhobafahddgcelffkeicbaginigeejlf
+
+##### 三、打包发行(H5)
+
+    1. 打开HBuilderX -> 顶部菜单栏 -> 发行 -> 网站H5-手机版
+    2. 打包后的文件路径:/unpackage/dist/build/h5
+    3. 将打包完成的所有文件 复制到商城后端/pulic目录下,全部替换
+
+##### 四、打包发行(微信小程序)
+
+    1. 下载微信开发者工具并安装,地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html
+    2. 打开HBuilderX -> 顶部菜单栏 -> 发行 -> 小程序-微信
+    3. 打包后的文件路径:/unpackage/dist/build/mp-weixin
+    5. 打开微信开发者工具 导入 打包完成的项目
+    6. 检查没有运行错误,在右上方上传小程序
+
+
+#### UNIAPP-页面展示
+![前端展示](https://images.gitee.com/uploads/images/2021/0316/215102_7bcb0802_2166072.png "前端展示.png")
+
+#### 系统演示
+![前端演示二维码](https://images.gitee.com/uploads/images/2021/0316/104516_3778337e_2166072.png "111.png")
+
+
+
+
+#### 版权须知
+
+1. 允许个人学习研究使用,支持二次开发,允许商业用途(仅限自运营)。
+2. 允许商业用途,但仅限自运营,如果商用必须保留版权信息,望自觉遵守。
+3. 不允许对程序代码以任何形式任何目的的再发行或出售,否则将追究侵权者法律责任。
+
+
+本项目包含的第三方源码和二进制文件之版权信息另行标注。
+
+版权所有Copyright © 2017-2021 By 萤火科技 (https://www.yiovo.com) All rights reserved。
+
+
+
+
+

+ 14 - 0
androidPrivacy.json

@@ -0,0 +1,14 @@
+{
+    "version" : "1",
+    "prompt" : "template",
+    "title" : "服务协议和隐私政策",
+    "message" : "  请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/>  你可阅读<a href=\"static/protocol.html\">《服务协议》</a>和<a href=\"static/privacy.html\">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。",
+    "buttonAccept" : "同意并接受",
+    "buttonRefuse" : "暂不同意",
+    "second" : {
+        "title" : "确认提示",
+        "message" : "  进入应用前,你需先同意<a href=\"static/protocol.html\">《服务协议》</a>和<a href=\"static/privacy.html\">《隐私政策》</a>,否则将退出应用。",
+        "buttonAccept" : "同意并继续",
+        "buttonRefuse" : "退出应用"
+    }
+}

+ 47 - 0
api/address.js

@@ -0,0 +1,47 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'address/list',
+  defaultId: 'address/defaultId',
+  detail: 'address/detail',
+  add: 'address/add',
+  edit: 'address/edit',
+  setDefault: 'address/setDefault',
+  remove: 'address/remove'
+}
+
+// 收货地址列表
+export const list = (param) => {
+  return request.get(api.list, param)
+}
+
+// 默认收货地址ID
+export const defaultId = (param) => {
+  return request.get(api.defaultId, param)
+}
+
+// 收货地址详情
+export const detail = (addressId) => {
+  return request.get(api.detail, { addressId })
+}
+
+// 新增收货地址
+export const add = (data) => {
+  return request.post(api.add, { form: data })
+}
+
+// 编辑收货地址
+export const edit = (addressId, data) => {
+  return request.post(api.edit, { addressId, form: data })
+}
+
+// 设置默认收货地址
+export const setDefault = (addressId) => {
+  return request.post(api.setDefault, { addressId })
+}
+
+// 删除收货地址
+export const remove = (addressId) => {
+  return request.post(api.remove, { addressId })
+}

+ 11 - 0
api/article/category.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'article.category/list'
+}
+
+// 页面数据
+export function list() {
+  return request.get(api.list)
+}

+ 17 - 0
api/article/index.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'article/list',
+  detail: 'article/detail'
+}
+
+// 文章列表
+export function list(param, option) {
+  return request.get(api.list, param, option)
+}
+
+// 文章详情
+export function detail(articleId) {
+  return request.get(api.detail, { articleId })
+}

+ 11 - 0
api/balance.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'balance.log/list'
+}
+
+// 余额账单明细列表
+export const list = (param) => {
+  return request.get(api.list, param)
+}

+ 11 - 0
api/balance/log.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'balance.log/list'
+}
+
+// 余额账单明细
+export const list = (param) => {
+  return request.get(api.list, param)
+}

+ 17 - 0
api/captcha.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  image: 'captcha/image',
+  sendSmsCaptcha: 'captcha/sendSmsCaptcha'
+}
+
+// 图形验证码
+export function image() {
+  return request.get(api.image, {}, { load: false })
+}
+
+// 发送短信验证码
+export function sendSmsCaptcha(data) {
+  return request.post(api.sendSmsCaptcha, data, { load: false })
+}

+ 35 - 0
api/cart.js

@@ -0,0 +1,35 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'cart/list',
+  total: 'cart/total',
+  add: 'cart/add',
+  update: 'cart/update',
+  clear: 'cart/clear'
+}
+
+// 购物车列表
+export const list = () => {
+  return request.get(api.list, {}, { load: false })
+}
+
+// 购物车商品总数量
+export const total = () => {
+  return request.get(api.total, {}, { load: false })
+}
+
+// 加入购物车
+export const add = (goodsId, goodsSkuId, goodsNum) => {
+  return request.post(api.add, { goodsId, goodsSkuId, goodsNum })
+}
+
+// 更新购物车商品数量
+export const update = (goodsId, goodsSkuId, goodsNum) => {
+  return request.post(api.update, { goodsId, goodsSkuId, goodsNum }, { isPrompt: false })
+}
+
+// 删除购物车中指定记录
+export const clear = (cartIds = []) => {
+  return request.post(api.clear, { cartIds })
+}

+ 11 - 0
api/category/index.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'category/list'
+}
+
+// 页面数据
+export function list() {
+  return request.get(api.list)
+}

+ 19 - 0
api/checkout.js

@@ -0,0 +1,19 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  order: 'checkout/order',
+  submit: 'checkout/submit',
+}
+
+// mode: 结算模式 (buyNow立即购买 cart购物车)
+
+// 结算台订单信息
+export const order = (mode, param) => {
+  return request.get(api.order, { mode, ...param })
+}
+
+// 结算台订单提交
+export const submit = (mode, data) => {
+  return request.post(api.submit, { mode, ...data })
+}

+ 23 - 0
api/comment.js

@@ -0,0 +1,23 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'comment/list',
+  listRows: 'comment/listRows',
+  total: 'comment/total'
+}
+
+// 商品评价列表
+export const list = (goodsId, param, option) => {
+  return request.get(api.list, { ...param, goodsId }, option)
+}
+
+// 商品评价列表 (限制数量, 用于商品详情页展示)
+export const listRows = (goodsId, limit = 5) => {
+  return request.get(api.listRows, { goodsId, limit })
+}
+
+// 商品评分总数
+export const total = (goodsId) => {
+  return request.get(api.total, { goodsId })
+}

+ 16 - 0
api/coupon.js

@@ -0,0 +1,16 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'coupon/list'
+}
+
+// 优惠券列表
+export const list = (param, option) => {
+  const options = {
+    isPrompt: true, //(默认 true 说明:本接口抛出的错误是否提示)
+    load: true, //(默认 true 说明:本接口是否提示加载动画)
+    ...option
+  }
+  return request.get(api.list, param, options)
+}

+ 11 - 0
api/express.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'express/list'
+}
+
+// 物流公司列表
+export const list = (param) => {
+  return request.get(api.list, param)
+}

+ 23 - 0
api/goods.js

@@ -0,0 +1,23 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'goods/list',
+  detail: 'goods/detail',
+  specData: 'goods/specData'
+}
+
+// 商品列表
+export const list = param => {
+  return request.get(api.list, param)
+}
+
+// 商品详情
+export const detail = goodsId => {
+  return request.get(api.detail, { goodsId })
+}
+
+// 获取商品规格数据
+export const specData = (goodsId) => {
+  return request.get(api.specData, { goodsId })
+}

+ 11 - 0
api/goods/service.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'goods.service/list'
+}
+
+// 商品评价列表
+export function list(goodsId) {
+  return request.get(api.list, { goodsId })
+}

+ 11 - 0
api/help.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'help/list'
+}
+
+// 帮助中心列表
+export const list = (param) => {
+  return request.get(api.list, param)
+}

+ 23 - 0
api/login/index.js

@@ -0,0 +1,23 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  login: 'passport/login',
+  loginMpWx: 'passport/loginMpWx',
+  loginMpWxMobile: 'passport/loginMpWxMobile',
+}
+
+// 用户登录(手机号+验证码)
+export function login(data) {
+  return request.post(api.login, data)
+}
+
+// 微信小程序快捷登录(获取微信用户基本信息)
+export function loginMpWx(data, option) {
+  return request.post(api.loginMpWx, data, option)
+}
+
+// 微信小程序快捷登录(授权手机号)
+export function loginMpWxMobile(data, option) {
+  return request.post(api.loginMpWxMobile, data, option)
+}

+ 22 - 0
api/myCoupon.js

@@ -0,0 +1,22 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'myCoupon/list',
+  receive: 'myCoupon/receive'
+}
+
+// 我的优惠券列表
+export const list = (param, option) => {
+  const options = {
+    isPrompt: true, //(默认 true 说明:本接口抛出的错误是否提示)
+    load: true, //(默认 true 说明:本接口是否提示加载动画)
+    ...option
+  }
+  return request.get(api.list, param, options)
+}
+
+// 领取优惠券
+export const receive = (couponId, data) => {
+  return request.post(api.receive, { couponId, ...couponId, data })
+}

+ 47 - 0
api/order.js

@@ -0,0 +1,47 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  todoCounts: 'order/todoCounts',
+  list: 'order/list',
+  detail: 'order/detail',
+  express: 'order/express',
+  cancel: 'order/cancel',
+  receipt: 'order/receipt',
+  pay: 'order/pay'
+}
+
+// 当前用户待处理的订单数量
+export function todoCounts(param, option) {
+  return request.get(api.todoCounts, param, option)
+}
+
+// 我的订单列表
+export function list(param, option) {
+  return request.get(api.list, param, option)
+}
+
+// 订单详情
+export function detail(orderId, param) {
+  return request.get(api.detail, { orderId, ...param })
+}
+
+// 获取物流信息
+export function express(orderId, param) {
+  return request.get(api.express, { orderId, ...param })
+}
+
+// 取消订单
+export function cancel(orderId, data) {
+  return request.post(api.cancel, { orderId, ...data })
+}
+
+// 确认收货
+export function receipt(orderId, data) {
+  return request.post(api.receipt, { orderId, ...data })
+}
+
+// 立即支付
+export function pay(orderId, payType, param) {
+  return request.get(api.pay, { orderId, payType, ...param })
+}

+ 17 - 0
api/order/comment.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'order.comment/list',
+  submit: 'order.comment/submit'
+}
+
+// 待评价订单商品列表
+export const list = (orderId, param) => {
+  return request.get(api.list, { orderId, ...param })
+}
+
+// 创建商品评价
+export const submit = (orderId, data) => {
+  return request.post(api.submit, { orderId, form: data })
+}

+ 13 - 0
api/page.js

@@ -0,0 +1,13 @@
+import request from '@/utils/request'
+
+// api地址
+const apiUri = {
+  detail: 'page/detail'
+}
+
+// 页面数据
+export function detail(pageId) {
+  return request.get(apiUri.detail, {
+    pageId
+  })
+}

+ 11 - 0
api/points/log.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'points.log/list'
+}
+
+// 积分明细列表
+export const list = (param) => {
+  return request.get(api.list, param)
+}

+ 11 - 0
api/recharge.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  submit: 'recharge/submit'
+}
+
+// 积分明细列表
+export const submit = (data) => {
+  return request.post(api.submit, data)
+}

+ 11 - 0
api/recharge/order.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'recharge.order/list'
+}
+
+// 我的充值记录列表
+export const list = (param) => {
+  return request.get(api.list, param)
+}

+ 11 - 0
api/recharge/plan.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'recharge.plan/list'
+}
+
+// 充值套餐列表
+export const list = (param) => {
+  return request.get(api.list, param)
+}

+ 35 - 0
api/refund.js

@@ -0,0 +1,35 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'refund/list',
+  goods: 'refund/goods',
+  apply: 'refund/apply',
+  detail: 'refund/detail',
+  delivery: 'refund/delivery'
+}
+
+// 售后单列表
+export const list = (param, option) => {
+  return request.get(api.list, param, option)
+}
+
+// 订单商品详情
+export const goods = (orderGoodsId, param) => {
+  return request.get(api.goods, { orderGoodsId, ...param })
+}
+
+// 申请售后
+export const apply = (orderGoodsId, data) => {
+  return request.post(api.apply, { orderGoodsId, form: data })
+}
+
+// 售后单详情
+export const detail = (orderRefundId, param) => {
+  return request.get(api.detail, { orderRefundId, ...param })
+}
+
+// 用户发货
+export const delivery = (orderRefundId, data) => {
+  return request.post(api.delivery, { orderRefundId, form: data })
+}

+ 17 - 0
api/region.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  all: 'region/all',
+  tree: 'region/tree'
+}
+
+// 获取所有地区
+export const all = (param) => {
+  return request.get(api.all, param)
+}
+
+// 获取所有地区(树状)
+export const tree = (param) => {
+  return request.get(api.tree, param)
+}

+ 11 - 0
api/setting.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  data: 'setting/data'
+}
+
+// 设置项详情
+export function data() {
+  return request.get(api.data)
+}

+ 18 - 0
api/upload.js

@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  image: 'upload/image'
+}
+
+// 图片上传
+export const image = files => {
+  // 文件上传大小, 2M
+  const maxSize = 1024 * 1024 * 2
+  // 执行上传
+  return new Promise((resolve, reject) => {
+    request.urlFileUpload({ files, maxSize })
+      .then(result => resolve(result.map(item => item.data.fileInfo.file_id), result))
+      .catch(reject)
+  })
+}

+ 34 - 0
api/user.js

@@ -0,0 +1,34 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  userInfo: 'user/info',
+  assets: 'user/assets',
+  bindMobile: 'user/bindMobile',
+  personal: 'user/personal'
+}
+
+// 当前登录的用户信息
+export const info = (param, option) => {
+  const options = {
+    isPrompt: true, //(默认 true 说明:本接口抛出的错误是否提示)
+    load: true, //(默认 true 说明:本接口是否提示加载动画)
+    ...option
+  }
+  return request.get(api.userInfo, param, options)
+}
+
+// 账户资产
+export const assets = (param, option) => {
+  return request.get(api.assets, param, option)
+}
+
+// 绑定手机号
+export const bindMobile = (data, option) => {
+  return request.post(api.bindMobile, data, option)
+}
+
+// 修改个人信息(头像昵称)
+export const personal = (data, option) => {
+  return request.post(api.personal, data, option)
+}

+ 11 - 0
api/user/coupon.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  receive: 'user.coupon/receive'
+}
+
+// 优惠券列表
+export const receive = (data) => {
+  return request.post(api.receive, data)
+}

+ 44 - 0
app.scss

@@ -0,0 +1,44 @@
+/* utils.scss */
+@import "/utils/utils.scss";
+
+page {
+  background: #fafafa;
+}
+
+@-webkit-keyframes rotate {
+  0% {
+    transform: rotate(0deg) scale(1);
+  }
+
+  100% {
+    transform: rotate(360deg) scale(1);
+  }
+}
+
+@keyframes rotate {
+  0% {
+    transform: rotate(0deg) scale(1);
+  }
+
+  100% {
+    transform: rotate(360deg) scale(1);
+  }
+}
+
+/* #ifdef H5*/
+
+uni-page {
+  box-shadow: 0 1px 22px rgb(169, 169, 169, .3);
+}
+
+.footer-fixed {
+  left: var(--window-left) !important;
+  right: var(--window-right) !important;
+}
+
+.u-mask,.u-drawer {
+  left: var(--window-left) !important;
+  right: var(--window-right) !important;
+}
+
+/* #endif */

+ 3 - 0
common/constant/index.js

@@ -0,0 +1,3 @@
+import paginate from './paginate'
+
+export { paginate }

+ 7 - 0
common/constant/paginate.js

@@ -0,0 +1,7 @@
+export default {
+  data: [], // 列表数据
+  current_page: 1, // 当前页码
+  last_page: 1, // 最大页码
+  per_page: 15, // 每页记录数
+  total: 0, // 总记录数
+}

+ 10 - 0
common/enum/coupon/ApplyRange.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:优惠券适用范围
+ * ApplyRangeEnum
+ */
+export default new Enum([
+  { key: 'ALL', name: '全部商品', value: 10 },
+  { key: 'SOME_GOODS', name: '指定商品', value: 20 }
+])

+ 10 - 0
common/enum/coupon/CouponType.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:优惠券类型
+ * CouponTypeEnum
+ */
+export default new Enum([
+  { key: 'FULL_DISCOUNT', name: '满减券', value: 10 },
+  { key: 'DISCOUNT', name: '折扣券', value: 20 }
+])

+ 10 - 0
common/enum/coupon/ExpireType.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:优惠券到期类型
+ * ExpireTypeEnum
+ */
+export default new Enum([
+  { key: 'RECEIVE', name: '领取后', value: 10 },
+  { key: 'FIXED_TIME', name: '固定时间', value: 20 }
+])

+ 5 - 0
common/enum/coupon/index.js

@@ -0,0 +1,5 @@
+import ApplyRangeEnum from './ApplyRange'
+import ExpireTypeEnum from './ExpireType'
+import CouponTypeEnum from './CouponType'
+
+export { ApplyRangeEnum, CouponTypeEnum, ExpireTypeEnum }

+ 85 - 0
common/enum/enum.js

@@ -0,0 +1,85 @@
+/**
+ * 枚举类
+ * Enum.IMAGE.name                => "图片"
+ * Enum.getNameByKey('IMAGE')     => "图片"
+ * Enum.getValueByKey('IMAGE')    => 10
+ * Enum.getNameByValue(10)        => "图片"
+ * Enum.getData()                 => [{key: "IMAGE", name: "图片", value: 10}]
+ */
+class Enum {
+  constructor (param) {
+    const keyArr = []
+    const valueArr = []
+
+    if (!Array.isArray(param)) {
+      throw new Error('param is not an array!')
+    }
+
+    param.map(element => {
+      if (!element.key || !element.name) {
+        return
+      }
+      // 保存key值组成的数组,方便A.getName(name)类型的调用
+      keyArr.push(element.key)
+      valueArr.push(element.value)
+      // 根据key生成不同属性值,以便A.B.name类型的调用
+      this[element.key] = element
+      if (element.key !== element.value) {
+        this[element.value] = element
+      }
+    })
+
+
+    // 保存源数组
+    this.data = param
+    this.keyArr = keyArr
+    this.valueArr = valueArr
+
+    // 防止被修改
+    // Object.freeze(this)
+  }
+
+  // 根据key得到对象
+  keyOf (key) {
+    return this.data[this.keyArr.indexOf(key)]
+  }
+
+  // 根据key得到对象
+  valueOf (key) {
+    return this.data[this.valueArr.indexOf(key)]
+  }
+
+  // 根据key获取name值
+  getNameByKey (key) {
+    const prop = this.keyOf(key)
+    if (!prop) {
+      throw new Error('No enum constant' + key)
+    }
+    return prop.name
+  }
+
+  // 根据value获取name值
+  getNameByValue (value) {
+    const prop = this.valueOf(value)
+    if (!prop) {
+      throw new Error('No enum constant' + value)
+    }
+    return prop.name
+  }
+
+  // 根据key获取value值
+  getValueByKey (key) {
+    const prop = this.keyOf(key)
+    if (!prop) {
+      throw new Error('No enum constant' + key)
+    }
+    return prop.key
+  }
+
+  // 返回源数组
+  getData () {
+    return this.data
+  }
+}
+
+export default Enum

+ 10 - 0
common/enum/goods/SpecType.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:商品规格类型
+ * SpecTypeEnum
+ */
+export default new Enum([
+  { key: 'SINGLE', name: '单规格', value: 10 },
+  { key: 'MULTI', name: '多规格', value: 20 }
+])

+ 3 - 0
common/enum/goods/index.js

@@ -0,0 +1,3 @@
+import SpecTypeEnum from './SpecType'
+
+export { SpecTypeEnum }

+ 10 - 0
common/enum/order/DeliveryStatus.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:订单发货状态
+ * DeliveryStatusEnum
+ */
+export default new Enum([
+  { key: 'NOT_DELIVERED', name: '未发货', value: 10 },
+  { key: 'DELIVERED', name: '已发货', value: 20 }
+])

+ 9 - 0
common/enum/order/DeliveryType.js

@@ -0,0 +1,9 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:配送方式
+ * DeliveryTypeEnum
+ */
+export default new Enum([
+  { key: 'EXPRESS', name: '快递配送', value: 10 }
+])

+ 11 - 0
common/enum/order/OrderSource.js

@@ -0,0 +1,11 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:订单来源
+ * OrderSourceEnum
+ */
+export default new Enum([
+  { key: 'MASTER', name: '普通订单', value: 10 },
+  { key: 'BARGAIN', name: '砍价订单', value: 20 },
+  { key: 'SHARP', name: '秒杀订单', value: 30 }
+])

+ 12 - 0
common/enum/order/OrderStatus.js

@@ -0,0 +1,12 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:订单状态
+ * OrderStatusEnum
+ */
+export default new Enum([
+  { key: 'NORMAL', name: '进行中', value: 10 },
+  { key: 'CANCELLED', name: '已取消', value: 20 },
+  { key: 'APPLY_CANCEL', name: '待取消', value: 21 },
+  { key: 'COMPLETED', name: '已完成', value: 30 }
+])

+ 10 - 0
common/enum/order/PayStatus.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:订单支付状态
+ * PayStatusEnum
+ */
+export default new Enum([
+  { key: 'PENDING', name: '待支付', value: 10 },
+  { key: 'SUCCESS', name: '已支付', value: 20 }
+])

+ 10 - 0
common/enum/order/PayType.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:订单支付方式
+ * PayTypeEnum
+ */
+export default new Enum([
+  { key: 'BALANCE', name: '余额支付', value: 10 },
+  { key: 'WECHAT', name: '微信支付', value: 20 }
+])

+ 10 - 0
common/enum/order/ReceiptStatus.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:订单收货状态
+ * ReceiptStatusEnum
+ */
+export default new Enum([
+  { key: 'NOT_RECEIVED', name: '未收货', value: 10 },
+  { key: 'RECEIVED', name: '已收货', value: 20 }
+])

+ 17 - 0
common/enum/order/index.js

@@ -0,0 +1,17 @@
+import DeliveryStatusEnum from './DeliveryStatus'
+import DeliveryTypeEnum from './DeliveryType'
+import OrderSourceEnum from './OrderSource'
+import OrderStatusEnum from './OrderStatus'
+import PayStatusEnum from './PayStatus'
+import PayTypeEnum from './PayType'
+import ReceiptStatusEnum from './ReceiptStatus'
+
+export {
+    DeliveryStatusEnum,
+    DeliveryTypeEnum,
+    OrderSourceEnum,
+    OrderStatusEnum,
+    PayStatusEnum,
+    PayTypeEnum,
+    ReceiptStatusEnum
+}

+ 11 - 0
common/enum/order/refund/AuditStatus.js

@@ -0,0 +1,11 @@
+import Enum from '../../enum'
+
+/**
+ * 枚举类:商家审核状态
+ * AuditStatusEnum
+ */
+export default new Enum([
+  { key: 'WAIT', name: '待审核', value: 0 },
+  { key: 'REVIEWED', name: '已同意', value: 10 },
+  { key: 'REJECTED', name: '已拒绝', value: 20 }
+])

+ 12 - 0
common/enum/order/refund/RefundStatus.js

@@ -0,0 +1,12 @@
+import Enum from '../../enum'
+
+/**
+ * 枚举类:售后单状态
+ * RefundStatusEnum
+ */
+export default new Enum([
+  { key: 'NORMAL', name: '进行中', value: 0 },
+  { key: 'REJECTED', name: '已拒绝', value: 10 },
+  { key: 'COMPLETED', name: '已完成', value: 20 },
+  { key: 'CANCELLED', name: '已取消', value: 30 }
+])

+ 10 - 0
common/enum/order/refund/RefundType.js

@@ -0,0 +1,10 @@
+import Enum from '../../enum'
+
+/**
+ * 枚举类:售后类型
+ * RefundTypeEnum
+ */
+export default new Enum([
+  { key: 'RETURN', name: '退货退款', value: 10 },
+  { key: 'EXCHANGE', name: '换货', value: 20 }
+])

+ 9 - 0
common/enum/order/refund/index.js

@@ -0,0 +1,9 @@
+import AuditStatusEnum from './AuditStatus'
+import RefundStatusEnum from './RefundStatus'
+import RefundTypeEnum from './RefundType'
+
+export {
+    AuditStatusEnum,
+    RefundStatusEnum,
+    RefundTypeEnum
+}

+ 27 - 0
common/enum/setting/Key.js

@@ -0,0 +1,27 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:设置项索引
+ * SettingKeyEnum
+ */
+export default new Enum([{
+    key: 'REGISTER',
+    name: '账户注册设置',
+    value: 'register'
+  },
+  {
+    key: 'PAGE_CATEGORY_TEMPLATE',
+    name: '分类页模板',
+    value: 'page_category_template'
+  },
+  {
+    key: 'POINTS',
+    name: '积分设置',
+    value: 'points'
+  },
+  {
+    key: 'RECHARGE',
+    name: '充值设置',
+    value: 'recharge'
+  }
+])

+ 12 - 0
common/enum/store/page/category/Style.js

@@ -0,0 +1,12 @@
+import Enum from '../../../enum'
+
+/**
+ * 枚举类:地址类型
+ * PageCategoryStyleEnum
+ */
+export default new Enum([
+  { key: 'ONE_LEVEL_BIG', name: '一级分类[大图]', value: 10 },
+  { key: 'ONE_LEVEL_SMALL', name: '一级分类[小图]', value: 11 },
+  { key: 'TWO_LEVEL', name: '二级分类', value: 20 },
+  { key: 'COMMODITY', name: '一级分类+商品', value: 30 }
+])

+ 3 - 0
common/enum/store/page/category/index.js

@@ -0,0 +1,3 @@
+import PageCategoryStyleEnum from './Style'
+
+export { PageCategoryStyleEnum }

+ 57 - 0
common/model/Region.js

@@ -0,0 +1,57 @@
+import * as Api from '@/api/region'
+import storage from '@/utils/storage'
+
+const REGION_TREE = 'region_tree'
+
+/**
+ * 商品分类 model类
+ * RegionModel
+ */
+export default {
+
+  // 从服务端获取全部地区数据(树状)
+  getTreeDataFromApi () {
+    return new Promise((resolve, reject) => {
+      Api.tree().then(result => resolve(result.data.list))
+    })
+  },
+
+  // 获取所有地区(树状)
+  getTreeData () {
+    return new Promise((resolve, reject) => {
+      // 判断缓存中是否存在
+      const data = storage.get(REGION_TREE)
+      // 从服务端获取全部地区数据
+      if (data) {
+        resolve(data)
+      } else {
+        this.getTreeDataFromApi().then(list => {
+          // 缓存24小时
+          storage.set(REGION_TREE, list, 24 * 60 * 60)
+          resolve(list)
+        })
+      }
+    })
+  },
+
+  // 获取所有地区的总数
+  getCitysCount () {
+    return new Promise((resolve, reject) => {
+      // 获取所有地区(树状)
+      this.getTreeData().then(data => {
+        const cityIds = []
+        // 遍历省份
+        for (const pidx in data) {
+          const province = data[pidx]
+          // 遍历城市
+          for (const cidx in province.city) {
+            const cityItem = province.city[cidx]
+            cityIds.push(cityItem.id)
+          }
+        }
+        resolve(cityIds.length)
+      })
+    })
+  }
+
+}

+ 77 - 0
common/model/Setting.js

@@ -0,0 +1,77 @@
+import Config from '@/core/config'
+import storage from '@/utils/storage'
+import * as SettingApi from '@/api/setting'
+
+const CACHE_KEY = 'Setting'
+const OTHER = '_other'
+
+// 写入缓存, 到期时间10分钟
+const setStorage = (data) => {
+  const expireTime = 10 * 60
+  storage.set(CACHE_KEY, data, expireTime)
+}
+
+// 获取缓存中的数据
+const getStorage = () => {
+  return storage.get(CACHE_KEY)
+}
+
+// 获取后端接口商城设置 (最新)
+const getApiData = () => {
+  return new Promise((resolve, reject) => {
+    SettingApi.data()
+      .then(result => {
+        resolve(result.data.setting)
+      })
+  })
+}
+
+/**
+ * 获取商城设置
+ * 有缓存的情况下返回缓存, 没有缓存从后端api获取
+ * @param {bool} isCache 是否从缓存中获取 [优点不用每次请求后端api 缺点后台更新设置后需等待时效性]
+ */
+const data = isCache => {
+  if (isCache == undefined) {
+    isCache = Config.get('enabledSettingCache')
+  }
+  return new Promise((resolve, reject) => {
+    const cacheData = getStorage()
+    if (isCache && cacheData) {
+      resolve(cacheData)
+    } else {
+      getApiData()
+        .then(data => {
+          setStorage(data)
+          resolve(data)
+        })
+    }
+  })
+}
+
+// 获取商城设置(指定项)
+const item = (key, isCache) => {
+  return new Promise((resolve, reject) => {
+    data(isCache)
+      .then(setting => {
+        resolve(setting[key])
+      })
+  })
+}
+
+// 获取H5端访问地址
+const h5Url = (isCache = false) => {
+  return new Promise((resolve, reject) => {
+    data(isCache)
+      .then(setting => {
+        const h5Url = setting[OTHER]['h5Url']
+        resolve(h5Url)
+      })
+  })
+}
+
+export default {
+  data,
+  item,
+  h5Url
+}

+ 36 - 0
components/add-cart-btn/index.vue

@@ -0,0 +1,36 @@
+<template>
+  <view class="add-cart" @click.stop="handleAddCart">
+    <text class="icon iconfont" :class="[`icon-jiagou${btnStyle}`]"></text>
+  </view>
+</template>
+
+<script>
+  export default {
+    props: {
+      // 购物车按钮样式 1 2 3
+      btnStyle: {
+        type: Number,
+        default: 1
+      },
+    },
+    data() {
+      return {
+        value: false,
+        goodsInfo: {}
+      }
+    },
+    methods: {
+      handleAddCart() {
+        this.$emit('click')
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .add-cart {
+    font-size: 38rpx;
+    padding: 0 20rpx;
+    color: #fa2209;
+  }
+</style>

+ 173 - 0
components/add-cart-popup/index.vue

@@ -0,0 +1,173 @@
+<template>
+  <goods-sku-popup :value="visible" @input="onChangeValue" border-radius="20" :localdata="localdata" :mode="2" :maskCloseAble="true"
+    @add-cart="addCart" @buy-now="buyNow" buyNowText="立即购买" />
+</template>
+
+<script>
+  import { setCartTotalNum } from '@/core/app'
+  import { SpecTypeEnum } from '@/common/enum/goods'
+  import * as CartApi from '@/api/cart'
+  import * as GoodsApi from '@/api/goods'
+  import GoodsSkuPopup from '@/components/goods-sku-popup'
+
+  export default {
+    components: {
+      GoodsSkuPopup
+    },
+    props: {
+      // 购物车按钮样式 1 2 3
+      btnStyle: {
+        type: Number,
+        default: 1
+      },
+    },
+    data() {
+      return {
+        // 是否显示
+        visible: false,
+        // 主商品信息
+        goods: {},
+        // SKU商品信息
+        localdata: {}
+      }
+    },
+    methods: {
+
+      // 加入购物车事件
+      async handle(goods) {
+        this.goods = goods
+        if (goods.spec_type == SpecTypeEnum.SINGLE.value) {
+          this.singleEvent()
+        }
+        if (goods.spec_type == SpecTypeEnum.MULTI.value) {
+          this.multiEvent()
+        }
+      },
+
+      // 单规格商品事件
+      singleEvent() {
+        const { goods } = this
+        this.addCart({
+          goods_id: goods.goods_id,
+          goods_sku_id: '0',
+          buy_num: 1
+        })
+      },
+
+      // 多规格商品事件
+      async multiEvent() {
+        const app = this
+        const { goods } = app
+        // 获取商品的规格信息
+        const { data: { specData } } = await GoodsApi.specData(goods.goods_id)
+        goods.skuList = specData.skuList
+        goods.specList = specData.specList
+        // 整理SKU商品信息
+        app.localdata = {
+          _id: goods.goods_id,
+          name: goods.goods_name,
+          goods_thumb: goods.goods_image,
+          sku_list: app.getSkuList(),
+          spec_list: app.getSpecList()
+        }
+        this.visible = true
+      },
+
+      // 监听组件显示隐藏
+      onChangeValue(val) {
+        this.visible = val
+      },
+
+      // 整理商品SKU列表 (多规格)
+      getSkuList() {
+        const app = this
+        const { goods: { goods_name, goods_image, skuList } } = app
+        const skuData = []
+        skuList.forEach(item => {
+          skuData.push({
+            _id: item.id,
+            goods_sku_id: item.goods_sku_id,
+            goods_id: item.goods_id,
+            goods_name: goods_name,
+            image: item.image_url ? item.image_url : goods_image,
+            price: item.goods_price * 100,
+            stock: item.stock_num,
+            spec_value_ids: item.spec_value_ids,
+            sku_name_arr: app.getSkuNameArr(item.spec_value_ids)
+          })
+        })
+        return skuData
+      },
+
+      // 获取sku记录的规格值列表
+      getSkuNameArr(specValueIds) {
+        const app = this
+        const defaultData = ['默认']
+        const skuNameArr = []
+        if (specValueIds) {
+          specValueIds.forEach((valueId, groupIndex) => {
+            const specValueName = app.getSpecValueName(valueId, groupIndex)
+            skuNameArr.push(specValueName)
+          })
+        }
+        return skuNameArr.length ? skuNameArr : defaultData
+      },
+
+      // 获取指定的规格值名称
+      getSpecValueName(valueId, groupIndex) {
+        const app = this
+        const { goods: { specList } } = app
+        const res = specList[groupIndex].valueList.find(specValue => {
+          return specValue.spec_value_id == valueId
+        })
+        return res.spec_value
+      },
+
+      // 整理规格数据 (多规格)
+      getSpecList() {
+        const { goods: { specList } } = this
+        const defaultData = [{ name: '默认', list: [{ name: '默认' }] }]
+        const specData = []
+        specList.forEach(group => {
+          const children = []
+          group.valueList.forEach(specValue => {
+            children.push({ name: specValue.spec_value })
+          })
+          specData.push({
+            name: group.spec_name,
+            list: children
+          })
+        })
+        return specData.length ? specData : defaultData
+      },
+
+      // 加入购物车按钮
+      addCart(selectShop) {
+        const app = this
+        const { goods_id, goods_sku_id, buy_num } = selectShop
+        CartApi.add(goods_id, goods_sku_id, buy_num)
+          .then(result => {
+            // 显示成功
+            app.$toast(result.message, 1000, false)
+            // 隐藏当前弹窗
+            app.onChangeValue(false)
+            // 购物车商品总数量
+            const cartTotal = result.data.cartTotal
+            // 缓存购物车数量
+            setCartTotalNum(cartTotal)
+            // 传递给父级
+            app.$emit('addCart', cartTotal)
+          })
+      }
+
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .add-cart {
+    font-size: 38rpx;
+    padding: 0 20rpx;
+    color: #fa2209;
+  }
+</style>

+ 57 - 0
components/avatar-image/index.vue

@@ -0,0 +1,57 @@
+<template>
+  <view class="avatar-image">
+    <image class="image"
+      :style="{ width: `${width}rpx`, height: `${width}rpx`, borderWidth: `${borderWidth}rpx`, borderColor: borderColor }"
+      :src="url ? url : '/static/default-avatar.png'"></image>
+  </view>
+</template>
+
+<script>
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      url: {
+        type: String,
+        default: ''
+      },
+      width: {
+        type: Number,
+        default: 90
+      },
+      borderWidth: {
+        type: Number,
+        default: 0
+      },
+      borderColor: {
+        type: String,
+        default: '#000000'
+      }
+    },
+
+    data() {
+      return {
+
+      }
+    },
+
+    methods: {
+
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .avatar-image {
+    .image {
+      display: block;
+      width: 60rpx;
+      height: 60rpx;
+      border-radius: 50%;
+      border-style: solid;
+    }
+  }
+</style>

+ 66 - 0
components/empty/index.vue

@@ -0,0 +1,66 @@
+<template>
+  <view v-if="!isLoading" class="empty-content" :style="customStyle">
+    <view class="empty-icon">
+      <image class="image" src="/static/empty.png" mode="widthFix"></image>
+    </view>
+    <view class="tips">{{ tips }}</view>
+    <slot name="slot"></slot>
+  </view>
+</template>
+
+<script>
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      // 正在加载
+      isLoading: {
+        type: Boolean,
+        default: true
+      },
+      // 自定义样式
+      customStyle: {
+        type: Object,
+        default () {
+          return {}
+        }
+      },
+      // 提示的问题
+      tips: {
+        type: String,
+        default: '亲,暂无相关数据'
+      }
+    },
+
+    data() {
+      return {}
+    },
+
+    methods: {
+
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .empty-content {
+    box-sizing: border-box;
+    width: 100%;
+    padding: 140rpx 50rpx;
+    text-align: center;
+
+    .tips {
+      font-size: 28rpx;
+      color: gray;
+      margin: 50rpx 0;
+    }
+
+    .empty-icon .image {
+      width: 280rpx;
+    }
+
+  }
+</style>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1364 - 0
components/goods-sku-popup/index.vue


+ 450 - 0
components/goods-sku-popup/number-box/index.vue

@@ -0,0 +1,450 @@
+<!-- 步进器 -->
+<template>
+	<view class="number-box">
+		<view class="u-icon-minus" @touchstart.prevent="btnTouchStart('minus')" @touchend.stop.prevent="clearTimer" :class="{ 'u-icon-disabled': disabled || inputVal <= min }"
+		    :style="{
+				background: bgColor,
+				height: inputHeight + 'rpx',
+				color: color,
+				fontSize: size + 'rpx',
+				minHeight: '1.4em'
+			}">
+			<view :style="'font-size:'+(Number(size)+10)+'rpx'" class="num-btn">-</view>
+		</view>
+		<input :disabled="disabledInput || disabled" :cursor-spacing="getCursorSpacing" :class="{ 'u-input-disabled': disabled }"
+		    v-model="inputVal" class="u-number-input" @blur="onBlur"
+		    type="number" :style="{
+				color: color,
+				fontSize: size + 'rpx',
+				background: bgColor,
+				height: inputHeight + 'rpx',
+				width: inputWidth + 'rpx',
+			}" />
+		<view class="u-icon-plus" @touchstart.prevent="btnTouchStart('plus')" @touchend.stop.prevent="clearTimer" :class="{ 'u-icon-disabled': disabled || inputVal >= max }"
+		    :style="{
+				background: bgColor,
+				height: inputHeight + 'rpx',
+				color: color,
+				fontSize: size + 'rpx',
+				minHeight: '1.4em',
+			}">
+			<view :style="'font-size:'+(Number(size)+10)+'rpx'" class="num-btn">+</view>
+		</view>
+	</view>
+</template>
+<script>
+	/**
+	 * numberBox 步进器
+	 * @description 该组件一般用于商城购物选择物品数量的场景。注意:该输入框只能输入大于或等于0的整数,不支持小数输入
+	 * @tutorial https://www.uviewui.com/components/numberBox.html
+	 * @property {Number} value 输入框初始值(默认1)
+	 * @property {String} bg-color 输入框和按钮的背景颜色(默认#F2F3F5)
+	 * @property {Number} min 用户可输入的最小值(默认0)
+	 * @property {Number} max 用户可输入的最大值(默认99999)
+	 * @property {Number} step 步长,每次加或减的值(默认1)
+	 * @property {Number} stepFirst 步进值,首次增加或最后减的值(默认step值和一致)
+	 * @property {Boolean} disabled 是否禁用操作,禁用后无法加减或手动修改输入框的值(默认false)
+	 * @property {Boolean} disabled-input 是否禁止输入框手动输入值(默认false)
+	 * @property {Boolean} positive-integer 是否只能输入正整数(默认true)
+	 * @property {String | Number} size 输入框文字和按钮字体大小,单位rpx(默认26)
+	 * @property {String} color 输入框文字和加减按钮图标的颜色(默认#323233)
+	 * @property {String | Number} input-width 输入框宽度,单位rpx(默认80)
+	 * @property {String | Number} input-height 输入框和按钮的高度,单位rpx(默认50)
+	 * @property {String | Number} index 事件回调时用以区分当前发生变化的是哪个输入框
+	 * @property {Boolean} long-press 是否开启长按连续递增或递减(默认true)
+	 * @property {String | Number} press-time 开启长按触发后,每触发一次需要多久,单位ms(默认250)
+	 * @property {String | Number} cursor-spacing 指定光标于键盘的距离,避免键盘遮挡输入框,单位rpx(默认200)
+	 * @event {Function} change 输入框内容发生变化时触发,对象形式
+	 * @event {Function} blur 输入框失去焦点时触发,对象形式
+	 * @event {Function} minus 点击减少按钮时触发(按钮可点击情况下),对象形式
+	 * @event {Function} plus 点击增加按钮时触发(按钮可点击情况下),对象形式
+	 * @example <number-box :min="1" :max="100"></number-box>
+	 */
+	export default {
+		name: "NumberBox",
+		emits: ["update:modelValue", "input", "change", "blur", "plus", "minus"],
+		props: {
+			// 预显示的数字
+			value: {
+				type: Number,
+				default: 1
+			},
+			modelValue: {
+				type: Number,
+				default: 1
+			},
+			// 背景颜色
+			bgColor: {
+				type: String,
+				default: '#F2F3F5'
+			},
+			// 最小值
+			min: {
+				type: Number,
+				default: 0
+			},
+			// 最大值
+			max: {
+				type: Number,
+				default: 99999
+			},
+			// 步进值,每次加或减的值
+			step: {
+				type: Number,
+				default: 1
+			},
+			// 步进值,首次增加或最后减的值
+			stepFirst: {
+				type: Number,
+				default: 0
+			},
+			// 是否只能输入 step 的倍数
+			stepStrictly: {
+				type: Boolean,
+				default: false
+			},
+			// 是否禁用加减操作
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			// input的字体大小,单位rpx
+			size: {
+				type: [Number, String],
+				default: 26
+			},
+			// 加减图标的颜色
+			color: {
+				type: String,
+				default: '#323233'
+			},
+			// input宽度,单位rpx
+			inputWidth: {
+				type: [Number, String],
+				default: 80
+			},
+			// input高度,单位rpx
+			inputHeight: {
+				type: [Number, String],
+				default: 50
+			},
+			// index索引,用于列表中使用,让用户知道是哪个numberbox发生了变化,一般使用for循环出来的index值即可
+			index: {
+				type: [Number, String],
+				default: ''
+			},
+			// 是否禁用输入框,与disabled作用于输入框时,为OR的关系,即想要禁用输入框,又可以加减的话
+			// 设置disabled为false,disabledInput为true即可
+			disabledInput: {
+				type: Boolean,
+				default: false
+			},
+			// 输入框于键盘之间的距离
+			cursorSpacing: {
+				type: [Number, String],
+				default: 100
+			},
+			// 是否开启长按连续递增或递减
+			longPress: {
+				type: Boolean,
+				default: true
+			},
+			// 开启长按触发后,每触发一次需要多久
+			pressTime: {
+				type: [Number, String],
+				default: 250
+			},
+			// 是否只能输入大于或等于0的整数(正整数)
+			positiveInteger: {
+				type: Boolean,
+				default: true
+			}
+		},
+		watch: {
+			value(v1, v2) {
+				// 只有value的改变是来自外部的时候,才去同步inputVal的值,否则会造成循环错误
+				if(!this.changeFromInner) {
+					this.inputVal = v1;
+					// 因为inputVal变化后,会触发this.handleChange(),在其中changeFromInner会再次被设置为true,
+					// 造成外面修改值,也导致被认为是内部修改的混乱,这里进行this.$nextTick延时,保证在运行周期的最后处
+					// 将changeFromInner设置为false
+					this.$nextTick(function(){
+						this.changeFromInner = false;
+					})
+				}
+			},
+			modelValue(v1, v2) {
+				// 只有value的改变是来自外部的时候,才去同步inputVal的值,否则会造成循环错误
+				if(!this.changeFromInner) {
+					this.inputVal = v1;
+					// 因为inputVal变化后,会触发this.handleChange(),在其中changeFromInner会再次被设置为true,
+					// 造成外面修改值,也导致被认为是内部修改的混乱,这里进行this.$nextTick延时,保证在运行周期的最后处
+					// 将changeFromInner设置为false
+					this.$nextTick(function(){
+						this.changeFromInner = false;
+					})
+				}
+			},
+			inputVal(v1, v2) {
+				// 为了让用户能够删除所有输入值,重新输入内容,删除所有值后,内容为空字符串
+				if (v1 == '') return;
+				let value = 0;
+				// 首先判断是否数值,并且在min和max之间,如果不是,使用原来值
+				let tmp = this.isNumber(v1);
+				if (tmp && v1 >= this.min && v1 <= this.max) value = v1;
+				else value = v2;
+				// 判断是否只能输入大于等于0的整数
+				if(this.positiveInteger) {
+					// 小于0,或者带有小数点,
+					if(v1 < 0 || String(v1).indexOf('.') !== -1) {
+						value = v2;
+						// 双向绑定input的值,必须要使用$nextTick修改显示的值
+						this.$nextTick(() => {
+							this.inputVal = v2;
+						})
+					}
+				}
+				// 发出change事件
+				this.handleChange(value, 'change');
+			},
+			min(v1){
+				if(v1 !== undefined && v1!="" && this.getValue() < v1){
+					this.$emit("input",v1);
+				}
+			},
+			max(v1){
+				if(v1 !== undefined && v1!="" && this.getValue() > v1){
+					this.$emit("input",v1);
+				}
+			}
+		},
+		data() {
+			return {
+				inputVal: 1, // 输入框中的值,不能直接使用props中的value,因为应该改变props的状态
+				timer: null, // 用作长按的定时器
+				changeFromInner: false, // 值发生变化,是来自内部还是外部
+				innerChangeTimer: null, // 内部定时器
+			};
+		},
+		created() {
+			this.inputVal = Number(this.getValue());
+		},
+		computed: {
+			getCursorSpacing() {
+				// 先将值转为px单位,再转为数值
+				return Number(uni.upx2px(this.cursorSpacing));
+			}
+		},
+		methods: {
+			getValue(){
+				// #ifndef VUE3
+				return this.value;
+				// #endif
+				
+				// #ifdef VUE3
+				return this.modelValue;
+				// #endif
+			},
+			// 点击退格键
+			btnTouchStart(callback) {
+				// 先执行一遍方法,否则会造成松开手时,就执行了clearTimer,导致无法实现功能
+				this[callback]();
+				// 如果没开启长按功能,直接返回
+				if (!this.longPress) return;
+				clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
+				this.timer = null;
+				this.timer = setInterval(() => {
+					// 执行加或减函数
+					this[callback]();
+				}, this.pressTime);
+			},
+			clearTimer() {
+				this.$nextTick(() => {
+					clearInterval(this.timer);
+					this.timer = null;
+				})
+			},
+			minus() {
+				this.computeVal('minus');
+			},
+			plus() {
+				this.computeVal('plus');
+			},
+			// 为了保证小数相加减出现精度溢出的问题
+			calcPlus(num1, num2) {
+				let baseNum, baseNum1, baseNum2;
+				try {
+					baseNum1 = num1.toString().split('.')[1].length;
+				} catch (e) {
+					baseNum1 = 0;
+				}
+				try {
+					baseNum2 = num2.toString().split('.')[1].length;
+				} catch (e) {
+					baseNum2 = 0;
+				}
+				baseNum = Math.pow(10, Math.max(baseNum1, baseNum2));
+				let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2; //精度
+				return ((num1 * baseNum + num2 * baseNum) / baseNum).toFixed(precision);
+			},
+			// 为了保证小数相加减出现精度溢出的问题
+			calcMinus(num1, num2) {
+				let baseNum, baseNum1, baseNum2;
+				try {
+					baseNum1 = num1.toString().split('.')[1].length;
+				} catch (e) {
+					baseNum1 = 0;
+				}
+				try {
+					baseNum2 = num2.toString().split('.')[1].length;
+				} catch (e) {
+					baseNum2 = 0;
+				}
+				baseNum = Math.pow(10, Math.max(baseNum1, baseNum2));
+				let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2;
+				return ((num1 * baseNum - num2 * baseNum) / baseNum).toFixed(precision);
+			},
+			computeVal(type) {
+				uni.hideKeyboard();
+				if (this.disabled) return;
+				let value = 0;
+				// 新增stepFirst开始
+				// 减
+				if (type === 'minus') {
+					if(this.stepFirst > 0 && this.inputVal == this.stepFirst){
+						value = this.min;
+					}else{
+						value = this.calcMinus(this.inputVal, this.step);
+					}
+				} else if (type === 'plus') {
+					if(this.stepFirst > 0 && this.inputVal < this.stepFirst){
+						value = this.stepFirst;
+					}else{
+						value = this.calcPlus(this.inputVal, this.step);
+					}
+				}
+				if(this.stepStrictly){
+					let strictly = value % this.step;
+					if(strictly > 0){
+						value -= strictly;
+					}
+				}
+				if (value > this.max ) {
+					value = this.max;
+				}else if (value < this.min) {
+					value = this.min;
+				}
+				// 新增stepFirst结束
+				this.inputVal = value;
+				this.handleChange(value, type);
+			},
+			// 处理用户手动输入的情况
+			onBlur(event) {
+				let val = 0;
+				let value = event.detail.value;
+				// 如果为非0-9数字组成,或者其第一位数值为0,直接让其等于min值
+				// 这里不直接判断是否正整数,是因为用户传递的props min值可能为0
+				if (!/(^\d+$)/.test(value) || value[0] == 0) val = this.min;
+				val = +value;
+				
+				// 新增stepFirst开始
+				if(this.stepFirst > 0 && this.inputVal < this.stepFirst && this.inputVal>0){
+					val = this.stepFirst;
+				}
+				// 新增stepFirst结束
+				if(this.stepStrictly){
+					let strictly = val % this.step;
+					if(strictly > 0){
+						val -= strictly;
+					}
+				}
+				if (val > this.max) {
+					val = this.max;
+				} else if (val < this.min) {
+					val = this.min;
+				}
+				this.$nextTick(() => {
+					this.inputVal = val;
+				})
+				this.handleChange(val, 'blur');
+			},
+			handleChange(value, type) {
+				if (this.disabled) return;
+				// 清除定时器,避免造成混乱
+				if(this.innerChangeTimer) {
+					clearTimeout(this.innerChangeTimer);
+					this.innerChangeTimer = null;
+				}
+				// 发出input事件,修改通过v-model绑定的值,达到双向绑定的效果
+				this.changeFromInner = true;
+				// 一定时间内,清除changeFromInner标记,否则内部值改变后
+				// 外部通过程序修改value值,将会无效
+				this.innerChangeTimer = setTimeout(() => {
+					this.changeFromInner = false;
+				}, 150);
+				this.$emit('input', Number(value));
+				this.$emit("update:modelValue", Number(value));
+				this.$emit(type, {
+					// 转为Number类型
+					value: Number(value),
+					index: this.index
+				})
+			},
+			/**
+			 * 验证十进制数字
+			 */
+			isNumber(value) {
+				return /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value)
+			}
+		}
+	};
+</script>
+
+<style lang="scss" scoped>
+	.number-box {
+		display: inline-flex;
+		align-items: center;
+	}
+
+	.u-number-input {
+		position: relative;
+		text-align: center;
+		padding: 0;
+		margin: 0 6rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.u-icon-plus,
+	.u-icon-minus {
+		width: 60rpx;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+	}
+
+	.u-icon-plus {
+		border-radius: 0 8rpx 8rpx 0;
+	}
+
+	.u-icon-minus {
+		border-radius: 8rpx 0 0 8rpx;
+	}
+
+	.u-icon-disabled {
+		color: #c8c9cc !important;
+		background: #f7f8fa !important;
+	}
+
+	.u-input-disabled {
+		color: #c8c9cc !important;
+		background-color: #f2f3f5 !important;
+	}
+	.num-btn{
+		font-weight:550;
+		position: relative;
+		top:-4rpx;
+	}
+
+</style>

+ 55 - 0
components/mescroll-uni/components/mescroll-down.css

@@ -0,0 +1,55 @@
+/* 下拉刷新区域 */
+.mescroll-downwarp {
+	position: absolute;
+	top: -100%;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	text-align: center;
+}
+
+/* 下拉刷新--内容区,定位于区域底部 */
+.mescroll-downwarp .downwarp-content {
+	position: absolute;
+	left: 0;
+	bottom: 0;
+	width: 100%;
+	min-height: 60rpx;
+	padding: 20rpx 0;
+	text-align: center;
+}
+
+/* 下拉刷新--提示文本 */
+.mescroll-downwarp .downwarp-tip {
+	display: inline-block;
+	font-size: 28rpx;
+	vertical-align: middle;
+	margin-left: 16rpx;
+	/* color: gray; 已在style设置color,此处删去*/
+}
+
+/* 下拉刷新--旋转进度条 */
+.mescroll-downwarp .downwarp-progress {
+	display: inline-block;
+	width: 32rpx;
+	height: 32rpx;
+	border-radius: 50%;
+	border: 2rpx solid gray;
+	border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
+	vertical-align: middle;
+}
+
+/* 旋转动画 */
+.mescroll-downwarp .mescroll-rotate {
+	animation: mescrollDownRotate 0.6s linear infinite;
+}
+
+@keyframes mescrollDownRotate {
+	0% {
+		transform: rotate(0deg);
+	}
+
+	100% {
+		transform: rotate(360deg);
+	}
+}

+ 47 - 0
components/mescroll-uni/components/mescroll-down.vue

@@ -0,0 +1,47 @@
+<!-- 下拉刷新区域 -->
+<template>
+	<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
+		<view class="downwarp-content">
+			<view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mOption.textColor, 'transform':downRotate}"></view>
+			<view class="downwarp-tip">{{downText}}</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	props: {
+		option: Object , // down的配置项
+		type: Number, // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
+		rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+	},
+	computed: {
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
+		mOption(){
+			return this.option || {}
+		},
+		// 是否在加载中
+		isDownLoading(){
+			return this.type === 3
+		},
+		// 旋转的角度
+		downRotate(){
+			return 'rotate(' + 360 * this.rate + 'deg)'
+		},
+		// 文本提示
+		downText(){
+			switch (this.type){
+				case 1: return this.mOption.textInOffset;
+				case 2: return this.mOption.textOutOffset;
+				case 3: return this.mOption.textLoading;
+				case 4: return this.mOption.textLoading;
+				default: return this.mOption.textInOffset;
+			}
+		}
+	}
+};
+</script>
+
+<style>
+@import "./mescroll-down.css";
+</style>

+ 90 - 0
components/mescroll-uni/components/mescroll-empty.vue

@@ -0,0 +1,90 @@
+<!--空布局
+
+可作为独立的组件, 不使用mescroll的页面也能单独引入, 以便APP全局统一管理:
+import MescrollEmpty from '@/components/mescroll-uni/components/mescroll-empty.vue';
+<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
+
+-->
+<template>
+	<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
+		<view> <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" /> </view>
+		<view v-if="tip" class="empty-tip">{{ tip }}</view>
+		<view v-if="option.btnText" class="empty-btn" @click="emptyClick">{{ option.btnText }}</view>
+	</view>
+</template>
+
+<script>
+// 引入全局配置
+import GlobalOption from './../mescroll-uni-option.js';
+export default {
+	props: {
+		// empty的配置项: 默认为GlobalOption.up.empty
+		option: {
+			type: Object,
+			default() {
+				return {};
+			}
+		}
+	},
+	// 使用computed获取配置,用于支持option的动态配置
+	computed: {
+		// 图标
+		icon() {
+			return this.option.icon == null ? GlobalOption.up.empty.icon : this.option.icon; // 此处不使用短路求值, 用于支持传空串不显示图标
+		},
+		// 文本提示
+		tip() {
+			return this.option.tip == null ? GlobalOption.up.empty.tip : this.option.tip; // 此处不使用短路求值, 用于支持传空串不显示文本提示
+		}
+	},
+	methods: {
+		// 点击按钮
+		emptyClick() {
+			this.$emit('emptyclick');
+		}
+	}
+};
+</script>
+
+<style>
+/* 无任何数据的空布局 */
+.mescroll-empty {
+	box-sizing: border-box;
+	width: 100%;
+	padding: 100rpx 50rpx;
+	text-align: center;
+}
+
+.mescroll-empty.empty-fixed {
+	z-index: 99;
+	position: absolute; /*transform会使fixed失效,最终会降级为absolute */
+	top: 100rpx;
+	left: 0;
+}
+
+.mescroll-empty .empty-icon {
+	width: 280rpx;
+	height: 280rpx;
+}
+
+.mescroll-empty .empty-tip {
+	margin-top: 40rpx;
+	font-size: 28rpx;
+	color: gray;
+}
+
+.mescroll-empty .empty-btn {
+	display: inline-block;
+	margin-top: 40rpx;
+	min-width: 200rpx;
+	padding: 18rpx;
+	font-size: 28rpx;
+	border: 1rpx solid #e04b28;
+	border-radius: 60rpx;
+	color: #e04b28;
+}
+
+.mescroll-empty .empty-btn:active {
+	opacity: 0.75;
+}
+</style>

+ 83 - 0
components/mescroll-uni/components/mescroll-top.vue

@@ -0,0 +1,83 @@
+<!-- 回到顶部的按钮 -->
+<template>
+	<image
+		v-if="mOption.src"
+		class="mescroll-totop"
+		:class="[value ? 'mescroll-totop-in' : 'mescroll-totop-out', {'mescroll-totop-safearea': mOption.safearea}]"
+		:style="{'z-index':mOption.zIndex, 'left': left, 'right': right, 'bottom':addUnit(mOption.bottom), 'width':addUnit(mOption.width), 'border-radius':addUnit(mOption.radius)}"
+		:src="mOption.src"
+		mode="widthFix"
+		@click="toTopClick"
+	/>
+</template>
+
+<script>
+export default {
+	props: {
+		// up.toTop的配置项
+		option: Object,
+		// 是否显示
+		value: false
+	},
+	computed: {
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
+		mOption(){
+			return this.option || {}
+		},
+		// 优先显示左边
+		left(){
+			return this.mOption.left ? this.addUnit(this.mOption.left) : 'auto';
+		},
+		// 右边距离 (优先显示左边)
+		right() {
+			return this.mOption.left ? 'auto' : this.addUnit(this.mOption.right);
+		}
+	},
+	methods: {
+		addUnit(num){
+			if(!num) return 0;
+			if(typeof num === 'number') return num + 'rpx';
+			return num
+		},
+		toTopClick() {
+			this.$emit('input', false); // 使v-model生效
+			this.$emit('click'); // 派发点击事件
+		}
+	}
+};
+</script>
+
+<style>
+/* 回到顶部的按钮 */
+.mescroll-totop {
+	z-index: 9990;
+	position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */
+	right: 20rpx;
+	bottom: 120rpx;
+	width: 72rpx;
+	height: auto;
+	border-radius: 50%;
+	opacity: 0;
+	transition: opacity 0.5s; /* 过渡 */
+	margin-bottom: var(--window-bottom); /* css变量 */
+}
+
+/* 适配 iPhoneX */
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
+	.mescroll-totop-safearea {
+		margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom)); /* window-bottom + 适配 iPhoneX */
+		margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom));
+	}
+}
+
+/* 显示 -- 淡入 */
+.mescroll-totop-in {
+	opacity: 1;
+}
+
+/* 隐藏 -- 淡出且不接收事件*/
+.mescroll-totop-out {
+	opacity: 0;
+	pointer-events: none;
+}
+</style>

+ 47 - 0
components/mescroll-uni/components/mescroll-up.css

@@ -0,0 +1,47 @@
+/* 上拉加载区域 */
+.mescroll-upwarp {
+	box-sizing: border-box;
+	min-height: 110rpx;
+	padding: 30rpx 0;
+	text-align: center;
+	clear: both;
+}
+
+/*提示文本 */
+.mescroll-upwarp .upwarp-tip,
+.mescroll-upwarp .upwarp-nodata {
+	display: inline-block;
+	font-size: 28rpx;
+	vertical-align: middle;
+	/* color: gray; 已在style设置color,此处删去*/
+}
+
+.mescroll-upwarp .upwarp-tip {
+	margin-left: 16rpx;
+}
+
+/*旋转进度条 */
+.mescroll-upwarp .upwarp-progress {
+	display: inline-block;
+	width: 32rpx;
+	height: 32rpx;
+	border-radius: 50%;
+	border: 2rpx solid gray;
+	border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
+	vertical-align: middle;
+}
+
+/* 旋转动画 */
+.mescroll-upwarp .mescroll-rotate {
+	animation: mescrollUpRotate 0.6s linear infinite;
+}
+
+@keyframes mescrollUpRotate {
+	0% {
+		transform: rotate(0deg);
+	}
+
+	100% {
+		transform: rotate(360deg);
+	}
+}

+ 39 - 0
components/mescroll-uni/components/mescroll-up.vue

@@ -0,0 +1,39 @@
+<!-- 上拉加载区域 -->
+<template>
+	<view class="mescroll-upwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
+		<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+		<view v-show="isUpLoading">
+			<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mOption.textColor}"></view>
+			<view class="upwarp-tip">{{ mOption.textLoading }}</view>
+		</view>
+		<!-- 无数据 -->
+		<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
+	</view>
+</template>
+
+<script>
+export default {
+	props: {
+		option: Object, // up的配置项
+		type: Number // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了)
+	},
+	computed: {
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
+		mOption() {
+			return this.option || {};
+		},
+		// 加载中
+		isUpLoading() {
+			return this.type === 1;
+		},
+		// 没有更多了
+		isUpNoMore() {
+			return this.type === 2;
+		}
+	}
+};
+</script>
+
+<style>
+@import './mescroll-up.css';
+</style>

+ 19 - 0
components/mescroll-uni/mescroll-body.css

@@ -0,0 +1,19 @@
+.mescroll-body {
+	position: relative; /* 下拉刷新区域相对自身定位 */
+	height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/
+	overflow: hidden; /* 当有元素写在mescroll-body标签前面时,可遮住下拉刷新区域 */
+	box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
+}
+
+/* 使sticky生效: 父元素不能overflow:hidden或者overflow:auto属性 */
+.mescroll-body.mescorll-sticky{
+	overflow: unset !important
+}
+
+/* 适配 iPhoneX */
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
+	.mescroll-safearea {
+		padding-bottom: constant(safe-area-inset-bottom);
+		padding-bottom: env(safe-area-inset-bottom);
+	}
+}

+ 352 - 0
components/mescroll-uni/mescroll-body.vue

@@ -0,0 +1,352 @@
+<template>
+	<view 
+	class="mescroll-body mescroll-render-touch" 
+	:class="{'mescorll-sticky': sticky}"
+	:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" 
+	@touchstart="wxsBiz.touchstartEvent" 
+	@touchmove="wxsBiz.touchmoveEvent" 
+	@touchend="wxsBiz.touchendEvent" 
+	@touchcancel="wxsBiz.touchendEvent"
+	:change:prop="wxsBiz.propObserver"
+	:prop="wxsProp"
+	>
+		<!-- 状态栏 -->
+		<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
+		
+		<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp">
+			<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
+			<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
+			<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
+				<view class="downwarp-content">
+					<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
+					<view class="downwarp-tip">{{downText}}</view>
+				</view>
+			</view>
+	
+			<!-- 列表内容 -->
+			<slot></slot>
+
+			<!-- 空布局 -->
+			<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
+
+
+      <view class="mescroll-upwarp--container">
+        <!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
+        <!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+        <view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
+          <!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+          <view v-show="upLoadType===1">
+            <view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
+            <view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
+          </view>
+          <!-- 无数据 -->
+          <view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
+        </view>
+      </view>
+      
+		</view>
+		
+		<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
+		<!-- #ifdef H5 -->
+		<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
+		<!-- #endif -->
+		
+		<!-- 适配iPhoneX -->
+		<view v-if="safearea" class="mescroll-safearea"></view>
+		
+		<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
+		
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from './wxs/renderjs.js';
+	export default {
+		mixins: [renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	// 引入mescroll-uni.js,处理核心逻辑
+	import MeScroll from './mescroll-uni.js';
+	// 引入全局配置
+	import GlobalOption from './mescroll-uni-option.js';
+	// 引入空布局组件
+	import MescrollEmpty from './components/mescroll-empty.vue';
+	// 引入回到顶部组件
+	import MescrollTop from './components/mescroll-top.vue';
+	// 引入兼容wxs(含renderjs)写法的mixins
+	import WxsMixin from './wxs/mixins.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollEmpty,
+			MescrollTop
+		},
+		data() {
+			return {
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
+				downHight: 0, //下拉刷新: 容器高度
+				downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
+				upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
+				isShowEmpty: false, // 是否显示空布局
+				isShowToTop: false, // 是否显示回到顶部按钮
+				windowHeight: 0, // 可使用窗口的高度
+				windowBottom: 0, // 可使用窗口的底部位置
+				statusBarHeight: 0 // 状态栏高度
+			};
+		},
+		props: {
+			down: Object, // 下拉刷新的参数配置
+			up: Object, // 上拉加载的参数配置
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+			height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
+				type: Boolean,
+				default: true
+			},
+			sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏
+		},
+		computed: {
+			// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+			minHeight(){
+				return this.toPx(this.height || '100%') + 'px'
+			},
+			// 下拉布局往下偏移的距离 (px)
+			numTop() {
+				return this.toPx(this.top)
+			},
+			padTop() {
+				return this.numTop + 'px';
+			},
+			// 上拉布局往上偏移 (px)
+			numBottom() {
+				return this.toPx(this.bottom);
+			},
+			padBottom() {
+				return this.numBottom + 'px';
+			},
+			// 是否为重置下拉的状态
+			isDownReset() {
+				return this.downLoadType === 3 || this.downLoadType === 4;
+			},
+			// 过渡
+			transition() {
+				return this.isDownReset ? 'transform 300ms' : '';
+			},
+			translateY() {
+				return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
+			},
+			// 是否在加载中
+			isDownLoading(){
+				return this.downLoadType === 3
+			},
+			// 旋转的角度
+			downRotate(){
+				return 'rotate(' + 360 * this.downRate + 'deg)'
+			},
+			// 文本提示
+			downText(){
+				if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
+				switch (this.downLoadType){
+					case 1: return this.mescroll.optDown.textInOffset;
+					case 2: return this.mescroll.optDown.textOutOffset;
+					case 3: return this.mescroll.optDown.textLoading;
+					case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
+					default: return this.mescroll.optDown.textInOffset;
+				}
+			}
+		},
+		methods: {
+			//number,rpx,upx,px,% --> px的数值
+			toPx(num) {
+				if (typeof num === 'string') {
+					if (num.indexOf('px') !== -1) {
+						if (num.indexOf('rpx') !== -1) {
+							// "10rpx"
+							num = num.replace('rpx', '');
+						} else if (num.indexOf('upx') !== -1) {
+							// "10upx"
+							num = num.replace('upx', '');
+						} else {
+							// "10px"
+							return Number(num.replace('px', ''));
+						}
+					} else if (num.indexOf('%') !== -1) {
+						// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
+						let rate = Number(num.replace('%', '')) / 100;
+						return this.windowHeight * rate;
+					}
+				}
+				return num ? uni.upx2px(Number(num)) : 0;
+			},
+			// 点击空布局的按钮回调
+			emptyClick() {
+				this.$emit('emptyclick', this.mescroll);
+			},
+			// 点击回到顶部的按钮回调
+			toTopClick() {
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
+			}
+		},
+		// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
+		created() {
+			let vm = this;
+
+			let diyOption = {
+				// 下拉刷新的配置
+				down: {
+					inOffset() {
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					outOffset() {
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					onMoving(mescroll, rate, downHight) {
+						// 下拉过程中的回调,滑动过程一直在执行;
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+					},
+					showLoading(mescroll, downHight) {
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+					},
+					beforeEndDownScroll(mescroll){
+						vm.downLoadType = 4; 
+						return mescroll.optDown.beforeEndDelay // 延时结束的时长
+					},
+					endDownScroll() {
+						vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
+						vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset
+							if(vm.downLoadType === 4) vm.downLoadType = 0
+						},300)
+					},
+					// 派发下拉刷新的回调
+					callback: function(mescroll) {
+						vm.$emit('down', mescroll);
+					}
+				},
+				// 上拉加载的配置
+				up: {
+					// 显示加载中的回调
+					showLoading() {
+						vm.upLoadType = 1;
+					},
+					// 显示无更多数据的回调
+					showNoMore() {
+						vm.upLoadType = 2;
+					},
+					// 隐藏上拉加载的回调
+					hideUpScroll(mescroll) {
+						vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
+					},
+					// 空布局
+					empty: {
+						onShow(isShow) {
+							// 显示隐藏的回调
+							vm.isShowEmpty = isShow;
+						}
+					},
+					// 回到顶部
+					toTop: {
+						onShow(isShow) {
+							// 显示隐藏的回调
+							vm.isShowToTop = isShow;
+						}
+					},
+					// 派发上拉加载的回调
+					callback: function(mescroll) {
+						vm.$emit('up', mescroll);
+					}
+				}
+			};
+
+			MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
+			// init回调mescroll对象
+			vm.$emit('init', vm.mescroll);
+
+			// 设置高度
+			const sys = uni.getSystemInfoSync();
+			if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
+			if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
+			if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+			// 使down的bottomOffset生效
+			vm.mescroll.setBodyHeight(sys.windowHeight);
+
+			// 因为使用的是page的scroll,这里需自定义scrollTo
+			vm.mescroll.resetScrollTo((y, t) => {
+				if(typeof y === 'string'){
+					// 滚动到指定view (y为css选择器)
+					setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
+						let selector;
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
+							selector = '#'+y // 不带#和. 则默认为id选择器
+						}else{
+							selector = y
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
+								selector = y.split('>>>')[1].trim()
+							}
+							// #endif
+						}
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
+							if (rect) {
+								let top = rect.top
+								top += vm.mescroll.getScrollTop()
+								uni.pageScrollTo({
+									scrollTop: top,
+									duration: t
+								})
+							} else{
+								console.error(selector + ' does not exist');
+							}
+						}).exec()
+					},30)
+				} else{
+					// 滚动到指定位置 (y必须为数字)
+					uni.pageScrollTo({
+						scrollTop: y,
+						duration: t
+					})
+				}
+			});
+
+			// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
+			if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
+				vm.mescroll.optUp.toTop.safearea = vm.safearea;
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	@import "./mescroll-body.css";
+	@import "./components/mescroll-down.css";
+	@import './components/mescroll-up.css';
+</style>

+ 65 - 0
components/mescroll-uni/mescroll-mixins.js

@@ -0,0 +1,65 @@
+// mescroll-body 和 mescroll-uni 通用
+
+// import MescrollUni from "./mescroll-uni.vue";
+// import MescrollBody from "./mescroll-body.vue";
+
+const MescrollMixin = {
+	// components: { // 非H5端无法通过mixin注册组件, 只能在main.js中注册全局组件或具体界面中注册
+	// 	MescrollUni,
+	// 	MescrollBody
+	// },
+	data() {
+		return {
+			mescroll: null //mescroll实例对象
+		}
+	},
+	// 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+	onPullDownRefresh(){
+		this.mescroll && this.mescroll.onPullDownRefresh();
+	},
+	// 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
+	onPageScroll(e) {
+		this.mescroll && this.mescroll.onPageScroll(e);
+	},
+	// 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
+	onReachBottom() {
+		this.mescroll && this.mescroll.onReachBottom();
+	},
+	methods: {
+		// mescroll组件初始化的回调,可获取到mescroll对象
+		mescrollInit(mescroll) {
+			this.mescroll = mescroll;
+			this.mescrollInitByRef(); // 兼容字节跳动小程序
+		},
+		// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序)
+		mescrollInitByRef() {
+			if(!this.mescroll || !this.mescroll.resetUpScroll){
+				let mescrollRef = this.$refs.mescrollRef;
+				if(mescrollRef) this.mescroll = mescrollRef.mescroll
+			}
+		},
+		// 下拉刷新的回调 (mixin默认resetUpScroll)
+		downCallback() {
+			if(this.mescroll.optUp.use){
+				this.mescroll.resetUpScroll()
+			}else{
+				setTimeout(()=>{
+					this.mescroll.endSuccess();
+				}, 500)
+			}
+		},
+		// 上拉加载的回调
+		upCallback() {
+			// mixin默认延时500自动结束加载
+			setTimeout(()=>{
+				this.mescroll.endErr();
+			}, 500)
+		}
+	},
+	mounted() {
+		this.mescrollInitByRef(); // 兼容字节跳动小程序, 避免未设置@init或@init此时未能取到ref的情况
+	}
+	
+}
+
+export default MescrollMixin;

+ 37 - 0
components/mescroll-uni/mescroll-uni-option.js

@@ -0,0 +1,37 @@
+// 全局配置
+// mescroll-body 和 mescroll-uni 通用
+const GlobalOption = {
+	down: {
+		// 其他down的配置参数也可以写,这里只展示了常用的配置:
+		textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
+		textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textSuccess: '加载成功', // 加载成功的文本
+		textErr: '加载失败', // 加载失败的文本
+		beforeEndDelay: 100, // 延时结束的时长 (显示加载成功/失败的时长)
+		offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
+		native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+	},
+	up: {
+		// 其他up的配置参数也可以写,这里只展示了常用的配置:
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textNoMore: '亲, 没有更多了', // 没有更多数据的提示文本
+		offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
+		toTop: {
+			// 回到顶部按钮,需配置src才显示
+			src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
+			offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
+			right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+			bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+			width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+		},
+		empty: {
+			use: true, // 是否显示空布局
+			// icon: "https://www.mescroll.com/img/mescroll-empty.png", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
+      		icon: '/static/empty.png',
+			tip: '亲,暂无相关数据' // 提示
+		}
+	}
+}
+
+export default GlobalOption

+ 36 - 0
components/mescroll-uni/mescroll-uni.css

@@ -0,0 +1,36 @@
+.mescroll-uni-warp{
+	height: 100%;
+}
+
+.mescroll-uni-content{
+	height: 100%;
+}
+
+.mescroll-uni {
+	position: relative;
+	width: 100%;
+	height: 100%;
+	min-height: 200rpx;
+	overflow-y: auto;
+	box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
+}
+
+/* 定位的方式固定高度 */
+.mescroll-uni-fixed{
+	z-index: 1;
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	width: auto; /* 使right生效 */
+	height: auto; /* 使bottom生效 */
+}
+
+/* 适配 iPhoneX */
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
+	.mescroll-safearea {
+		padding-bottom: constant(safe-area-inset-bottom);
+		padding-bottom: env(safe-area-inset-bottom);
+	}
+}

+ 799 - 0
components/mescroll-uni/mescroll-uni.js

@@ -0,0 +1,799 @@
+/* mescroll
+ * version 1.3.3
+ * 2020-09-15 wenju
+ * https://www.mescroll.com
+ */
+
+export default function MeScroll(options, isScrollBody) {
+	let me = this;
+	me.version = '1.3.3'; // mescroll版本号
+	me.options = options || {}; // 配置
+	me.isScrollBody = isScrollBody || false; // 滚动区域是否为原生页面滚动; 默认为scroll-view
+
+	me.isDownScrolling = false; // 是否在执行下拉刷新的回调
+	me.isUpScrolling = false; // 是否在执行上拉加载的回调
+	let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
+
+	// 初始化下拉刷新
+	me.initDownScroll();
+	// 初始化上拉加载,则初始化
+	me.initUpScroll();
+
+	// 自动加载
+	setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
+		// 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
+		if ((me.optDown.use || me.optDown.native) && me.optDown.auto && hasDownCallback) {
+			if (me.optDown.autoShowLoading) {
+				me.triggerDownScroll(); // 显示下拉进度,执行下拉回调
+			} else {
+				me.optDown.callback && me.optDown.callback(me); // 不显示下拉进度,直接执行下拉回调
+			}
+		}
+		// 自动触发上拉加载
+		if(!me.isUpAutoLoad){ // 部分小程序(头条小程序)emit是异步, 会导致isUpAutoLoad判断有误, 先延时确保先执行down的callback,再执行up的callback
+			setTimeout(function(){
+				me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll();
+			},100)
+		}
+	}, 30); // 需让me.optDown.inited和me.optUp.inited先执行
+}
+
+/* 配置参数:下拉刷新 */
+MeScroll.prototype.extendDownScroll = function(optDown) {
+	// 下拉刷新的配置
+	MeScroll.extend(optDown, {
+		use: true, // 是否启用下拉刷新; 默认true
+		auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
+		native: false, // 是否使用系统自带的下拉刷新; 默认false; 仅mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+		autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
+		isLock: false, // 是否锁定下拉刷新,默认false;
+		offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
+		startTop: 100, // scroll-view快速滚动到顶部时,此时的scroll-top可能大于0, 此值用于控制最大的误差
+		inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
+		outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
+		bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
+		minAngle: 45, // 向下滑动最少偏移的角度,取值区间  [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
+		textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
+		textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textSuccess: '加载成功', // 加载成功的文本
+		textErr: '加载失败', // 加载失败的文本
+		beforeEndDelay: 100, // 延时结束的时长 (显示加载成功/失败的时长)
+		bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
+		textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+		inited: null, // 下拉刷新初始化完毕的回调
+		inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
+		outOffset: null, // 下拉的距离大于offset那一刻的回调
+		onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
+		beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
+		showLoading: null, // 显示下拉刷新进度的回调
+		afterLoading: null, // 显示下拉刷新进度的回调之后,马上要执行的代码 (如: 在wxs中使用)
+		beforeEndDownScroll: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
+		endDownScroll: null, // 结束下拉刷新的回调
+		afterEndDownScroll: null, // 结束下拉刷新的回调,马上要执行的代码 (如: 在wxs中使用)
+		callback: function(mescroll) {
+			// 下拉刷新的回调;默认重置上拉加载列表为第一页
+			mescroll.resetUpScroll();
+		}
+	})
+}
+
+/* 配置参数:上拉加载 */
+MeScroll.prototype.extendUpScroll = function(optUp) {
+	// 上拉加载的配置
+	MeScroll.extend(optUp, {
+		use: true, // 是否启用上拉加载; 默认true
+		auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
+		isLock: false, // 是否锁定上拉加载,默认false;
+		isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
+		callback: null, // 上拉加载的回调;function(page,mescroll){ }
+		page: {
+			num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
+			size: 10, // 每页数据的数量
+			time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
+		},
+		noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
+		offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textNoMore: '-- END --', // 没有更多数据的提示文本
+		bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
+		textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+		inited: null, // 初始化完毕的回调
+		showLoading: null, // 显示加载中的回调
+		showNoMore: null, // 显示无更多数据的回调
+		hideUpScroll: null, // 隐藏上拉加载的回调
+		errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
+		toTop: {
+			// 回到顶部按钮,需配置src才显示
+			src: null, // 图片路径,默认null (绝对路径或网络图)
+			offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
+			duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
+			btnClick: null, // 点击按钮的回调
+			onShow: null, // 是否显示的回调
+			zIndex: 9990, // fixed定位z-index值
+			left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
+			width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+		},
+		empty: {
+			use: true, // 是否显示空布局
+			icon: null, // 图标路径
+			tip: '~ 暂无相关数据 ~', // 提示
+			btnText: '', // 按钮
+			btnClick: null, // 点击按钮的回调
+			onShow: null, // 是否显示的回调
+			fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
+			top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
+			zIndex: 99 // fixed定位z-index值
+		},
+		onScroll: false // 是否监听滚动事件
+	})
+}
+
+/* 配置参数 */
+MeScroll.extend = function(userOption, defaultOption) {
+	if (!userOption) return defaultOption;
+	for (let key in defaultOption) {
+		if (userOption[key] == null) {
+			let def = defaultOption[key];
+			if (def != null && typeof def === 'object') {
+				userOption[key] = MeScroll.extend({}, def); // 深度匹配
+			} else {
+				userOption[key] = def;
+			}
+		} else if (typeof userOption[key] === 'object') {
+			MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
+		}
+	}
+	return userOption;
+}
+
+/* 简单判断是否配置了颜色 (非透明,非白色) */
+MeScroll.prototype.hasColor = function(color) {
+	if(!color) return false;
+	let c = color.toLowerCase();
+	return c != "#fff" && c != "#ffffff" && c != "transparent" && c != "white"
+}
+
+/* -------初始化下拉刷新------- */
+MeScroll.prototype.initDownScroll = function() {
+	let me = this;
+	// 配置参数
+	me.optDown = me.options.down || {};
+	if(!me.optDown.textColor && me.hasColor(me.optDown.bgColor)) me.optDown.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
+	me.extendDownScroll(me.optDown);
+	
+	// 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
+	if(me.isScrollBody && me.optDown.native){
+		me.optDown.use = false
+	}else{
+		me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
+	}
+	
+	me.downHight = 0; // 下拉区域的高度
+
+	// 在页面中加入下拉布局
+	if (me.optDown.use && me.optDown.inited) {
+		// 初始化完毕的回调
+		setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
+			me.optDown.inited(me);
+		}, 0)
+	}
+}
+
+/* 列表touchstart事件 */
+MeScroll.prototype.touchstartEvent = function(e) {
+	if (!this.optDown.use) return;
+
+	this.startPoint = this.getPoint(e); // 记录起点
+	this.startTop = this.getScrollTop(); // 记录此时的滚动条位置
+	this.startAngle = 0; // 初始角度
+	this.lastPoint = this.startPoint; // 重置上次move的点
+	this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
+	this.inTouchend = false; // 标记不是touchend
+}
+
+/* 列表touchmove事件 */
+MeScroll.prototype.touchmoveEvent = function(e) {
+	if (!this.optDown.use) return;
+	let me = this;
+
+	let scrollTop = me.getScrollTop(); // 当前滚动条的距离
+	let curPoint = me.getPoint(e); // 当前点
+
+	let moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+
+	// 向下拉 && 在顶部
+	// mescroll-body,直接判定在顶部即可
+	// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
+	// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
+	if (moveY > 0 && (
+			(me.isScrollBody && scrollTop <= 0)
+			||
+			(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
+		)) {
+		// 可下拉的条件
+		if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
+				me.optUp.isBoth))) {
+
+			// 下拉的初始角度是否在配置的范围内
+			if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
+			if (me.startAngle < me.optDown.minAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
+
+			// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
+			if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
+				me.inTouchend = true; // 标记执行touchend
+				me.touchendEvent(); // 提前触发touchend
+				return;
+			}
+			
+			me.preventDefault(e); // 阻止默认事件
+
+			let diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
+
+			// 下拉距离  < 指定距离
+			if (me.downHight < me.optDown.offset) {
+				if (me.movetype !== 1) {
+					me.movetype = 1; // 加入标记,保证只执行一次
+					me.isDownEndSuccess = null; // 重置是否加载成功的状态 (wxs执行的是wxs.wxs)
+					me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+				}
+				me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
+
+				// 指定距离  <= 下拉距离
+			} else {
+				if (me.movetype !== 2) {
+					me.movetype = 2; // 加入标记,保证只执行一次
+					me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+				}
+				if (diff > 0) { // 向下拉
+					me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
+				} else { // 向上收
+					me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
+				}
+			}
+			
+			me.downHight = Math.round(me.downHight) // 取整
+			let rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
+			me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
+		}
+	}
+
+	me.lastPoint = curPoint; // 记录本次移动的点
+}
+
+/* 列表touchend事件 */
+MeScroll.prototype.touchendEvent = function(e) {
+	if (!this.optDown.use) return;
+	// 如果下拉区域高度已改变,则需重置回来
+	if (this.isMoveDown) {
+		if (this.downHight >= this.optDown.offset) {
+			// 符合触发刷新的条件
+			this.triggerDownScroll();
+		} else {
+			// 不符合的话 则重置
+			this.downHight = 0;
+			this.endDownScrollCall(this);
+		}
+		this.movetype = 0;
+		this.isMoveDown = false;
+	} else if (!this.isScrollBody && this.getScrollTop() === this.startTop) { // scroll-view到顶/左/右/底的滑动事件
+		let isScrollUp = this.getPoint(e).y - this.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+		// 上滑
+		if (isScrollUp) {
+			// 需检查滑动的角度
+			let angle = this.getAngle(this.getPoint(e), this.startPoint); // 两点之间的角度,区间 [0,90]
+			if (angle > 80) {
+				// 检查并触发上拉
+				this.triggerUpScroll(true);
+			}
+		}
+	}
+}
+
+/* 根据点击滑动事件获取第一个手指的坐标 */
+MeScroll.prototype.getPoint = function(e) {
+	if (!e) {
+		return {
+			x: 0,
+			y: 0
+		}
+	}
+	if (e.touches && e.touches[0]) {
+		return {
+			x: e.touches[0].pageX,
+			y: e.touches[0].pageY
+		}
+	} else if (e.changedTouches && e.changedTouches[0]) {
+		return {
+			x: e.changedTouches[0].pageX,
+			y: e.changedTouches[0].pageY
+		}
+	} else {
+		return {
+			x: e.clientX,
+			y: e.clientY
+		}
+	}
+}
+
+/* 计算两点之间的角度: 区间 [0,90]*/
+MeScroll.prototype.getAngle = function(p1, p2) {
+	let x = Math.abs(p1.x - p2.x);
+	let y = Math.abs(p1.y - p2.y);
+	let z = Math.sqrt(x * x + y * y);
+	let angle = 0;
+	if (z !== 0) {
+		angle = Math.asin(y / z) / Math.PI * 180;
+	}
+	return angle
+}
+
+/* 触发下拉刷新 */
+MeScroll.prototype.triggerDownScroll = function() {
+	if (this.optDown.beforeLoading && this.optDown.beforeLoading(this)) {
+		//return true则处于完全自定义状态
+	} else {
+		this.showDownScroll(); // 下拉刷新中...
+		!this.optDown.native && this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
+	}
+}
+
+/* 显示下拉进度布局 */
+MeScroll.prototype.showDownScroll = function() {
+	this.isDownScrolling = true; // 标记下拉中
+	if (this.optDown.native) {
+		uni.startPullDownRefresh(); // 系统自带的下拉刷新
+		this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
+	} else{
+		this.downHight = this.optDown.offset; // 更新下拉区域高度
+		this.showDownLoadingCall(this.downHight); // 下拉刷新中...
+	}
+}
+
+MeScroll.prototype.showDownLoadingCall = function(downHight) {
+	this.optDown.showLoading && this.optDown.showLoading(this, downHight); // 下拉刷新中...
+	this.optDown.afterLoading && this.optDown.afterLoading(this, downHight); // 下拉刷新中...触发之后马上要执行的代码
+}
+
+/* 显示系统自带的下拉刷新时需要处理的业务 */
+MeScroll.prototype.onPullDownRefresh = function() {
+	this.isDownScrolling = true; // 标记下拉中
+	this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
+	this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
+}
+
+/* 结束下拉刷新 */
+MeScroll.prototype.endDownScroll = function() {
+	if (this.optDown.native) { // 结束原生下拉刷新
+		this.isDownScrolling = false;
+		this.endDownScrollCall(this);
+		uni.stopPullDownRefresh();
+		return
+	}
+	let me = this;
+	// 结束下拉刷新的方法
+	let endScroll = function() {
+		me.downHight = 0;
+		me.isDownScrolling = false;
+		me.endDownScrollCall(me);
+		if(!me.isScrollBody){
+			me.setScrollHeight(0) // scroll-view重置滚动区域,使数据不满屏时仍可检查触发翻页
+			me.scrollTo(0,0) // scroll-view需重置滚动条到顶部,避免startTop大于0时,对下拉刷新的影响
+		}
+	}
+	// 结束下拉刷新时的回调
+	let delay = 0;
+	if (me.optDown.beforeEndDownScroll) {
+		delay = me.optDown.beforeEndDownScroll(me); // 结束下拉刷新的延时,单位ms
+		if(me.isDownEndSuccess == null) delay = 0; // 没有执行加载中,则不延时
+	}
+	if (typeof delay === 'number' && delay > 0) {
+		setTimeout(endScroll, delay);
+	} else {
+		endScroll();
+	}
+}
+
+MeScroll.prototype.endDownScrollCall = function() {
+	this.optDown.endDownScroll && this.optDown.endDownScroll(this);
+	this.optDown.afterEndDownScroll && this.optDown.afterEndDownScroll(this);
+}
+
+/* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */
+MeScroll.prototype.lockDownScroll = function(isLock) {
+	if (isLock == null) isLock = true;
+	this.optDown.isLock = isLock;
+}
+
+/* 锁定上拉加载:isLock=ture,null锁定;isLock=false解锁 */
+MeScroll.prototype.lockUpScroll = function(isLock) {
+	if (isLock == null) isLock = true;
+	this.optUp.isLock = isLock;
+}
+
+/* -------初始化上拉加载------- */
+MeScroll.prototype.initUpScroll = function() {
+	let me = this;
+	// 配置参数
+	me.optUp = me.options.up || {use: false}
+	if(!me.optUp.textColor && me.hasColor(me.optUp.bgColor)) me.optUp.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
+	me.extendUpScroll(me.optUp);
+
+	if (me.optUp.use === false) return; // 配置不使用上拉加载时,则不初始化上拉布局
+	me.optUp.hasNext = true; // 如果使用上拉,则默认有下一页
+	me.startNum = me.optUp.page.num + 1; // 记录page开始的页码
+
+	// 初始化完毕的回调
+	if (me.optUp.inited) {
+		setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
+			me.optUp.inited(me);
+		}, 0)
+	}
+}
+
+/*滚动到底部的事件 (仅mescroll-body生效)*/
+MeScroll.prototype.onReachBottom = function() {
+	if (this.isScrollBody && !this.isUpScrolling) { // 只能支持下拉刷新的时候同时可以触发上拉加载,否则滚动到底部就需要上滑一点才能触发onReachBottom
+		if (!this.optUp.isLock && this.optUp.hasNext) {
+			this.triggerUpScroll();
+		}
+	}
+}
+
+/*列表滚动事件 (仅mescroll-body生效)*/
+MeScroll.prototype.onPageScroll = function(e) {
+	if (!this.isScrollBody) return;
+	
+	// 更新滚动条的位置 (主要用于判断下拉刷新时,滚动条是否在顶部)
+	this.setScrollTop(e.scrollTop);
+
+	// 顶部按钮的显示隐藏
+	if (e.scrollTop >= this.optUp.toTop.offset) {
+		this.showTopBtn();
+	} else {
+		this.hideTopBtn();
+	}
+}
+
+/*列表滚动事件*/
+MeScroll.prototype.scroll = function(e, onScroll) {
+	// 更新滚动条的位置
+	this.setScrollTop(e.scrollTop);
+	// 更新滚动内容高度
+	this.setScrollHeight(e.scrollHeight);
+
+	// 向上滑还是向下滑动
+	if (this.preScrollY == null) this.preScrollY = 0;
+	this.isScrollUp = e.scrollTop - this.preScrollY > 0;
+	this.preScrollY = e.scrollTop;
+
+	// 上滑 && 检查并触发上拉
+	this.isScrollUp && this.triggerUpScroll(true);
+
+	// 顶部按钮的显示隐藏
+	if (e.scrollTop >= this.optUp.toTop.offset) {
+		this.showTopBtn();
+	} else {
+		this.hideTopBtn();
+	}
+
+	// 滑动监听
+	this.optUp.onScroll && onScroll && onScroll()
+}
+
+/* 触发上拉加载 */
+MeScroll.prototype.triggerUpScroll = function(isCheck) {
+	if (!this.isUpScrolling && this.optUp.use && this.optUp.callback) {
+		// 是否校验在底部; 默认不校验
+		if (isCheck === true) {
+			let canUp = false;
+			// 还有下一页 && 没有锁定 && 不在下拉中
+			if (this.optUp.hasNext && !this.optUp.isLock && !this.isDownScrolling) {
+				if (this.getScrollBottom() <= this.optUp.offset) { // 到底部
+					canUp = true; // 标记可上拉
+				}
+			}
+			if (canUp === false) return;
+		}
+		this.showUpScroll(); // 上拉加载中...
+		this.optUp.page.num++; // 预先加一页,如果失败则减回
+		this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
+		this.num = this.optUp.page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
+		this.size = this.optUp.page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
+		this.time = this.optUp.page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
+		this.optUp.callback(this); // 执行回调,联网加载数据
+	}
+}
+
+/* 显示上拉加载中 */
+MeScroll.prototype.showUpScroll = function() {
+	this.isUpScrolling = true; // 标记上拉加载中
+	this.optUp.showLoading && this.optUp.showLoading(this); // 回调
+}
+
+/* 显示上拉无更多数据 */
+MeScroll.prototype.showNoMore = function() {
+	this.optUp.hasNext = false; // 标记无更多数据
+	this.optUp.showNoMore && this.optUp.showNoMore(this); // 回调
+}
+
+/* 隐藏上拉区域**/
+MeScroll.prototype.hideUpScroll = function() {
+	this.optUp.hideUpScroll && this.optUp.hideUpScroll(this); // 回调
+}
+
+/* 结束上拉加载 */
+MeScroll.prototype.endUpScroll = function(isShowNoMore) {
+	if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
+		if (isShowNoMore) {
+			this.showNoMore(); // isShowNoMore=true,显示无更多数据
+		} else {
+			this.hideUpScroll(); // isShowNoMore=false,隐藏上拉加载
+		}
+	}
+	this.isUpScrolling = false; // 标记结束上拉加载
+}
+
+/* 重置上拉加载列表为第一页
+ *isShowLoading 是否显示进度布局;
+ * 1.默认null,不传参,则显示上拉加载的进度布局
+ * 2.传参true, 则显示下拉刷新的进度布局
+ * 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据)
+ */
+MeScroll.prototype.resetUpScroll = function(isShowLoading) {
+	if (this.optUp && this.optUp.use) {
+		let page = this.optUp.page;
+		this.prePageNum = page.num; // 缓存重置前的页码,加载失败可退回
+		this.prePageTime = page.time; // 缓存重置前的时间,加载失败可退回
+		page.num = this.startNum; // 重置为第一页
+		page.time = null; // 重置时间为空
+		if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
+			if (isShowLoading == null) {
+				this.removeEmpty(); // 移除空布局
+				this.showUpScroll(); // 不传参,默认显示上拉加载的进度布局
+			} else {
+				this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
+			}
+		}
+		this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
+		this.num = page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
+		this.size = page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
+		this.time = page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
+		this.optUp.callback && this.optUp.callback(this); // 执行上拉回调
+	}
+}
+
+/* 设置page.num的值 */
+MeScroll.prototype.setPageNum = function(num) {
+	this.optUp.page.num = num - 1;
+}
+
+/* 设置page.size的值 */
+MeScroll.prototype.setPageSize = function(size) {
+	this.optUp.page.size = size;
+}
+
+/* 联网回调成功,结束下拉刷新和上拉加载
+ * dataSize: 当前页的数据量(必传)
+ * totalPage: 总页数(必传)
+ * systime: 服务器时间 (可空)
+ */
+MeScroll.prototype.endByPage = function(dataSize, totalPage, systime) {
+	let hasNext;
+	if (this.optUp.use && totalPage != null) hasNext = this.optUp.page.num < totalPage; // 是否还有下一页
+	this.endSuccess(dataSize, hasNext, systime);
+}
+
+/* 联网回调成功,结束下拉刷新和上拉加载
+ * dataSize: 当前页的数据量(必传)
+ * totalSize: 列表所有数据总数量(必传)
+ * systime: 服务器时间 (可空)
+ */
+MeScroll.prototype.endBySize = function(dataSize, totalSize, systime) {
+	let hasNext;
+	if (this.optUp.use && totalSize != null) {
+		let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
+		hasNext = loadSize < totalSize; // 是否还有下一页
+	}
+	this.endSuccess(dataSize, hasNext, systime);
+}
+
+/* 联网回调成功,结束下拉刷新和上拉加载
+ * dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页
+ * hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
+ * systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录
+ */
+MeScroll.prototype.endSuccess = function(dataSize, hasNext, systime) {
+	let me = this;
+	// 结束下拉刷新
+	if (me.isDownScrolling) {
+		me.isDownEndSuccess = true
+		me.endDownScroll();
+	}
+
+	// 结束上拉加载
+	if (me.optUp.use) {
+		let isShowNoMore; // 是否已无更多数据
+		if (dataSize != null) {
+			let pageNum = me.optUp.page.num; // 当前页码
+			let pageSize = me.optUp.page.size; // 每页长度
+			// 如果是第一页
+			if (pageNum === 1) {
+				if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
+			}
+			if (dataSize < pageSize || hasNext === false) {
+				// 返回的数据不满一页时,则说明已无更多数据
+				me.optUp.hasNext = false;
+				if (dataSize === 0 && pageNum === 1) {
+					// 如果第一页无任何数据且配置了空布局
+					isShowNoMore = false;
+					me.showEmpty();
+				} else {
+					// 总列表数少于配置的数量,则不显示无更多数据
+					let allDataSize = (pageNum - 1) * pageSize + dataSize;
+					if (allDataSize < me.optUp.noMoreSize) {
+						isShowNoMore = false;
+					} else {
+						isShowNoMore = true;
+					}
+					me.removeEmpty(); // 移除空布局
+				}
+			} else {
+				// 还有下一页
+				isShowNoMore = false;
+				me.optUp.hasNext = true;
+				me.removeEmpty(); // 移除空布局
+			}
+		}
+
+		// 隐藏上拉
+		me.endUpScroll(isShowNoMore);
+	}
+}
+
+/* 回调失败,结束下拉刷新和上拉加载 */
+MeScroll.prototype.endErr = function(errDistance) {
+	// 结束下拉,回调失败重置回原来的页码和时间
+	if (this.isDownScrolling) {
+		this.isDownEndSuccess = false
+		let page = this.optUp.page;
+		if (page && this.prePageNum) {
+			page.num = this.prePageNum;
+			page.time = this.prePageTime;
+		}
+		this.endDownScroll();
+	}
+	// 结束上拉,回调失败重置回原来的页码
+	if (this.isUpScrolling) {
+		this.optUp.page.num--;
+		this.endUpScroll(false);
+		// 如果是mescroll-body,则需往回滚一定距离
+		if(this.isScrollBody && errDistance !== 0){ // 不处理0
+			if(!errDistance) errDistance = this.optUp.errDistance; // 不传,则取默认
+			this.scrollTo(this.getScrollTop() - errDistance, 0) // 往上回滚的距离
+		}
+	}
+}
+
+/* 显示空布局 */
+MeScroll.prototype.showEmpty = function() {
+	this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(true)
+}
+
+/* 移除空布局 */
+MeScroll.prototype.removeEmpty = function() {
+	this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(false)
+}
+
+/* 显示回到顶部的按钮 */
+MeScroll.prototype.showTopBtn = function() {
+	if (!this.topBtnShow) {
+		this.topBtnShow = true;
+		this.optUp.toTop.onShow && this.optUp.toTop.onShow(true);
+	}
+}
+
+/* 隐藏回到顶部的按钮 */
+MeScroll.prototype.hideTopBtn = function() {
+	if (this.topBtnShow) {
+		this.topBtnShow = false;
+		this.optUp.toTop.onShow && this.optUp.toTop.onShow(false);
+	}
+}
+
+/* 获取滚动条的位置 */
+MeScroll.prototype.getScrollTop = function() {
+	return this.scrollTop || 0
+}
+
+/* 记录滚动条的位置 */
+MeScroll.prototype.setScrollTop = function(y) {
+	this.scrollTop = y;
+}
+
+/* 滚动到指定位置 */
+MeScroll.prototype.scrollTo = function(y, t) {
+	this.myScrollTo && this.myScrollTo(y, t) // scrollview需自定义回到顶部方法
+}
+
+/* 自定义scrollTo */
+MeScroll.prototype.resetScrollTo = function(myScrollTo) {
+	this.myScrollTo = myScrollTo
+}
+
+/* 滚动条到底部的距离 */
+MeScroll.prototype.getScrollBottom = function() {
+	return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop()
+}
+
+/* 计步器
+ star: 开始值
+ end: 结束值
+ callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器;
+ t: 计步时长,传0则直接回调end值;不传则默认300ms
+ rate: 周期;不传则默认30ms计步一次
+ * */
+MeScroll.prototype.getStep = function(star, end, callback, t, rate) {
+	let diff = end - star; // 差值
+	if (t === 0 || diff === 0) {
+		callback && callback(end);
+		return;
+	}
+	t = t || 300; // 时长 300ms
+	rate = rate || 30; // 周期 30ms
+	let count = t / rate; // 次数
+	let step = diff / count; // 步长
+	let i = 0; // 计数
+	let timer = setInterval(function() {
+		if (i < count - 1) {
+			star += step;
+			callback && callback(star, timer);
+			i++;
+		} else {
+			callback && callback(end, timer); // 最后一次直接设置end,避免计算误差
+			clearInterval(timer);
+		}
+	}, rate);
+}
+
+/* 滚动容器的高度 */
+MeScroll.prototype.getClientHeight = function(isReal) {
+	let h = this.clientHeight || 0
+	if (h === 0 && isReal !== true) { // 未获取到容器的高度,可临时取body的高度 (可能会有误差)
+		h = this.getBodyHeight()
+	}
+	return h
+}
+MeScroll.prototype.setClientHeight = function(h) {
+	this.clientHeight = h;
+}
+
+/* 滚动内容的高度 */
+MeScroll.prototype.getScrollHeight = function() {
+	return this.scrollHeight || 0;
+}
+MeScroll.prototype.setScrollHeight = function(h) {
+	this.scrollHeight = h;
+}
+
+/* body的高度 */
+MeScroll.prototype.getBodyHeight = function() {
+	return this.bodyHeight || 0;
+}
+MeScroll.prototype.setBodyHeight = function(h) {
+	this.bodyHeight = h;
+}
+
+/* 阻止浏览器默认滚动事件 */
+MeScroll.prototype.preventDefault = function(e) {
+	// 小程序不支持e.preventDefault, 已在wxs中禁止
+	// app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止, 或使用renderjs禁止
+	// cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
+	if (e && e.cancelable && !e.defaultPrevented) e.preventDefault()
+}

+ 424 - 0
components/mescroll-uni/mescroll-uni.vue

@@ -0,0 +1,424 @@
+<template>
+	<view class="mescroll-uni-warp">
+		<scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false">
+			<view class="mescroll-uni-content mescroll-render-touch"
+			@touchstart="wxsBiz.touchstartEvent" 
+			@touchmove="wxsBiz.touchmoveEvent" 
+			@touchend="wxsBiz.touchendEvent" 
+			@touchcancel="wxsBiz.touchendEvent"
+			:change:prop="wxsBiz.propObserver"
+			:prop="wxsProp">
+				<!-- 状态栏 -->
+				<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
+		
+				<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp">
+					<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
+					<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
+					<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
+						<view class="downwarp-content">
+							<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
+							<view class="downwarp-tip">{{downText}}</view>
+						</view>
+					</view>
+
+					<!-- 列表内容 -->
+					<slot></slot>
+
+					<!-- 空布局 -->
+					<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
+
+					<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
+					<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+					<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
+						<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+						<view v-show="upLoadType===1">
+							<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
+							<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
+						</view>
+						<!-- 无数据 -->
+						<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
+					</view>
+				</view>
+			
+				<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
+				<!-- #ifdef H5 -->
+				<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
+				<!-- #endif -->
+				
+				<!-- 适配iPhoneX -->
+				<view v-if="safearea" class="mescroll-safearea"></view>
+			</view>
+		</scroll-view>
+
+		<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
+		
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from './wxs/renderjs.js';
+	export default {
+		mixins:[renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	// 引入mescroll-uni.js,处理核心逻辑
+	import MeScroll from './mescroll-uni.js';
+	// 引入全局配置
+	import GlobalOption from './mescroll-uni-option.js';
+	// 引入空布局组件
+	import MescrollEmpty from './components/mescroll-empty.vue';
+	// 引入回到顶部组件
+	import MescrollTop from './components/mescroll-top.vue';
+	// 引入兼容wxs(含renderjs)写法的mixins
+	import WxsMixin from './wxs/mixins.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollEmpty,
+			MescrollTop
+		},
+		data() {
+			return {
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
+				viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
+				downHight: 0, //下拉刷新: 容器高度
+				downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
+				upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
+				isShowEmpty: false, // 是否显示空布局
+				isShowToTop: false, // 是否显示回到顶部按钮
+				scrollTop: 0, // 滚动条的位置
+				scrollAnim: false, // 是否开启滚动动画
+				windowTop: 0, // 可使用窗口的顶部位置
+				windowBottom: 0, // 可使用窗口的底部位置
+				windowHeight: 0, // 可使用窗口的高度
+				statusBarHeight: 0 // 状态栏高度
+			}
+		},
+		props: {
+			down: Object, // 下拉刷新的参数配置
+			up: Object, // 上拉加载的参数配置
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+			fixed: { // 是否通过fixed固定mescroll的高度, 默认true
+				type: Boolean,
+				default: true
+			},
+			height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
+				type: Boolean,
+				default: true
+			}
+		},
+		computed: {
+			// 是否使用fixed定位 (当height有值,则不使用)
+			isFixed(){
+				return !this.height && this.fixed
+			},
+			// mescroll的高度
+			scrollHeight(){
+				if (this.isFixed) {
+					return "auto"
+				} else if(this.height){
+					return this.toPx(this.height) + 'px'
+				}else{
+					return "100%"
+				}
+			},
+			// 下拉布局往下偏移的距离 (px)
+			numTop() {
+				return this.toPx(this.top)
+			},
+			fixedTop() {
+				return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
+			},
+			padTop() {
+				return !this.isFixed ? this.numTop + 'px' : 0
+			},
+			// 上拉布局往上偏移 (px)
+			numBottom() {
+				return this.toPx(this.bottom)
+			},
+			fixedBottom() {
+				return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
+			},
+			padBottom() {
+				return !this.isFixed ? this.numBottom + 'px' : 0
+			},
+			// 是否为重置下拉的状态
+			isDownReset(){
+				return this.downLoadType===3 || this.downLoadType===4
+			},
+			// 过渡
+			transition() {
+				return this.isDownReset ? 'transform 300ms' : '';
+			},
+			translateY() {
+				return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
+			},
+			// 列表是否可滑动
+			scrollable(){
+				return this.downLoadType===0 || this.isDownReset
+			},
+			// 是否在加载中
+			isDownLoading(){
+				return this.downLoadType === 3
+			},
+			// 旋转的角度
+			downRotate(){
+				return 'rotate(' + 360 * this.downRate + 'deg)'
+			},
+			// 文本提示
+			downText(){
+				if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
+				switch (this.downLoadType){
+					case 1: return this.mescroll.optDown.textInOffset;
+					case 2: return this.mescroll.optDown.textOutOffset;
+					case 3: return this.mescroll.optDown.textLoading;
+					case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
+					default: return this.mescroll.optDown.textInOffset;
+				}
+			}
+		},
+		methods: {
+			//number,rpx,upx,px,% --> px的数值
+			toPx(num){
+				if(typeof num === "string"){
+					if (num.indexOf('px') !== -1) {
+						if(num.indexOf('rpx') !== -1) { // "10rpx"
+							num = num.replace('rpx', '');
+						} else if(num.indexOf('upx') !== -1) { // "10upx"
+							num = num.replace('upx', '');
+						} else { // "10px"
+							return Number(num.replace('px', ''))
+						}
+					}else if (num.indexOf('%') !== -1){
+						// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
+						let rate = Number(num.replace("%","")) / 100
+						return this.windowHeight * rate
+					}
+				}
+				return num ? uni.upx2px(Number(num)) : 0
+			},
+			//注册列表滚动事件,用于下拉刷新和上拉加载
+			scroll(e) {
+				this.mescroll.scroll(e.detail, () => {
+					this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
+				})
+			},
+			// 点击空布局的按钮回调
+			emptyClick() {
+				this.$emit('emptyclick', this.mescroll)
+			},
+			// 点击回到顶部的按钮回调
+			toTopClick() {
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
+			},
+			// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
+			setClientHeight() {
+				if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
+					this.isExec = true; // 避免多次获取
+					this.$nextTick(() => { // 确保dom已渲染
+						this.getClientInfo(data=>{
+							this.isExec = false;
+							if (data) {
+								this.mescroll.setClientHeight(data.height);
+							} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
+								this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
+								setTimeout(() => {
+									this.setClientHeight()
+								}, this.clientNum * 100)
+							}
+						})
+					})
+				}
+			},
+			// 获取滚动区域的信息
+			getClientInfo(success){
+				let query = uni.createSelectorQuery();
+				// #ifndef MP-ALIPAY || MP-DINGTALK
+				query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
+				// #endif
+				let view = query.select('#' + this.viewId);
+				view.boundingClientRect(data => {
+					success(data)
+				}).exec();
+			}
+		},
+		// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
+		created() {
+			let vm = this;
+
+			let diyOption = {
+				// 下拉刷新的配置
+				down: {
+					inOffset() {
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					outOffset() {
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					onMoving(mescroll, rate, downHight) {
+						// 下拉过程中的回调,滑动过程一直在执行;
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+					},
+					showLoading(mescroll, downHight) {
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+					},
+					beforeEndDownScroll(mescroll){
+						vm.downLoadType = 4; 
+						return mescroll.optDown.beforeEndDelay // 延时结束的时长
+					},
+					endDownScroll() {
+						vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
+						vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						vm.downResetTimer && clearTimeout(vm.downResetTimer)
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
+							if(vm.downLoadType===4) vm.downLoadType = 0
+						},300)
+					},
+					// 派发下拉刷新的回调
+					callback: function(mescroll) {
+						vm.$emit('down', mescroll)
+					}
+				},
+				// 上拉加载的配置
+				up: {
+					// 显示加载中的回调
+					showLoading() {
+						vm.upLoadType = 1;
+					},
+					// 显示无更多数据的回调
+					showNoMore() {
+						vm.upLoadType = 2;
+					},
+					// 隐藏上拉加载的回调
+					hideUpScroll(mescroll) {
+						vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
+					},
+					// 空布局
+					empty: {
+						onShow(isShow) { // 显示隐藏的回调
+							vm.isShowEmpty = isShow;
+						}
+					},
+					// 回到顶部
+					toTop: {
+						onShow(isShow) { // 显示隐藏的回调
+							vm.isShowToTop = isShow;
+						}
+					},
+					// 派发上拉加载的回调
+					callback: function(mescroll) {
+						vm.$emit('up', mescroll);
+						// 更新容器的高度 (多mescroll的情况)
+						vm.setClientHeight()
+					}
+				}
+			}
+
+			MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption);
+			vm.mescroll.viewId = vm.viewId; // 附带id
+			// init回调mescroll对象
+			vm.$emit('init', vm.mescroll);
+			
+			// 设置高度
+			const sys = uni.getSystemInfoSync();
+			if(sys.windowTop) vm.windowTop = sys.windowTop;
+			if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
+			if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
+			if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+			// 使down的bottomOffset生效
+			vm.mescroll.setBodyHeight(sys.windowHeight);
+
+			// 因为使用的是scrollview,这里需自定义scrollTo
+			vm.mescroll.resetScrollTo((y, t) => {
+				vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
+				if(typeof y === 'string'){
+					// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现
+					vm.getClientInfo(function(rect){
+						let mescrollTop = rect.top // mescroll到顶部的距离
+						let selector;
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
+							selector = '#'+y // 不带#和. 则默认为id选择器
+						}else{
+							selector = y
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
+								selector = y.split('>>>')[1].trim()
+							}
+							// #endif
+						}
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
+							if (rect) {
+								let curY = vm.mescroll.getScrollTop()
+								let top = rect.top - mescrollTop
+								top += curY
+								if(!vm.isFixed) top -= vm.numTop
+								vm.scrollTop = curY;
+								vm.$nextTick(function() {
+									vm.scrollTop = top
+								})
+							} else{
+								console.error(selector + ' does not exist');
+							}
+						}).exec()
+					})
+					return;
+				}
+				let curY = vm.mescroll.getScrollTop()
+				if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
+					vm.scrollTop = curY;
+					vm.$nextTick(function() {
+						vm.scrollTop = y
+					})
+				} else {
+					vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
+						vm.scrollTop = step
+					}, t)
+				}
+			})
+			
+			// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
+			if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
+				vm.mescroll.optUp.toTop.safearea = vm.safearea;
+			}
+		},
+		mounted() {
+			// 设置容器的高度
+			this.setClientHeight()
+		}
+	}
+</script>
+
+<style>
+	@import "./mescroll-uni.css";
+	@import "./components/mescroll-down.css";
+	@import './components/mescroll-up.css';
+</style>

+ 48 - 0
components/mescroll-uni/mixins/mescroll-comp.js

@@ -0,0 +1,48 @@
+/**
+ * mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期
+ */
+const MescrollCompMixin = {
+	// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件 (一级)
+	onPageScroll(e) {
+		this.handlePageScroll(e)
+	},
+	onReachBottom() {
+		this.handleReachBottom()
+	},
+	// 当down的native: true时, 还需传递此方法进到子组件
+	onPullDownRefresh(){
+		this.handlePullDownRefresh()
+	},
+	// mescroll-body写在子子子...组件的情况 (多级)
+	data() {
+		return {
+			mescroll: {
+				onPageScroll: e=>{
+					this.handlePageScroll(e)
+				},
+				onReachBottom: ()=>{
+					this.handleReachBottom()
+				},
+				onPullDownRefresh: ()=>{
+					this.handlePullDownRefresh()
+				}
+			}
+		}
+	},
+	methods:{
+		handlePageScroll(e){
+			let item = this.$refs["mescrollItem"];
+			if(item && item.mescroll) item.mescroll.onPageScroll(e);
+		},
+		handleReachBottom(){
+			let item = this.$refs["mescrollItem"];
+			if(item && item.mescroll) item.mescroll.onReachBottom();
+		},
+		handlePullDownRefresh(){
+			let item = this.$refs["mescrollItem"];
+			if(item && item.mescroll) item.mescroll.onPullDownRefresh();
+		}
+	}
+}
+
+export default MescrollCompMixin;

+ 59 - 0
components/mescroll-uni/mixins/mescroll-more-item.js

@@ -0,0 +1,59 @@
+/**
+ * mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
+ */
+const MescrollMoreItemMixin = {
+	// 支付宝小程序不支持props的mixin,需写在具体的页面中
+	// #ifndef MP-ALIPAY || MP-DINGTALK
+	props:{
+		i: Number, // 每个tab页的专属下标
+		index: { // 当前tab的下标
+			type: Number,
+			default(){
+				return 0
+			}
+		}
+	},
+	// #endif
+	data() {
+		return {
+			downOption:{
+				auto:false // 不自动加载
+			},
+			upOption:{
+				auto:false // 不自动加载
+			},
+			isInit: false // 当前tab是否已初始化
+		}
+	},
+	watch:{
+		// 监听下标的变化
+		index(val){
+			if (this.i === val && !this.isInit) {
+				this.isInit = true; // 标记为true
+				this.mescroll && this.mescroll.triggerDownScroll();
+			}
+		}
+	},
+	methods: {
+		// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序)
+		mescrollInitByRef() {
+			if(!this.mescroll || !this.mescroll.resetUpScroll){
+				// 字节跳动小程序编辑器不支持一个页面存在相同的ref, 多mescroll的ref需动态生成, 格式为'mescrollRef下标'
+				let mescrollRef = this.$refs.mescrollRef || this.$refs['mescrollRef'+this.i];
+				if(mescrollRef) this.mescroll = mescrollRef.mescroll
+			}
+		},
+		// mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
+		mescrollInit(mescroll) {
+			this.mescroll = mescroll;
+			this.mescrollInitByRef && this.mescrollInitByRef(); // 兼容字节跳动小程序
+			// 自动加载当前tab的数据
+			if(this.i === this.index){
+				this.isInit = true; // 标记为true
+				this.mescroll.triggerDownScroll();
+			}
+		},
+	}
+}
+
+export default MescrollMoreItemMixin;

+ 74 - 0
components/mescroll-uni/mixins/mescroll-more.js

@@ -0,0 +1,74 @@
+/**
+ * mescroll-body写在子组件时, 需通过mescroll的mixins补充子组件缺少的生命周期
+ */
+const MescrollMoreMixin = {
+	data() {
+		return {
+			tabIndex: 0, // 当前tab下标
+			mescroll: {
+				onPageScroll: e=>{
+					this.handlePageScroll(e)
+				},
+				onReachBottom: ()=>{
+					this.handleReachBottom()
+				},
+				onPullDownRefresh: ()=>{
+					this.handlePullDownRefresh()
+				}
+			}
+		}
+	},
+	// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件
+	onPageScroll(e) {
+		this.handlePageScroll(e)
+	},
+	onReachBottom() {
+		this.handleReachBottom()
+	},
+	// 当down的native: true时, 还需传递此方法进到子组件
+	onPullDownRefresh(){
+		this.handlePullDownRefresh()
+	},
+	methods:{
+		handlePageScroll(e){
+			let mescroll = this.getMescroll(this.tabIndex);
+			mescroll && mescroll.onPageScroll(e);
+		},
+		handleReachBottom(){
+			let mescroll = this.getMescroll(this.tabIndex);
+			mescroll && mescroll.onReachBottom();
+		},
+		handlePullDownRefresh(){
+			let mescroll = this.getMescroll(this.tabIndex);
+			mescroll && mescroll.onPullDownRefresh();
+		},
+		// 根据下标获取对应子组件的mescroll
+		getMescroll(i){
+			if(!this.mescrollItems) this.mescrollItems = [];
+			if(!this.mescrollItems[i]) {
+				// v-for中的refs
+				let vForItem = this.$refs["mescrollItem"];
+				if(vForItem){
+					this.mescrollItems[i] = vForItem[i]
+				}else{
+					// 普通的refs,不可重复
+					this.mescrollItems[i] = this.$refs["mescrollItem"+i];
+				}
+			}
+			let item = this.mescrollItems[i]
+			return item ? item.mescroll : null
+		},
+		// 切换tab,恢复滚动条位置
+		tabChange(i){
+			let mescroll = this.getMescroll(i);
+			if(mescroll){
+				// 延时(比$nextTick靠谱一些),确保元素已渲染
+				setTimeout(()=>{
+					mescroll.scrollTo(mescroll.getScrollTop(),0)
+				},30)
+			}
+		}
+	}
+}
+
+export default MescrollMoreMixin;

+ 109 - 0
components/mescroll-uni/wxs/mixins.js

@@ -0,0 +1,109 @@
+// 定义在wxs (含renderjs) 逻辑层的数据和方法, 与视图层相互通信
+const WxsMixin = {
+	data() {
+		return {
+			// 传入wxs视图层的数据 (响应式)
+			wxsProp: {
+				optDown:{}, // 下拉刷新的配置
+				scrollTop:0, // 滚动条的距离
+				bodyHeight:0, // body的高度
+				isDownScrolling:false, // 是否正在下拉刷新中
+				isUpScrolling:false, // 是否正在上拉加载中
+				isScrollBody:true, // 是否为mescroll-body滚动
+				isUpBoth:true, // 上拉加载时,是否同时可以下拉刷新
+				t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
+			},
+			
+			// 标记调用wxs视图层的方法
+			callProp: {
+				callType: '', // 方法名
+				t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
+			},
+			
+			// 不用wxs的平台使用此处的wxsBiz对象,抹平wxs的写法 (微信小程序和APP使用的wxsBiz对象是./wxs/wxs.wxs)
+			// #ifndef MP-WEIXIN || MP-QQ || APP-PLUS || H5
+			wxsBiz: {
+				//注册列表touchstart事件,用于下拉刷新
+				touchstartEvent: e=> {
+					this.mescroll.touchstartEvent(e);
+				},
+				//注册列表touchmove事件,用于下拉刷新
+				touchmoveEvent: e=> {
+					this.mescroll.touchmoveEvent(e);
+				},
+				//注册列表touchend事件,用于下拉刷新
+				touchendEvent: e=> {
+					this.mescroll.touchendEvent(e);
+				},
+				propObserver(){}, // 抹平wxs的写法
+				callObserver(){} // 抹平wxs的写法
+			},
+			// #endif
+			
+			// 不用renderjs的平台使用此处的renderBiz对象,抹平renderjs的写法 (app 和 h5 使用的renderBiz对象是./wxs/renderjs.js)
+			// #ifndef APP-PLUS || H5
+			renderBiz: {
+				propObserver(){} // 抹平renderjs的写法
+			}
+			// #endif
+		}
+	},
+	methods: {
+		// wxs视图层调用逻辑层的回调
+		wxsCall(msg){
+			if(msg.type === 'setWxsProp'){
+				// 更新wxsProp数据 (值改变才触发更新)
+				this.wxsProp = {
+					optDown: this.mescroll.optDown,
+					scrollTop: this.mescroll.getScrollTop(),
+					bodyHeight: this.mescroll.getBodyHeight(),
+					isDownScrolling: this.mescroll.isDownScrolling,
+					isUpScrolling: this.mescroll.isUpScrolling,
+					isUpBoth: this.mescroll.optUp.isBoth,
+					isScrollBody:this.mescroll.isScrollBody,
+					t: Date.now()
+				}
+			}else if(msg.type === 'setLoadType'){
+				// 设置inOffset,outOffset的状态
+				this.downLoadType = msg.downLoadType
+				// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
+				this.$set(this.mescroll, 'downLoadType', this.downLoadType)
+				// 重置是否加载成功的状态
+				this.$set(this.mescroll, 'isDownEndSuccess', null)
+			}else if(msg.type === 'triggerDownScroll'){
+				// 主动触发下拉刷新
+				this.mescroll.triggerDownScroll();
+			}else if(msg.type === 'endDownScroll'){
+				// 结束下拉刷新
+				this.mescroll.endDownScroll();
+			}else if(msg.type === 'triggerUpScroll'){
+				// 主动触发上拉加载
+				this.mescroll.triggerUpScroll(true);
+			}
+		}
+	},
+	mounted() {
+		// #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5
+		// 配置主动触发wxs显示加载进度的回调
+		this.mescroll.optDown.afterLoading = ()=>{
+			this.callProp = {callType: "showLoading", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+		}
+		// 配置主动触发wxs隐藏加载进度的回调
+		this.mescroll.optDown.afterEndDownScroll = ()=>{
+			this.callProp = {callType: "endDownScroll", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+			let delay = 300 + (this.mescroll.optDown.beforeEndDelay || 0)
+			setTimeout(()=>{
+				if(this.downLoadType === 4 || this.downLoadType === 0){
+					this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+				}
+				// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
+				this.$set(this.mescroll, 'downLoadType', this.downLoadType)
+			}, delay)
+		}
+		// 初始化wxs的数据
+		this.wxsCall({type: 'setWxsProp'})
+		// #endif
+	}
+}
+
+export default WxsMixin;

+ 92 - 0
components/mescroll-uni/wxs/renderjs.js

@@ -0,0 +1,92 @@
+// 使用renderjs直接操作window对象,实现动态控制app和h5的bounce
+// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果 (下拉刷新时禁止)
+// https://uniapp.dcloud.io/frame?id=renderjs
+
+// 与wxs的me实例一致
+var me = {}
+
+// 初始化window对象的touch事件 (仅初始化一次)
+if(window && !window.$mescrollRenderInit){
+	window.$mescrollRenderInit = true
+	
+	
+	window.addEventListener('touchstart', function(e){
+		if (me.disabled()) return;
+		me.startPoint = me.getPoint(e); // 记录起点
+	}, {passive: true})
+	
+	
+	window.addEventListener('touchmove', function(e){
+		if (me.disabled()) return;
+		if (me.getScrollTop() > 0) return; // 需在顶部下拉,才禁止bounce
+		
+		var curPoint = me.getPoint(e); // 当前点
+		var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+		// 向下拉
+		if (moveY > 0) {
+			// 可下拉的条件
+			if (!me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && me.isUpBoth))) {
+				
+				// 只有touch在mescroll的view上面,才禁止bounce
+				var el = e.target;
+				var isMescrollTouch = false;
+				while (el && el.tagName && el.tagName !== 'UNI-PAGE-BODY' && el.tagName != "BODY") {
+					var cls = el.classList;
+					if (cls && cls.contains('mescroll-render-touch')) {
+						isMescrollTouch = true
+						break;
+					}
+					el = el.parentNode; // 继续检查其父元素
+				}
+				// 禁止bounce (不会对swiper和iOS侧滑返回造成影响)
+				if (isMescrollTouch && e.cancelable && !e.defaultPrevented) e.preventDefault();
+			}
+		}
+	}, {passive: false})
+}
+
+/* 获取滚动条的位置 */
+me.getScrollTop = function() {
+	return me.scrollTop || 0
+}
+
+/* 是否禁用下拉刷新 */
+me.disabled = function(){
+	return !me.optDown || !me.optDown.use || me.optDown.native
+}
+
+/* 根据点击滑动事件获取第一个手指的坐标 */
+me.getPoint = function(e) {
+	if (!e) {
+		return {x: 0,y: 0}
+	}
+	if (e.touches && e.touches[0]) {
+		return {x: e.touches[0].pageX,y: e.touches[0].pageY}
+	} else if (e.changedTouches && e.changedTouches[0]) {
+		return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
+	} else {
+		return {x: e.clientX,y: e.clientY}
+	}
+}
+
+/**
+ * 监听逻辑层数据的变化 (实时更新数据)
+ */
+function propObserver(wxsProp) {
+	me.optDown = wxsProp.optDown
+	me.scrollTop = wxsProp.scrollTop
+	me.isDownScrolling = wxsProp.isDownScrolling
+	me.isUpScrolling = wxsProp.isUpScrolling
+	me.isUpBoth = wxsProp.isUpBoth
+}
+
+/* 导出模块 */
+const renderBiz = {
+	data() {
+		return {
+			propObserver: propObserver,
+		}
+	}
+}
+
+export default renderBiz;

+ 268 - 0
components/mescroll-uni/wxs/wxs.wxs

@@ -0,0 +1,268 @@
+// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响
+// https://uniapp.dcloud.io/frame?id=wxs
+// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html 
+
+// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致
+var me = {}
+
+// ------ 自定义下拉刷新动画 start ------
+
+/* 下拉过程中的回调,滑动过程一直在执行 (rate<1为inOffset; rate>1为outOffset) */
+me.onMoving = function (ins, rate, downHight){
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			'will-change': 'transform', // 可解决下拉过程中, image和swiper脱离文档流的问题
+			'transform': 'translateY(' + downHight + 'px)',
+			'transition': ''
+		})
+		// 环形进度条
+		var progress = ins.selectComponent('.mescroll-wxs-progress')
+		progress && progress.setStyle({transform: 'rotate(' + 360 * rate + 'deg)'})
+	})
+}
+
+/* 显示下拉刷新进度 */
+me.showLoading = function (ins){
+	me.downHight = me.optDown.offset
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			'will-change': 'auto',
+			'transform': 'translateY(' + me.downHight + 'px)',
+			'transition': 'transform 300ms'
+		})
+	})
+}
+
+/* 结束下拉 */
+me.endDownScroll = function (ins){
+	me.downHight = 0;
+	me.isDownScrolling = false;
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			'will-change': 'auto',
+			'transform': 'translateY(0)', // 不可以写空串,否则scroll-view渲染不完整 (延时350ms会调clearTransform置空)
+			'transition': 'transform 300ms'
+		})
+	})
+}
+
+/* 结束下拉动画执行完毕后, 清除transform和transition, 避免对列表内容样式造成影响, 如: h5的list-msg示例下拉进度条漏出来等 */
+me.clearTransform = function (ins){
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			'will-change': '',
+			'transform': '',
+			'transition': ''
+		})
+	})
+}
+
+// ------ 自定义下拉刷新动画 end ------
+
+/**
+ * 监听逻辑层数据的变化 (实时更新数据)
+ */
+function propObserver(wxsProp) {
+	me.optDown = wxsProp.optDown
+	me.scrollTop = wxsProp.scrollTop
+	me.bodyHeight = wxsProp.bodyHeight
+	me.isDownScrolling = wxsProp.isDownScrolling
+	me.isUpScrolling = wxsProp.isUpScrolling
+	me.isUpBoth = wxsProp.isUpBoth
+	me.isScrollBody = wxsProp.isScrollBody
+	me.startTop = wxsProp.scrollTop // 及时更新touchstart触发的startTop, 避免scroll-view快速惯性滚动到顶部取值不准确
+}
+
+/**
+ * 监听逻辑层数据的变化 (调用wxs的方法)
+ */
+function callObserver(callProp, oldValue, ins) {
+	if (me.disabled()) return;
+	if(callProp.callType){
+		// 逻辑层(App Service)的style已失效,需在视图层(Webview)设置style
+		if(callProp.callType === 'showLoading'){
+			me.showLoading(ins)
+		}else if(callProp.callType === 'endDownScroll'){
+			me.endDownScroll(ins)
+		}else if(callProp.callType === 'clearTransform'){
+			me.clearTransform(ins)
+		}
+	}
+}
+
+/**
+ * touch事件
+ */
+function touchstartEvent(e, ins) {
+	me.downHight = 0; // 下拉的距离
+	me.startPoint = me.getPoint(e); // 记录起点
+	me.startTop = me.getScrollTop(); // 记录此时的滚动条位置
+	me.startAngle = 0; // 初始角度
+	me.lastPoint = me.startPoint; // 重置上次move的点
+	me.maxTouchmoveY = me.getBodyHeight() - me.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
+	me.inTouchend = false; // 标记不是touchend
+	
+	me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
+}
+
+function touchmoveEvent(e, ins) {
+	var isPrevent = true // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
+	
+	if (me.disabled()) return isPrevent;
+	
+	var scrollTop = me.getScrollTop(); // 当前滚动条的距离
+	var curPoint = me.getPoint(e); // 当前点
+	
+	var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+	
+	// 向下拉 && 在顶部
+	// mescroll-body,直接判定在顶部即可
+	// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
+	// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
+	if (moveY > 0 && (
+			(me.isScrollBody && scrollTop <= 0)
+			||
+			(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
+		)) {
+		// 可下拉的条件
+		if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
+				me.isUpBoth))) {
+	
+			// 下拉的角度是否在配置的范围内
+			if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
+			if (me.startAngle < me.optDown.minAngle) return isPrevent; // 如果小于配置的角度,则不往下执行下拉刷新
+	
+			// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
+			if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
+				me.inTouchend = true; // 标记执行touchend
+				touchendEvent(e, ins); // 提前触发touchend
+				return isPrevent;
+			}
+			
+			isPrevent = false // 小程序是return false
+	
+			var diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
+	
+			// 下拉距离  < 指定距离
+			if (me.downHight < me.optDown.offset) {
+				if (me.movetype !== 1) {
+					me.movetype = 1; // 加入标记,保证只执行一次
+					// me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
+					me.callMethod(ins, {type: 'setLoadType', downLoadType: 1})
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+				}
+				me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
+	
+				// 指定距离  <= 下拉距离
+			} else {
+				if (me.movetype !== 2) {
+					me.movetype = 2; // 加入标记,保证只执行一次
+					// me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
+					me.callMethod(ins, {type: 'setLoadType', downLoadType: 2})
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+				}
+				if (diff > 0) { // 向下拉
+					me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
+				} else { // 向上收
+					me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
+				}
+			}
+			
+			me.downHight = Math.round(me.downHight) // 取整
+			var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
+			// me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
+			me.onMoving(ins, rate, me.downHight)
+		}
+	}
+	
+	me.lastPoint = curPoint; // 记录本次移动的点
+	
+	return isPrevent // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
+}
+
+function touchendEvent(e, ins) {
+	// 如果下拉区域高度已改变,则需重置回来
+	if (me.isMoveDown) {
+		if (me.downHight >= me.optDown.offset) {
+			// 符合触发刷新的条件
+			me.downHight = me.optDown.offset; // 更新下拉区域高度
+			// me.triggerDownScroll();
+			me.callMethod(ins, {type: 'triggerDownScroll'})
+		} else {
+			// 不符合的话 则重置
+			me.downHight = 0;
+			// me.optDown.endDownScroll && me.optDown.endDownScroll(me);
+			me.callMethod(ins, {type: 'endDownScroll'})
+		}
+		me.movetype = 0;
+		me.isMoveDown = false;
+	} else if (!me.isScrollBody && me.getScrollTop() === me.startTop) { // scroll-view到顶/左/右/底的滑动事件
+		var isScrollUp = me.getPoint(e).y - me.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+		// 上滑
+		if (isScrollUp) {
+			// 需检查滑动的角度
+			var angle = me.getAngle(me.getPoint(e), me.startPoint); // 两点之间的角度,区间 [0,90]
+			if (angle > 80) {
+				// 检查并触发上拉
+				// me.triggerUpScroll(true);
+				me.callMethod(ins, {type: 'triggerUpScroll'})
+			}
+		}
+	}
+	me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
+}
+
+/* 是否禁用下拉刷新 */
+me.disabled = function(){
+	return !me.optDown || !me.optDown.use || me.optDown.native
+}
+
+/* 根据点击滑动事件获取第一个手指的坐标 */
+me.getPoint = function(e) {
+	if (!e) {
+		return {x: 0,y: 0}
+	}
+	if (e.touches && e.touches[0]) {
+		return {x: e.touches[0].pageX,y: e.touches[0].pageY}
+	} else if (e.changedTouches && e.changedTouches[0]) {
+		return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
+	} else {
+		return {x: e.clientX,y: e.clientY}
+	}
+}
+
+/* 计算两点之间的角度: 区间 [0,90]*/
+me.getAngle = function (p1, p2) {
+	var x = Math.abs(p1.x - p2.x);
+	var y = Math.abs(p1.y - p2.y);
+	var z = Math.sqrt(x * x + y * y);
+	var angle = 0;
+	if (z !== 0) {
+		angle = Math.asin(y / z) / Math.PI * 180;
+	}
+	return angle
+}
+
+/* 获取滚动条的位置 */
+me.getScrollTop = function() {
+	return me.scrollTop || 0
+}
+
+/* 获取body的高度 */
+me.getBodyHeight = function() {
+	return me.bodyHeight || 0;
+}
+
+/* 调用逻辑层的方法 */
+me.callMethod = function(ins, param) {
+	if(ins) ins.callMethod('wxsCall', param)
+}
+
+/* 导出模块 */
+module.exports = {
+	propObserver: propObserver,
+	callObserver: callObserver,
+	touchstartEvent: touchstartEvent,
+	touchmoveEvent: touchmoveEvent,
+	touchendEvent: touchendEvent
+}

+ 123 - 0
components/page/article/index.vue

@@ -0,0 +1,123 @@
+<template>
+  <!-- 文章组 -->
+  <view class="diy-article">
+    <view class="article-item" :class="[`show-type__${item.show_type}`]" v-for="(item, index) in dataList" :key="index"
+      @click="onTargetDetail(item.article_id)">
+      <!-- 小图模式 -->
+      <block v-if="item.show_type == 10">
+        <view class="article-item__left flex-box">
+          <view class="article-item__title">
+            <text class="twoline-hide">{{ item.title }}</text>
+          </view>
+          <view class="article-item__footer m-top10">
+            <text class="article-views f-24 col-8">{{ item.show_views }}次浏览</text>
+          </view>
+        </view>
+        <view class="article-item__image">
+          <image class="image" mode="widthFix" :src="item.image_url"></image>
+        </view>
+      </block>
+      <!-- 大图模式 -->
+      <block v-if="item.show_type == 20">
+        <view class="article-item__title">
+          <text class="twoline-hide">{{ item.title }}</text>
+        </view>
+        <view class="article-item__image m-top20">
+          <image class="image" mode="widthFix" :src="item.image_url"></image>
+        </view>
+        <view class="article-item__footer m-top10">
+          <text class="article-views f-24 col-8">{{ item.show_views }}次浏览</text>
+        </view>
+      </block>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: "Article",
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemIndex: String,
+      params: Object,
+      dataList: Array
+    },
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+      /**
+       * 跳转文章详情页
+       */
+      onTargetDetail(id) {
+        uni.navigateTo({
+          url: '/pages/article/detail?articleId=' + id
+        })
+      }
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-article {
+    background: #f7f7f7;
+
+    .article-item {
+      margin-bottom: 20rpx;
+      padding: 30rpx;
+      background: #fff;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+
+      .article-item__title {
+        max-height: 74rpx;
+        font-size: 28rpx;
+        line-height: 38rpx;
+        color: #333;
+      }
+
+      .article-item__image .image {
+        display: block;
+      }
+
+    }
+
+
+
+  }
+
+  /* 小图模式 */
+
+  .show-type__10 {
+    display: flex;
+
+    .article-item__left {
+      padding-right: 20rpx;
+    }
+
+    .article-item__title {
+      // min-height: 72rpx;
+    }
+
+    .article-item__image .image {
+      width: 240rpx;
+    }
+
+  }
+
+  /* 大图模式 */
+
+  .show-type__20 .article-item__image .image {
+    width: 100%;
+  }
+</style>

+ 154 - 0
components/page/banner/index.vue

@@ -0,0 +1,154 @@
+<template>
+  <view class="diy-banner" :style="{ height: `${imgHeights[imgCurrent]}px` }">
+    <!-- 图片轮播 -->
+    <swiper class="swiper-box" :autoplay="autoplay" :duration="duration" :circular="true" :interval="itemStyle.interval * 1000" @change="_bindChange">
+      <swiper-item v-for="(dataItem, index) in dataList" :key="index">
+        <image mode="widthFix" class="slide-image" :src="dataItem.imgUrl" @click="onLink(dataItem.link)" @load="_imagesHeight" />
+      </swiper-item>
+    </swiper>
+    <!-- 指示点 -->
+    <view class="indicator-dots" :class="itemStyle.btnShape">
+      <view class="dots-item" :class="{ active: imgCurrent == index }" :style="{ backgroundColor: itemStyle.btnColor }"
+        v-for="(dataItem, index) in dataList" :key="index"></view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import mixin from '../mixin';
+
+  export default {
+    name: 'Banner',
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemIndex: String,
+      itemStyle: Object,
+      params: Object,
+      dataList: Array
+    },
+
+    mixins: [mixin],
+
+    /**
+     * 私有数据,组件的初始数据
+     * 可用于模版渲染
+     */
+    data() {
+      return {
+        windowWidth: 750,
+        indicatorDots: false, // 是否显示面板指示点
+        autoplay: true, // 是否自动切换
+        duration: 800, // 滑动动画时长
+        imgHeights: [], // 图片的高度
+        imgCurrent: 0 // 当前banne所在滑块指针
+      };
+    },
+
+    created() {
+      const app = this;
+      uni.getSystemInfo({
+        success({ windowWidth }) {
+          app.windowWidth = windowWidth > 750 ? 750 : windowWidth;
+        }
+      });
+    },
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+      /**
+       * 计算图片高度
+       */
+      _imagesHeight({ detail }) {
+        const app = this;
+        // 获取图片真实宽度
+        const { width, height } = detail;
+        // 宽高比
+        const ratio = width / height;
+        // 计算的高度值
+        const viewHeight = app.windowWidth / ratio;
+        // 把每一张图片的高度记录到数组里
+        app.imgHeights.push(viewHeight);
+      },
+
+      /**
+       * 记录当前指针
+       */
+      _bindChange(e) {
+        this.imgCurrent = e.detail.current;
+      }
+    }
+  };
+</script>
+
+<style lang="scss" scoped>
+  .diy-banner {
+    position: relative;
+
+    // swiper组件
+    .swiper-box {
+      height: 100%;
+
+      .slide-image {
+        width: 100%;
+        height: 100%;
+        margin: 0 auto;
+        display: block;
+      }
+    }
+
+    /* 指示点 */
+    .indicator-dots {
+      width: 100%;
+      height: 28rpx;
+      padding: 0 20rpx;
+      position: absolute;
+      left: 0;
+      right: 0;
+      bottom: 20rpx;
+      opacity: 0.8;
+      display: flex;
+      justify-content: center;
+
+      .dots-item {
+        width: 16rpx;
+        height: 16rpx;
+        margin-right: 8rpx;
+        background-color: #fff;
+
+        &:last-child {
+          margin-right: 0;
+        }
+
+        &.active {
+          background-color: #313131 !important;
+        }
+      }
+
+      // 圆形
+      &.round .dots-item {
+        width: 16rpx;
+        height: 16rpx;
+        border-radius: 20rpx;
+      }
+
+      // 正方形
+      &.square .dots-item {
+        width: 16rpx;
+        height: 16rpx;
+      }
+
+      // 长方形
+      &.rectangle .dots-item {
+        width: 22rpx;
+        height: 14rpx;
+      }
+    }
+  }
+</style>

+ 31 - 0
components/page/blank/index.vue

@@ -0,0 +1,31 @@
+<template>
+  <!-- 辅助空白 -->
+  <view class="diy-blank" :style="{ height: `${itemStyle.height}px`, background: itemStyle.background }">
+  </view>
+</template>
+
+<script>
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemStyle: Object
+    },
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 256 - 0
components/page/goods/index.vue

@@ -0,0 +1,256 @@
+<template>
+  <!-- 商品组 -->
+  <view class="diy-goods" :style="{ background: itemStyle.background }">
+    <view class="goods-list" :class="[`display__${itemStyle.display}`, `column__${itemStyle.column}`]">
+      <scroll-view :scroll-x="itemStyle.display === 'slide'">
+        <view class="goods-item" v-for="(dataItem, index) in dataList" :key="index" @click="onTargetGoods(dataItem.goods_id)">
+
+          <!-- 单列商品 -->
+          <block v-if="itemStyle.column === 1">
+            <view class="dis-flex">
+              <!-- 商品图片 -->
+              <view class="goods-item_left">
+                <image class="image" :src="dataItem.goods_image"></image>
+              </view>
+              <view class="goods-item_right">
+                <!-- 商品名称 -->
+                <view v-if="itemStyle.show.includes('goodsName')" class="goods-name">
+                  <text class="twoline-hide">{{ dataItem.goods_name }}</text>
+                </view>
+                <view class="goods-item_desc">
+                  <!-- 商品卖点 -->
+                  <view v-if="itemStyle.show.includes('sellingPoint')" class="desc-selling_point dis-flex">
+                    <text class="oneline-hide">{{ dataItem.selling_point }}</text>
+                  </view>
+                  <!-- 商品销量 -->
+                  <view v-if="itemStyle.show.includes('goodsSales')" class="desc-goods_sales dis-flex">
+                    <text>已售{{ dataItem.goods_sales }}件</text>
+                  </view>
+                  <!-- 商品价格 -->
+                  <view class="desc_footer">
+                    <text v-if="itemStyle.show.includes('goodsPrice')" class="price_x">¥{{ dataItem.goods_price_min }}</text>
+                    <text class="price_y col-9"
+                      v-if="itemStyle.show.includes('linePrice') && dataItem.line_price_min > 0">¥{{ dataItem.line_price_min }}</text>
+                  </view>
+                </view>
+              </view>
+            </view>
+          </block>
+          <!-- 多列商品 -->
+          <block v-else>
+            <!-- 商品图片 -->
+            <view class="goods-image">
+              <image class="image" mode="aspectFill" :src="dataItem.goods_image"></image>
+            </view>
+            <view class="detail">
+              <!-- 商品标题 -->
+              <view v-if="itemStyle.show.includes('goodsName')" class="goods-name">
+                <text class="twoline-hide">{{ dataItem.goods_name }}</text>
+              </view>
+              <!-- 商品价格 -->
+              <view class="detail-price oneline-hide">
+                <text v-if="itemStyle.show.includes('goodsPrice')" class="goods-price f-30 col-m">¥{{ dataItem.goods_price_min }}</text>
+                <text v-if="itemStyle.show.includes('linePrice') && dataItem.line_price_min > 0"
+                  class="line-price col-9 f-24">¥{{ dataItem.line_price_min }}</text>
+              </view>
+            </view>
+          </block>
+
+        </view>
+      </scroll-view>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: "Goods",
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemIndex: String,
+      itemStyle: Object,
+      params: Object,
+      dataList: Array
+    },
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+      /**
+       * 跳转商品详情页
+       */
+      onTargetGoods(goodsId) {
+        this.$navTo(`pages/goods/detail`, { goodsId })
+      }
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-goods {
+    .goods-list {
+      padding: 4rpx;
+      box-sizing: border-box;
+
+      .goods-item {
+        box-sizing: border-box;
+        padding: 6rpx;
+
+        .goods-image {
+          position: relative;
+          width: 100%;
+          height: 0;
+          padding-bottom: 100%;
+          overflow: hidden;
+          background: #fff;
+
+          &:after {
+            content: '';
+            display: block;
+            margin-top: 100%;
+          }
+
+          .image {
+            position: absolute;
+            width: 100%;
+            height: 100%;
+            top: 0;
+            left: 0;
+            -o-object-fit: cover;
+            object-fit: cover;
+          }
+        }
+
+        .detail {
+          padding: 8rpx;
+          background: #fff;
+
+          .goods-name {
+            min-height: 68rpx;
+            line-height: 1.3;
+            white-space: normal;
+            color: #484848;
+            font-size: 26rpx;
+          }
+
+          .detail-price {
+            .goods-price {
+              margin-right: 8rpx;
+            }
+
+            .line-price {
+              text-decoration: line-through;
+            }
+          }
+        }
+      }
+
+      &.display__slide {
+        white-space: nowrap;
+        font-size: 0;
+
+        .goods-item {
+          display: inline-block;
+        }
+      }
+
+      &.display__list {
+        .goods-item {
+          float: left;
+        }
+      }
+
+      &.column__2 {
+        .goods-item {
+          width: 50%;
+        }
+      }
+
+      &.column__3 {
+        .goods-item {
+          width: 33.33333%;
+        }
+      }
+
+      &.column__1 {
+        .goods-item {
+          width: 100%;
+          height: 280rpx;
+          margin-bottom: 12rpx;
+          padding: 20rpx;
+          box-sizing: border-box;
+          background: #fff;
+          line-height: 1.6;
+
+          &:last-child {
+            margin-bottom: 0;
+          }
+        }
+
+        .goods-item_left {
+          display: flex;
+          width: 40%;
+          background: #fff;
+          align-items: center;
+
+          .image {
+            display: block;
+            width: 240rpx;
+            height: 240rpx;
+          }
+        }
+
+        .goods-item_right {
+          position: relative;
+          width: 60%;
+
+          .goods-name {
+            margin-top: 20rpx;
+            min-height: 68rpx;
+            line-height: 1.3;
+            white-space: normal;
+            color: #484848;
+            font-size: 26rpx;
+          }
+        }
+
+        .goods-item_desc {
+          margin-top: 8rpx;
+        }
+
+        .desc-selling_point {
+          width: 400rpx;
+          font-size: 24rpx;
+          color: #e49a3d;
+        }
+
+        .desc-goods_sales {
+          color: #999;
+          font-size: 24rpx;
+        }
+
+        .desc_footer {
+          font-size: 24rpx;
+
+          .price_x {
+            margin-right: 16rpx;
+            color: #f03c3c;
+            font-size: 30rpx;
+          }
+
+          .price_y {
+            text-decoration: line-through;
+          }
+        }
+      }
+    }
+  }
+</style>

+ 36 - 0
components/page/guide/index.vue

@@ -0,0 +1,36 @@
+<template>
+  <!-- 辅助线 -->
+  <view class="diy-guide" :style="{ padding: `${itemStyle.paddingTop}px 0`, background: itemStyle.background }">
+    <view class="line" :style="{ borderTop: `${itemStyle.lineHeight}px ${itemStyle.lineStyle} ${itemStyle.lineColor}` }">
+    </view>
+  </view>
+</template>
+
+<script>
+
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemStyle: Object
+    },
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-guide .line {
+    width: 100%;
+  }
+</style>

+ 47 - 0
components/page/image/index.vue

@@ -0,0 +1,47 @@
+<template>
+  <!-- 单图组 -->
+  <view class="diy-imageSingle" :style="{ paddingBottom: `${itemStyle.paddingTop}px`, background: itemStyle.background }">
+    <view class="item-image" v-for="(dataItem, index) in dataList" :key="index" :style="{ padding: `${itemStyle.paddingTop}px ${itemStyle.paddingLeft}px 0` }">
+      <view class="nav-to" @click="onLink(dataItem.link)">
+        <image class="image" :src="dataItem.imgUrl" mode="widthFix"></image>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import mixin from '../mixin'
+
+  export default {
+    name: "Images",
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemIndex: String,
+      itemStyle: Object,
+      params: Object,
+      dataList: Array
+    },
+
+    mixins: [mixin],
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-imageSingle .item-image .image {
+    display: block;
+    width: 100%;
+  }
+</style>

+ 108 - 0
components/page/index.vue

@@ -0,0 +1,108 @@
+<template>
+  <view class="page-items">
+    <block v-for="(item, index) in items" :key="index">
+      <!-- 搜索框 -->
+      <block v-if="item.type === 'search'">
+        <Search :itemStyle="item.style" :params="item.params" />
+      </block>
+      <!-- 图片组 -->
+      <block v-if="item.type === 'image'">
+        <Images :itemStyle="item.style" :params="item.params" :dataList="item.data" />
+      </block>
+      <!-- 轮播图 -->
+      <block v-if="item.type === 'banner'">
+        <Banner :itemStyle="item.style" :params="item.params" :dataList="item.data" />
+      </block>
+      <!-- 图片橱窗 -->
+      <block v-if="item.type === 'window'">
+        <Window :itemStyle="item.style" :params="item.params" :dataList="item.data" />
+      </block>
+      <!-- 视频 -->
+      <block v-if="item.type === 'video'">
+        <Videos :itemStyle="item.style" :params="item.params" />
+      </block>
+      <!-- 文章组 -->
+      <block v-if="item.type === 'article'">
+        <Article :params="item.params" :dataList="item.data" />
+      </block>
+      <!-- 店铺公告 -->
+      <block v-if="item.type === 'notice'">
+        <Notice :itemStyle="item.style" :params="item.params" />
+      </block>
+      <!-- 导航 -->
+      <block v-if="item.type === 'navBar'">
+        <NavBar :itemStyle="item.style" :params="item.params" :dataList="item.data" />
+      </block>
+      <!-- 商品 -->
+      <block v-if="item.type === 'goods'">
+        <Goods :itemStyle="item.style" :params="item.params" :dataList="item.data" />
+      </block>
+      <!-- 在线客服 -->
+      <block v-if="item.type === 'service'">
+        <Service :itemStyle="item.style" :params="item.params" />
+      </block>
+      <!-- 辅助空白 -->
+      <block v-if="item.type === 'blank'">
+        <Blank :itemStyle="item.style" />
+      </block>
+      <!-- 辅助线 -->
+      <block v-if="item.type === 'guide'">
+        <Guide :itemStyle="item.style" />
+      </block>
+      <!-- 富文本 -->
+      <block v-if="item.type === 'richText'">
+        <RichText :itemStyle="item.style" :params="item.params" />
+      </block>
+    </block>
+  </view>
+</template>
+
+<script>
+  import Search from './search'
+  import Images from './image'
+  import Banner from './banner'
+  import Window from './window'
+  import Videos from './video'
+  import Article from './article'
+  import Notice from './notice'
+  import NavBar from './navBar'
+  import Goods from './goods'
+  import Service from './service'
+  import Blank from './blank'
+  import Guide from './guide'
+  import RichText from './richText'
+
+  export default {
+    name: "Page",
+    components: {
+      Search,
+      Images,
+      Banner,
+      Window,
+      Videos,
+      Article,
+      Notice,
+      NavBar,
+      Goods,
+      Service,
+      Blank,
+      Guide,
+      RichText
+    },
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      items: {
+        type: Array,
+        default () {
+          return []
+        }
+      }
+    },
+  }
+</script>
+<style lang="scss">
+  // 组件样式
+</style>

+ 23 - 0
components/page/mixin.js

@@ -0,0 +1,23 @@
+import util from '@/utils/util'
+
+export default {
+  data() {
+    return {}
+  },
+  methods: {
+
+    /**
+     * link对象点击事件
+     * 支持tabBar页面
+     */
+    onLink(linkObj) {
+      if (!linkObj) return false
+      // 跳转到指定页面
+      if (linkObj.type === 'PAGE') {
+        this.$navTo(linkObj.param.path, linkObj.param.query)
+      }
+      return true
+    }
+  },
+
+}

+ 87 - 0
components/page/navBar/index.vue

@@ -0,0 +1,87 @@
+<template>
+  <!-- 导航组 -->
+  <view class="diy-navBar" :style="{ background: itemStyle.background, color: itemStyle.textColor }">
+    <view class="data-list" :class="[`avg-sm-${itemStyle.rowsNum}`]">
+      <view class="item-nav" v-for="(dataItem, index) in dataList" :key="index">
+        <view class="nav-to" @click="onLink(dataItem.link)">
+          <view class="item-image">
+            <image class="image" mode="widthFix" :src="dataItem.imgUrl"></image>
+          </view>
+          <view class="item-text oneline-hide">{{ dataItem.text }}</view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import mixin from '../mixin'
+
+  export default {
+    name: "NavBar",
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemIndex: String,
+      itemStyle: Object,
+      params: Object,
+      dataList: Array
+    },
+
+    mixins: [mixin],
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-navBar .data-list::after {
+    clear: both;
+    content: " ";
+    display: table;
+  }
+
+  .item-nav {
+    float: left;
+    margin: 10px 0;
+    text-align: center;
+
+    .item-text {
+      font-size: 26rpx;
+    }
+
+    .item-image {
+      margin-bottom: 4px;
+      font-size: 0;
+    }
+
+    .item-image .image {
+      width: 88rpx;
+      height: 88rpx;
+    }
+
+  }
+
+  /* 分列布局 */
+
+  .diy-navBar .avg-sm-3>.item-nav {
+    width: 33.33333333%;
+  }
+
+  .diy-navBar .avg-sm-4>.item-nav {
+    width: 25%;
+  }
+
+  .diy-navBar .avg-sm-5>.item-nav {
+    width: 20%;
+  }
+</style>

+ 40 - 0
components/page/notice/index.vue

@@ -0,0 +1,40 @@
+<template>
+  <!-- 店铺公告 -->
+  <view class="diy-notice" :style="{ paddingTop: `${itemStyle.paddingTop}px`, paddingBottom: `${itemStyle.paddingTop}px` }"
+    @click="onLink(params.link)">
+    <u-notice-bar padding="10rpx 24rpx" :volume-icon="params.showIcon" :autoplay="params.scrollable"
+      :bg-color="itemStyle.background" :color="itemStyle.textColor" :list="[params.text]"></u-notice-bar>
+  </view>
+</template>
+
+<script>
+  import mixin from '../mixin'
+
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemStyle: Object,
+      params: Object
+    },
+
+    mixins: [mixin],
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 33 - 0
components/page/richText/index.vue

@@ -0,0 +1,33 @@
+<template>
+  <!-- 富文本 -->
+  <view class="diy-richText"
+    :style="{ padding: `${itemStyle.paddingTop}px ${itemStyle.paddingLeft}px`, background: itemStyle.background }">
+    <mp-html :content="params.content" />
+  </view>
+</template>
+
+<script>
+  export default {
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemStyle: Object,
+      params: Object
+    },
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-richText {
+    font-size: 28rpx;
+  }
+</style>

+ 0 - 0
components/page/search/index.vue


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio