szr190 2 月之前
父節點
當前提交
8699167f16
共有 100 個文件被更改,包括 30460 次插入0 次删除
  1. 13 0
      .editorconfig
  2. 17 0
      .env.production
  3. 15 0
      .env.sandbox
  4. 53 0
      .eslintrc.cjs
  5. 23 0
      .gitignore
  6. 7 0
      .prettierignore
  7. 8 0
      .prettierrc.json
  8. 29 0
      env.d.ts
  9. 20 0
      index.html
  10. 10054 0
      package-lock.json
  11. 113 0
      package.json
  12. 7904 0
      pnpm-lock.yaml
  13. 67 0
      src/App.vue
  14. 1 0
      src/api/home.ts
  15. 147 0
      src/api/login.ts
  16. 226 0
      src/api/system.ts
  17. 63 0
      src/base/components/Captcha.vue
  18. 238 0
      src/base/components/Editors/editor-icon.css
  19. 二進制
      src/base/components/Editors/iconfont.ttf
  20. 286 0
      src/base/components/Editors/index.vue
  21. 181 0
      src/base/forget.vue
  22. 791 0
      src/base/login.vue
  23. 177 0
      src/base/register.vue
  24. 248 0
      src/components/HyTabs.vue
  25. 463 0
      src/composables/index.ts
  26. 40 0
      src/directive/authDirective.ts
  27. 7 0
      src/directive/index.ts
  28. 22 0
      src/main.js
  29. 82 0
      src/manifest.json
  30. 1026 0
      src/menu/index.js
  31. 95 0
      src/pages.json
  32. 41 0
      src/pages/components/NotLogin.vue
  33. 8 0
      src/pages/index.vue
  34. 8 0
      src/pages/my.vue
  35. 7 0
      src/pages/service.vue
  36. 29 0
      src/project.config.json
  37. 7 0
      src/project.private.config.json
  38. 6 0
      src/shime-uni.d.ts
  39. 238 0
      src/static/editor/editor-icon.css
  40. 二進制
      src/static/editor/iconfont.ttf
  41. 228 0
      src/static/emoji.js
  42. 二進制
      src/static/iconfont/iconfont.ttf
  43. 二進制
      src/static/images/home.png
  44. 二進制
      src/static/images/home_active.png
  45. 二進制
      src/static/images/mescroll-empty.png
  46. 二進制
      src/static/images/mescroll-totop.png
  47. 二進制
      src/static/images/my.png
  48. 二進制
      src/static/images/my_active.png
  49. 二進制
      src/static/images/service.png
  50. 二進制
      src/static/images/service_active.png
  51. 19 0
      src/stores/index.ts
  52. 50 0
      src/stores/modules/system.ts
  53. 63 0
      src/stores/modules/user.ts
  54. 14 0
      src/stores/reset.ts
  55. 592 0
      src/styles/emoji.scss
  56. 1337 0
      src/styles/index.scss
  57. 0 0
      src/types/components.d.ts
  58. 83 0
      src/types/global.d.ts
  59. 0 0
      src/types/home.d.ts
  60. 114 0
      src/types/login.ts
  61. 21 0
      src/types/system.d.ts
  62. 21 0
      src/types/user.d.ts
  63. 23 0
      src/types/workflow.d.ts
  64. 77 0
      src/uni.scss
  65. 8 0
      src/uni_modules/mescroll-uni/changelog.md
  66. 19 0
      src/uni_modules/mescroll-uni/components/mescroll-body/mescroll-body.css
  67. 400 0
      src/uni_modules/mescroll-uni/components/mescroll-body/mescroll-body.vue
  68. 47 0
      src/uni_modules/mescroll-uni/components/mescroll-diy/beibei/components/mescroll-down.css
  69. 39 0
      src/uni_modules/mescroll-uni/components/mescroll-diy/beibei/components/mescroll-down.vue
  70. 360 0
      src/uni_modules/mescroll-uni/components/mescroll-diy/beibei/mescroll-body.vue
  71. 49 0
      src/uni_modules/mescroll-uni/components/mescroll-diy/beibei/mescroll-uni-option.js
  72. 434 0
      src/uni_modules/mescroll-uni/components/mescroll-diy/beibei/mescroll-uni.vue
  73. 44 0
      src/uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-down.css
  74. 53 0
      src/uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-down.vue
  75. 32 0
      src/uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-up.css
  76. 40 0
      src/uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-up.vue
  77. 380 0
      src/uni_modules/mescroll-uni/components/mescroll-diy/xinlang/mescroll-body.vue
  78. 71 0
      src/uni_modules/mescroll-uni/components/mescroll-diy/xinlang/mescroll-uni-option.js
  79. 459 0
      src/uni_modules/mescroll-uni/components/mescroll-diy/xinlang/mescroll-uni.vue
  80. 116 0
      src/uni_modules/mescroll-uni/components/mescroll-empty/mescroll-empty.vue
  81. 55 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-down.css
  82. 47 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-down.vue
  83. 99 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-top.vue
  84. 47 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-up.css
  85. 39 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-up.vue
  86. 15 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-i18n.js
  87. 46 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js
  88. 71 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni-option.js
  89. 36 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni.css
  90. 799 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni.js
  91. 480 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni.vue
  92. 47 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-comp.js
  93. 57 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-more-item.js
  94. 77 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-more.js
  95. 109 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/wxs/mixins.js
  96. 92 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/wxs/renderjs.js
  97. 269 0
      src/uni_modules/mescroll-uni/components/mescroll-uni/wxs/wxs.wxs
  98. 66 0
      src/uni_modules/mescroll-uni/hooks/useMescroll.js
  99. 56 0
      src/uni_modules/mescroll-uni/hooks/useMescrollComp.js
  100. 0 0
      src/uni_modules/mescroll-uni/hooks/useMescrollMore.js

+ 13 - 0
.editorconfig

@@ -0,0 +1,13 @@
1
+# editorconfig.org
2
+root = true
3
+
4
+[*]
5
+indent_style = space
6
+indent_size = 2
7
+end_of_line = crlf
8
+charset = utf-8
9
+trim_trailing_whitespace = true
10
+insert_final_newline = true
11
+
12
+[*.md]
13
+trim_trailing_whitespace = false

+ 17 - 0
.env.production

@@ -0,0 +1,17 @@
1
+###
2
+ # @Author: wyd
3
+ # @Date: 2024-02
4
+ # @LastEditors: JiangChunMei
5
+ # @LastEditTime: 2024-11-19 09:47:55
6
+ # @Description: 生产环境
7
+### 
8
+# 接口地址
9
+VITE_BASE_URL="https://new.hqhuitong.com/api"
10
+# 文件地址
11
+VITE_OSS_URL='https://new.hqhuitong.com'
12
+# 文件桶
13
+VITE_BARREL_URL="/smartpark/"
14
+# websocket地址
15
+VITE_SOCKET_URL='ws://new.hqhuitong.com/websocket?Authorization=Bearer%20'
16
+# 前端地址
17
+VITE_FRONT_URL='https://new.hqhuitong.com'

+ 15 - 0
.env.sandbox

@@ -0,0 +1,15 @@
1
+###
2
+ # @page: 测试环境
3
+ # @Author: wyd
4
+ # @LastEditors: JiangChunMei
5
+### 
6
+# 接口地址
7
+VITE_BASE_URL="http://sandbox.hqhuitong.com/api"
8
+# 文件地址
9
+VITE_OSS_URL='http://sandbox.hqhuitong.com'
10
+# 文件桶
11
+VITE_BARREL_URL="/smartparkdev/"
12
+# websocket地址
13
+VITE_SOCKET_URL='ws://sandbox.hqhuitong.com/websocket?Authorization=Bearer%20'
14
+# 前端地址
15
+VITE_FRONT_URL='http://sandbox.hqhuitong.com'

+ 53 - 0
.eslintrc.cjs

@@ -0,0 +1,53 @@
1
+/*
2
+ * @Author: wyd
3
+ * @Date: 2024-02
4
+ * @LastEditors: wyd
5
+ * @LastEditTime: 2024-03
6
+ * @Description:
7
+ */
8
+/* eslint-env node */
9
+require('@rushstack/eslint-patch/modern-module-resolution')
10
+
11
+module.exports = {
12
+  root: true,
13
+  extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-typescript', '@vue/eslint-config-prettier'],
14
+  // 小程序全局变量
15
+  globals: {
16
+    uni: true,
17
+    wx: true,
18
+    WechatMiniprogram: true,
19
+    getCurrentPages: true,
20
+    UniApp: true,
21
+    UniHelper: true,
22
+    Page: true,
23
+    AnyObject: true
24
+  },
25
+  parserOptions: {
26
+    ecmaVersion: 'latest'
27
+  },
28
+  rules: {
29
+    'prettier/prettier': [
30
+      'warn',
31
+      {
32
+        singleQuote: true,
33
+        semi: false,
34
+        printWidth: 150,
35
+        trailingComma: 'none',
36
+        endOfLine: 'auto'
37
+      }
38
+    ],
39
+    'vue/multi-word-component-names': ['off'],
40
+    'vue/no-setup-props-destructure': ['off'],
41
+    'vue/no-deprecated-html-element-is': ['off'],
42
+    '@typescript-eslint/no-unused-vars': ['off'],
43
+    'comma-dangle': ['error', 'never'],
44
+    'comma-spacing': [
45
+      2,
46
+      {
47
+        before: false,
48
+        after: true
49
+      }
50
+    ],
51
+    'comma-style': [2, 'last']
52
+  }
53
+}

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
1
+# Logs
2
+logs
3
+*.log
4
+npm-debug.log*
5
+yarn-debug.log*
6
+yarn-error.log*
7
+pnpm-debug.log*
8
+lerna-debug.log*
9
+
10
+node_modules
11
+.DS_Store
12
+dist
13
+*.local
14
+unpackage
15
+.env.development
16
+
17
+# Editor directories and files
18
+.idea
19
+*.suo
20
+*.ntvs*
21
+*.njsproj
22
+*.sln
23
+*.sw?

+ 7 - 0
.prettierignore

@@ -0,0 +1,7 @@
1
+node_modules
2
+.DS_Store
3
+dist
4
+*.local
5
+unpackage
6
+.env.development
7
+uni_modules

+ 8 - 0
.prettierrc.json

@@ -0,0 +1,8 @@
1
+{
2
+  "singleQuote": true,
3
+  "semi": false,
4
+  "printWidth": 150,
5
+  "trailingComma": "none",
6
+  "endOfLine": "auto",
7
+  "tabWidth": 2
8
+}

+ 29 - 0
env.d.ts

@@ -0,0 +1,29 @@
1
+/*
2
+ * @Author: wyd
3
+ * @Date: 2024-03
4
+ * @LastEditors: wyd
5
+ * @LastEditTime: 2024-03
6
+ * @Description:
7
+ */
8
+/// <reference types="vite/client" />
9
+
10
+declare module '*.vue' {
11
+  import { DefineComponent } from 'vue'
12
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
13
+  const component: DefineComponent<{}, {}, any>
14
+  export default component
15
+}
16
+
17
+interface ImportMetaEnv {
18
+  /* API基本路径 */
19
+  readonly VITE_BASE_URL: string
20
+  /* 文件路径前缀 */
21
+  readonly VITE_OSS_URL: string
22
+  /* 文件桶 */
23
+  readonly VITE_BARREL_URL: string
24
+  /* websocket地址 */
25
+  readonly VITE_SOCKET_URL: string
26
+}
27
+interface ImportMeta {
28
+  readonly env: ImportMetaEnv
29
+}

+ 20 - 0
index.html

@@ -0,0 +1,20 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+  <head>
4
+    <meta charset="UTF-8" />
5
+    <script>
6
+      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
7
+        CSS.supports('top: constant(a)'))
8
+      document.write(
9
+        '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
10
+        (coverSupport ? ', viewport-fit=cover' : '') + '" />')
11
+    </script>
12
+    <title></title>
13
+    <!--preload-links-->
14
+    <!--app-context-->
15
+  </head>
16
+  <body>
17
+    <div id="app"><!--app-html--></div>
18
+    <script type="module" src="/src/main.js"></script>
19
+  </body>
20
+</html>

文件差異過大導致無法顯示
+ 10054 - 0
package-lock.json


+ 113 - 0
package.json

@@ -0,0 +1,113 @@
1
+{
2
+  "name": "uni-preset-vue",
3
+  "version": "0.0.0",
4
+  "scripts": {
5
+    "dev:app": "uni -p app",
6
+    "dev:app-android": "uni -p app-android",
7
+    "dev:app-ios": "uni -p app-ios",
8
+    "dev:custom": "uni -p",
9
+    "dev:h5": "uni --mode development",
10
+    "dev:h5:ssr": "uni --ssr",
11
+    "dev:mp-alipay": "uni -p mp-alipay",
12
+    "dev:mp-baidu": "uni -p mp-baidu",
13
+    "dev:mp-jd": "uni -p mp-jd",
14
+    "dev:mp-kuaishou": "uni -p mp-kuaishou",
15
+    "dev:mp-lark": "uni -p mp-lark",
16
+    "dev:mp-qq": "uni -p mp-qq",
17
+    "dev:mp-toutiao": "uni -p mp-toutiao",
18
+    "dev:mp-weixin": "uni -p mp-weixin --mode development",
19
+    "dev:mp-xhs": "uni -p mp-xhs",
20
+    "dev:quickapp-webview": "uni -p quickapp-webview",
21
+    "dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
22
+    "dev:quickapp-webview-union": "uni -p quickapp-webview-union",
23
+    "build:app": "uni build -p app",
24
+    "build:app-android": "uni build -p app-android",
25
+    "build:app-ios": "uni build -p app-ios",
26
+    "build:custom": "uni build -p",
27
+    "build:h5": "uni build",
28
+    "build:h5:ssr": "uni build --ssr",
29
+    "build:mp-alipay": "uni build -p mp-alipay",
30
+    "build:mp-baidu": "uni build -p mp-baidu",
31
+    "build:mp-jd": "uni build -p mp-jd",
32
+    "build:mp-kuaishou": "uni build -p mp-kuaishou",
33
+    "build:mp-lark": "uni build -p mp-lark",
34
+    "build:mp-qq": "uni build -p mp-qq",
35
+    "build:mp-toutiao": "uni build -p mp-toutiao",
36
+    "build:mp-weixin": "uni build -p mp-weixin",
37
+    "build:mp-weixin:sandbox": "uni build -p mp-weixin --mode sandbox",
38
+    "build:mp-xhs": "uni build -p mp-xhs",
39
+    "build:quickapp-webview": "uni build -p quickapp-webview",
40
+    "build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
41
+    "build:quickapp-webview-union": "uni build -p quickapp-webview-union",
42
+    "fix": "eslint src --fix",
43
+    "format": "prettier --write src/**/*.{ts,vue,js}"
44
+  },
45
+  "dependencies": {
46
+    "@dcloudio/uni-app": "3.0.0-alpha-4000320240311001",
47
+    "@dcloudio/uni-app-plus": "3.0.0-alpha-4000320240311001",
48
+    "@dcloudio/uni-components": "3.0.0-alpha-4000320240311001",
49
+    "@dcloudio/uni-h5": "3.0.0-alpha-4000320240311001",
50
+    "@dcloudio/uni-mp-alipay": "3.0.0-alpha-4000320240311001",
51
+    "@dcloudio/uni-mp-baidu": "3.0.0-alpha-4000320240311001",
52
+    "@dcloudio/uni-mp-jd": "3.0.0-alpha-4000320240311001",
53
+    "@dcloudio/uni-mp-kuaishou": "3.0.0-alpha-4000320240311001",
54
+    "@dcloudio/uni-mp-lark": "3.0.0-alpha-4000320240311001",
55
+    "@dcloudio/uni-mp-qq": "3.0.0-alpha-4000320240311001",
56
+    "@dcloudio/uni-mp-toutiao": "3.0.0-alpha-4000320240311001",
57
+    "@dcloudio/uni-mp-weixin": "3.0.0-alpha-4000320240311001",
58
+    "@dcloudio/uni-mp-xhs": "3.0.0-alpha-4000320240311001",
59
+    "@dcloudio/uni-quickapp-webview": "3.0.0-alpha-4000320240311001",
60
+    "lodash": "^4.17.21",
61
+    "pinia": "2.0.3",
62
+    "pinia-plugin-persistedstate": "^3.2.1",
63
+    "qs": "^6.5.3",
64
+    "quill": "^1.3.7",
65
+    "vue": "^3.4.19",
66
+    "vue-i18n": "^9.9.1"
67
+  },
68
+  "devDependencies": {
69
+    "@dcloudio/types": "^3.4.7",
70
+    "@dcloudio/uni-automator": "3.0.0-alpha-4000320240311001",
71
+    "@dcloudio/uni-cli-shared": "3.0.0-alpha-4000320240311001",
72
+    "@dcloudio/uni-stacktracey": "3.0.0-alpha-4000320240311001",
73
+    "@dcloudio/vite-plugin-uni": "3.0.0-alpha-4000320240311001",
74
+    "@rushstack/eslint-patch": "^1.1.4",
75
+    "@types/node": "^18.11.9",
76
+    "@uni-helper/uni-app-types": "^0.5.12",
77
+    "@uni-helper/uni-ui-types": "^0.5.12",
78
+    "@vue/eslint-config-prettier": "^7.0.0",
79
+    "@vue/eslint-config-typescript": "^11.0.0",
80
+    "@vue/runtime-core": "^3.4.19",
81
+    "@vue/tsconfig": "^0.4.0",
82
+    "eslint": "^8.22.0",
83
+    "eslint-plugin-vue": "^9.3.0",
84
+    "husky": "^8.0.0",
85
+    "lint-staged": "^13.0.3",
86
+    "miniprogram-api-typings": "^3.12.0",
87
+    "prettier": "^2.7.1",
88
+    "sass": "^1.56.1",
89
+    "typescript": "^5.1.6",
90
+    "vite": "^4.3.5",
91
+    "vue-tsc": "^1.8.8"
92
+  },
93
+  "uni-app": {
94
+    "scripts": {
95
+      "mp-weixin-dev": {
96
+        "title": "微信小程序测试环境",
97
+        "env": {
98
+          "UNI_PLATFORM": "mp-weixin",
99
+          "MODE": "development",
100
+          "BASEURL": "http://sandbox.hqhuitong.com/api"
101
+        }
102
+      },
103
+      "mp-weixin-build": {
104
+        "title": "微信小程序生产环境",
105
+        "env": {
106
+          "UNI_PLATFORM": "mp-weixin",
107
+          "MODE": "production",
108
+          "BASEURL": "https://api.hqhuitong.com/api"
109
+        }
110
+      }
111
+    }
112
+  }
113
+}

文件差異過大導致無法顯示
+ 7904 - 0
pnpm-lock.yaml


+ 67 - 0
src/App.vue

@@ -0,0 +1,67 @@
1
+<script setup lang="ts">
2
+import { user } from '@/stores/modules/user'
3
+import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
4
+
5
+const userStore = user()
6
+
7
+onLaunch(() => {
8
+  if (userStore.token) {
9
+    //
10
+  }
11
+})
12
+onShow(() => {
13
+  console.log('App Show')
14
+})
15
+onHide(() => {
16
+  console.log('App Hide')
17
+})
18
+</script>
19
+
20
+<style lang="scss">
21
+/*每个页面公共css */
22
+@import '@/styles/index.scss';
23
+</style>
24
+
25
+<style lang="scss">
26
+@import './uni_modules/vk-uview-ui/index.scss';
27
+page {
28
+  background-color: #f8f8f8;
29
+}
30
+/* @font-face {
31
+  font-family: 'MyFont';
32
+  src: url('https://myqn-1305113899.cos.ap-nanjing.myqcloud.com/static/front/zlhy.ttf');
33
+} */
34
+
35
+@font-face {
36
+  font-family: 'iconfont';
37
+  src: url('@/static/iconfont/iconfont.ttf');
38
+}
39
+
40
+view,
41
+text {
42
+  font-family: 'PingFang SC', 'Noto', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif';
43
+}
44
+/* 解决小程序和app滚动条的问题 */
45
+/* #ifdef MP-WEIXIN || APP-PLUS */
46
+::-webkit-scrollbar {
47
+  display: none;
48
+  width: 0 !important;
49
+  height: 0 !important;
50
+  -webkit-appearance: none;
51
+  background: transparent;
52
+  color: transparent;
53
+}
54
+/* #endif */
55
+
56
+/* 解决H5 的问题 */
57
+/* #ifdef H5 */
58
+uni-scroll-view .uni-scroll-view::-webkit-scrollbar {
59
+  display: none;
60
+  width: 0 !important;
61
+  height: 0 !important;
62
+  -webkit-appearance: none;
63
+  background: transparent;
64
+  color: transparent;
65
+}
66
+/* #endif */
67
+</style>

+ 1 - 0
src/api/home.ts

@@ -0,0 +1 @@
1
+import { http } from '@/utils/http'

+ 147 - 0
src/api/login.ts

@@ -0,0 +1,147 @@
1
+/*
2
+ * @Author: wyd
3
+ * @Date: 2024-02
4
+ * @LastEditors: wyd
5
+ * @LastEditTime: 2024-07
6
+ * @Description: 登录api
7
+ */
8
+import { http } from '@/utils/http'
9
+import type { loginItem, captchaItem, captchaResult, smsItem, registerItem, forgetItem, loginResult, useInfoResult } from '@/types/login'
10
+
11
+/**
12
+ * @description: 登录
13
+ * @param {*} data isBind 1微信未绑定系统用户 0已绑定
14
+ * @return {*}
15
+ */
16
+export const loginApi = (data: loginItem, isBind: number) => {
17
+  if (isBind === 1) {
18
+    // 微信登录且需要绑定
19
+    return http<loginResult>({
20
+      method: 'POST',
21
+      url: '/xcxBind',
22
+      data
23
+    })
24
+  }
25
+  if (data.type === 1) {
26
+    // 短信登录
27
+    return http<loginResult>({
28
+      method: 'POST',
29
+      url: '/smsLogin',
30
+      data
31
+    })
32
+  }
33
+  // 账号密码登录
34
+  return http<loginResult>({
35
+    method: 'POST',
36
+    url: '/mpLogin',
37
+    data
38
+  })
39
+}
40
+
41
+/**
42
+ * @description: 获取用户信息
43
+ * @param {*} data
44
+ * @return {*}
45
+ */
46
+export const getUserInfoApi = () => {
47
+  return http<useInfoResult>({
48
+    method: 'GET',
49
+    url: '/getInfo'
50
+  })
51
+}
52
+
53
+/**
54
+ * @description: 获取验证码图片
55
+ * @param {*} data
56
+ * @return {*}
57
+ */
58
+export const getCaptchaImageApi = (data?: captchaItem) => {
59
+  return http<captchaResult>({
60
+    method: 'GET',
61
+    url: '/captchaImage',
62
+    data
63
+  })
64
+}
65
+
66
+/**
67
+ * @description: 获取短信验证码
68
+ * @param {*} data
69
+ * @return {*}
70
+ */
71
+export const getSmsApi = (data?: smsItem) => {
72
+  return http({
73
+    method: 'GET',
74
+    url: '/captchaSms',
75
+    data
76
+  })
77
+}
78
+
79
+/**
80
+ * @description: 注册
81
+ * @param {*} data
82
+ * @return {*}
83
+ */
84
+export const registerApi = (data?: registerItem) => {
85
+  return http({
86
+    method: 'POST',
87
+    url: '/register',
88
+    data
89
+  })
90
+}
91
+
92
+/**
93
+ * @description: 忘记密码
94
+ * @param {*} data
95
+ * @return {*}
96
+ */
97
+export const forgetApi = (data?: forgetItem) => {
98
+  return http({
99
+    method: 'POST',
100
+    url: '/password/reset',
101
+    data
102
+  })
103
+}
104
+
105
+/**
106
+ * @description: 微信登录
107
+ * @param {*} data
108
+ * @return {*}
109
+ */
110
+export const weixinApi = (data: { code: string }) => {
111
+  return http<loginResult>({
112
+    method: 'POST',
113
+    url: '/xcxLogin',
114
+    header: {
115
+      'content-type': 'application/x-www-form-urlencoded'
116
+    },
117
+    data
118
+  })
119
+}
120
+
121
+/**
122
+ * @description: 退出登录
123
+ * @param {*}
124
+ * @return {*}
125
+ */
126
+export const quiteApi = () => {
127
+  return http({
128
+    method: 'POST',
129
+    url: '/logout'
130
+  })
131
+}
132
+
133
+/**
134
+ * @description: 手机号登录
135
+ * @param {*} data
136
+ * @return {*}
137
+ */
138
+export const phoneLoginApi = (data: { code: string }) => {
139
+  return http<loginResult>({
140
+    method: 'POST',
141
+    url: '/xcxPhoneLogin',
142
+    header: {
143
+      'content-type': 'application/x-www-form-urlencoded'
144
+    },
145
+    data
146
+  })
147
+}

+ 226 - 0
src/api/system.ts

@@ -0,0 +1,226 @@
1
+/*
2
+ * @Author: wyd
3
+ * @Date: 2024-02
4
+ * @LastEditors: JiangChunMei
5
+ * @LastEditTime: 2024-06-24 14:28:49
6
+ * @Description: 全局api
7
+ */
8
+import type { ConfigItem } from '@/types/system'
9
+import { http } from '@/utils/http'
10
+import type { useInfoResult, userItem } from '@/types/login'
11
+
12
+/**
13
+ * @description: 获取全局配置
14
+ * @return {*}
15
+ */
16
+export const getConfigAPI = () => {
17
+  return http<ConfigItem>({
18
+    method: 'GET',
19
+    url: '/system/config/basic'
20
+  })
21
+}
22
+
23
+/**
24
+ * @description: 获取全部园区楼宇
25
+ * @return {*}
26
+ */
27
+export const getParkAPI = () => {
28
+  return http<ParkItem[]>({
29
+    method: 'GET',
30
+    url: '/space/park/select/all'
31
+  })
32
+}
33
+
34
+/**
35
+ * @description: 获取当前用户有权限的园区楼宇
36
+ * @return {*}
37
+ */
38
+export const getParkFilterAPI = () => {
39
+  return http<ParkItem[]>({
40
+    method: 'GET',
41
+    url: '/space/park/select'
42
+  })
43
+}
44
+
45
+/**
46
+ * @description: 获取当前用户的所属企业
47
+ * @return {*}
48
+ */
49
+export const getEnterpriseAPI = () => {
50
+  return http<enterpriseItem[]>({
51
+    method: 'GET',
52
+    url: '/enterprise/current/select'
53
+  })
54
+}
55
+
56
+/**
57
+ * @description: 获取用户信息
58
+ * @param {*} data
59
+ * @return {*}
60
+ */
61
+export const getUserInfoApi = () => {
62
+  return http<useInfoResult>({
63
+    method: 'GET',
64
+    url: '/system/user/profile'
65
+  })
66
+}
67
+
68
+/**
69
+ * @description: 保存用户信息
70
+ * @param {*} data
71
+ * @return {*}
72
+ */
73
+export const saveUserInfoApi = (data: userItem) => {
74
+  return http({
75
+    method: 'PUT',
76
+    url: '/system/user/profile',
77
+    data
78
+  })
79
+}
80
+
81
+/**
82
+ * @description: 修改密码
83
+ * @param {*} oldPassword 旧密码 newPassword新密码
84
+ * @return {*}
85
+ */
86
+export const savePwdApi = (data: { oldPassword: string; newPassword: string }) => {
87
+  return http({
88
+    method: 'PUT',
89
+    url: `/system/user/profile/updatePwd?oldPassword=${data.oldPassword}&newPassword=${data.newPassword}`,
90
+    data
91
+  })
92
+}
93
+
94
+/**
95
+ * @description: 系统管理员+园区管理员分组下拉框
96
+ * @param {*} data
97
+ * @return {*}
98
+ */
99
+export const getAdministratorApi = (data: { parkIds: EmptyArrayType }) => {
100
+  return http<EmptyObjectType[]>({
101
+    method: 'GET',
102
+    url: '/administrator/select',
103
+    data
104
+  })
105
+}
106
+
107
+/**
108
+ * @description: 获取省市区
109
+ * @return {*}
110
+ */
111
+export const getRegionApi = () => {
112
+  return http<EmptyObjectType[]>({
113
+    method: 'GET',
114
+    url: '/system/region/list'
115
+  })
116
+}
117
+
118
+/**
119
+ * @description: 获取系统管理员树
120
+ * @return {*}
121
+ * @author: JiangChunMei
122
+ */
123
+export const getSystemUserApi = () => {
124
+  return http<EmptyObjectType[]>({
125
+    url: '/user/system',
126
+    method: 'GET'
127
+  })
128
+}
129
+
130
+/**
131
+ * @description: 获取园区管理员树
132
+ * @param {object} data
133
+ * @return {*}
134
+ * @author: JiangChunMei
135
+ */
136
+export const getParkUserApi = (data: { parkIds?: string[] }) => {
137
+  return http<EmptyObjectType[]>({
138
+    url: '/user/park',
139
+    method: 'GET',
140
+    data
141
+  })
142
+}
143
+
144
+/**
145
+ * @description: 获取系统角色树
146
+ * @return {*}
147
+ * @author: JiangChunMei
148
+ */
149
+export const getSystemRoleApi = () => {
150
+  return http<EmptyObjectType[]>({
151
+    url: '/system/role/admin/list',
152
+    method: 'GET'
153
+  })
154
+}
155
+
156
+/**
157
+ * @description: 获取园区角色树
158
+ * @param {object} data
159
+ * @return {*}
160
+ * @author: JiangChunMei
161
+ */
162
+export const getParkRoleApi = (data: { parkId?: string; parkIds?: string[] }) => {
163
+  return http<EmptyObjectType[]>({
164
+    url: '/role/park',
165
+    method: 'GET',
166
+    data
167
+  })
168
+}
169
+
170
+/**
171
+ * @description: 获取企业用户树
172
+ * @param {object} data
173
+ * @return {*}
174
+ * @author: JiangChunMei
175
+ */
176
+export const getEnterpriseUserApi = (data: { status?: number; parkIds?: string[] }) => {
177
+  return http<EmptyObjectType[]>({
178
+    url: '/user/enterprise',
179
+    method: 'GET',
180
+    data
181
+  })
182
+}
183
+
184
+export const getDepartmentApi = () => {
185
+  return http<EmptyObjectType[]>({
186
+    url: '/department/tree',
187
+    method: 'GET'
188
+  })
189
+}
190
+
191
+/**
192
+ * @description: 自定义分类(无分页)
193
+ * @param {*} data type 1新闻分类 2通知公告 18智慧旅游
194
+ * @return {*}
195
+ */
196
+export function treeCategoryApi(data: { type: number }) {
197
+  return http<EmptyObjectType[]>({
198
+    url: '/operate/news/category/tree',
199
+    method: 'GET',
200
+    data
201
+  })
202
+}
203
+
204
+/**
205
+ * @description: 获取指定类型的数据字典
206
+ * @param {*} dictType 类型
207
+ * @return {*}
208
+ */
209
+export function getDictTypeApi(dictType: string) {
210
+  return http<EmptyArrayType>({
211
+    url: '/system/dict/data/type/' + dictType
212
+  })
213
+}
214
+
215
+/**
216
+ * @description: 查询OSS对象基于id串
217
+ * @param {string} id
218
+ * @return {*}
219
+ * @author: JiangChunMei
220
+ */
221
+export const getListByIdsApi = (id: string | string[]) => {
222
+  return http<EmptyArrayType>({
223
+    url: '/system/oss/listByIds/' + id,
224
+    method: 'GET'
225
+  })
226
+}

+ 63 - 0
src/base/components/Captcha.vue

@@ -0,0 +1,63 @@
1
+<!--
2
+ * @Author: wyd
3
+ * @Date: 2024-03
4
+ * @LastEditors: wyd
5
+ * @LastEditTime: 2024-04
6
+ * @Description: 验证码
7
+-->
8
+
9
+<script setup lang="ts">
10
+import { watch, ref, onMounted } from 'vue'
11
+import { getCaptchaImageApi } from '@/api/login'
12
+const props = defineProps({
13
+  modelValue: String
14
+})
15
+const emit = defineEmits(['getCaptchaInfo', 'update:modelValue'])
16
+const form = {
17
+  code: '',
18
+  uuid: '',
19
+  captchaEnabled: true
20
+}
21
+watch(
22
+  () => props.modelValue,
23
+  (val, pre) => {
24
+    form.code = props.modelValue || ''
25
+  }
26
+)
27
+// 更新code
28
+const codeChange = () => {
29
+  emit('update:modelValue', form.code)
30
+  emit('getCaptchaInfo', form)
31
+}
32
+const imgPath = ref('')
33
+// 获取验证码图片
34
+const getCaptchaImage = () => {
35
+  getCaptchaImageApi({ isWeb: false }).then((res) => {
36
+    form.captchaEnabled = res.data.captchaEnabled
37
+    form.uuid = res.data.uuid
38
+    if (res.data.captchaEnabled) {
39
+      imgPath.value = 'data:image/jpeg;base64,' + res.data.img
40
+    }
41
+    emit('getCaptchaInfo', form)
42
+  })
43
+}
44
+onMounted(() => {
45
+  getCaptchaImage()
46
+})
47
+defineExpose({ getCaptchaImage })
48
+</script>
49
+
50
+<template>
51
+  <view class="flex j-sb a-center">
52
+    <uni-easyinput type="text" v-model="form.code" placeholder="请输入验证码" @change="codeChange" />
53
+    <image :src="imgPath" mode="aspectFill" class="capcha-img ml10" @tap="getCaptchaImage" />
54
+  </view>
55
+</template>
56
+
57
+<style lang="scss" scoped>
58
+.capcha-img {
59
+  width: 100px;
60
+  height: 35px;
61
+  border-radius: 4px;
62
+}
63
+</style>

文件差異過大導致無法顯示
+ 238 - 0
src/base/components/Editors/editor-icon.css


二進制
src/base/components/Editors/iconfont.ttf


+ 286 - 0
src/base/components/Editors/index.vue

@@ -0,0 +1,286 @@
1
+<template>
2
+  <!-- <view class="container">
3
+    <view class="page-body"> -->
4
+  <view class="wrapper">
5
+    <view class="toolbar" @tap="format" style="height: 320rpx; overflow-y: auto">
6
+      <view :class="formats.bold ? 'ql-active' : ''" class="iconfont icon-zitijiacu" data-name="bold"></view>
7
+      <view :class="formats.italic ? 'ql-active' : ''" class="iconfont icon-zitixieti" data-name="italic"></view>
8
+      <view :class="formats.underline ? 'ql-active' : ''" class="iconfont icon-zitixiahuaxian" data-name="underline"></view>
9
+      <view :class="formats.strike ? 'ql-active' : ''" class="iconfont icon-zitishanchuxian" data-name="strike"></view>
10
+      <!-- #ifndef MP-BAIDU -->
11
+      <view :class="formats.align === 'left' ? 'ql-active' : ''" class="iconfont icon-zuoduiqi" data-name="align" data-value="left"></view>
12
+      <!-- #endif -->
13
+      <view :class="formats.align === 'center' ? 'ql-active' : ''" class="iconfont icon-juzhongduiqi" data-name="align" data-value="center"></view>
14
+      <view :class="formats.align === 'right' ? 'ql-active' : ''" class="iconfont icon-youduiqi" data-name="align" data-value="right"></view>
15
+      <view :class="formats.align === 'justify' ? 'ql-active' : ''" class="iconfont icon-zuoyouduiqi" data-name="align" data-value="justify"></view>
16
+      <!-- #ifndef MP-BAIDU -->
17
+      <view :class="formats.lineHeight ? 'ql-active' : ''" class="iconfont icon-line-height" data-name="lineHeight" data-value="2"></view>
18
+      <view
19
+        :class="formats.letterSpacing ? 'ql-active' : ''"
20
+        class="iconfont icon-Character-Spacing"
21
+        data-name="letterSpacing"
22
+        data-value="2em"
23
+      ></view>
24
+      <view :class="formats.marginTop ? 'ql-active' : ''" class="iconfont icon-722bianjiqi_duanqianju" data-name="marginTop" data-value="20px"></view>
25
+      <view
26
+        :class="formats.marginBottom ? 'ql-active' : ''"
27
+        class="iconfont icon-723bianjiqi_duanhouju"
28
+        data-name="marginBottom"
29
+        data-value="20px"
30
+      ></view>
31
+      <!-- #endif -->
32
+
33
+      <view class="iconfont icon-clearedformat" @tap="removeFormat"></view>
34
+
35
+      <!-- #ifndef MP-BAIDU -->
36
+      <view :class="formats.fontFamily ? 'ql-active' : ''" class="iconfont icon-font" data-name="fontFamily" data-value="Pacifico"></view>
37
+      <view :class="formats.fontSize === '24px' ? 'ql-active' : ''" class="iconfont icon-fontsize" data-name="fontSize" data-value="24px"></view>
38
+      <!-- #endif -->
39
+      <view :class="formats.color === '#0000ff' ? 'ql-active' : ''" class="iconfont icon-text_color" data-name="color" data-value="#0000ff"></view>
40
+      <view
41
+        :class="formats.backgroundColor === '#00ff00' ? 'ql-active' : ''"
42
+        class="iconfont icon-fontbgcolor"
43
+        data-name="backgroundColor"
44
+        data-value="#00ff00"
45
+      ></view>
46
+      <view class="iconfont icon-date" @tap="insertDate"></view>
47
+      <view class="iconfont icon--checklist" data-name="list" data-value="check"></view>
48
+      <view :class="formats.list === 'ordered' ? 'ql-active' : ''" class="iconfont icon-youxupailie" data-name="list" data-value="ordered"></view>
49
+      <view :class="formats.list === 'bullet' ? 'ql-active' : ''" class="iconfont icon-wuxupailie" data-name="list" data-value="bullet"></view>
50
+
51
+      <view class="iconfont icon-undo" @tap="undo"></view>
52
+      <view class="iconfont icon-redo" @tap="redo"></view>
53
+
54
+      <view class="iconfont icon-outdent" data-name="indent" data-value="-1"></view>
55
+      <view class="iconfont icon-indent" data-name="indent" data-value="+1"></view>
56
+      <view class="iconfont icon-fengexian" @tap="insertDivider"></view>
57
+      <view class="iconfont icon-charutupian" @tap="insertImage"></view>
58
+      <view :class="formats.header === 1 ? 'ql-active' : ''" class="iconfont icon-format-header-1" data-name="header" :data-value="1"></view>
59
+      <view :class="formats.script === 'sub' ? 'ql-active' : ''" class="iconfont icon-zitixiabiao" data-name="script" data-value="sub"></view>
60
+      <view :class="formats.script === 'super' ? 'ql-active' : ''" class="iconfont icon-zitishangbiao" data-name="script" data-value="super"></view>
61
+
62
+      <view class="iconfont icon-shanchu" @tap="clear"></view>
63
+
64
+      <view :class="formats.direction === 'rtl' ? 'ql-active' : ''" class="iconfont icon-direction-rtl" data-name="direction" data-value="rtl"></view>
65
+    </view>
66
+    <view class="editor-wrapper">
67
+      <editor
68
+        id="editor"
69
+        class="ql-container"
70
+        placeholder="请输入..."
71
+        show-img-size
72
+        show-img-toolbar
73
+        show-img-resize
74
+        @statuschange="onStatusChange"
75
+        :read-only="readOnly"
76
+        @ready="onEditorReady"
77
+        @input="onInput"
78
+      >
79
+      </editor>
80
+    </view>
81
+  </view>
82
+  <!-- </view>
83
+  </view> -->
84
+</template>
85
+
86
+<script>
87
+export default {
88
+  props: {
89
+    modelValue: {
90
+      type: String,
91
+      default: ''
92
+    }
93
+  },
94
+  data() {
95
+    return {
96
+      readOnly: false,
97
+      formats: {},
98
+      curContent: ''
99
+    }
100
+  },
101
+  watch: {
102
+    modelValue: {
103
+      handler(val) {
104
+        this.curContent = val
105
+      },
106
+      deep: true,
107
+      immediate: true
108
+    }
109
+  },
110
+  onLoad() {
111
+    // #ifndef MP-BAIDU
112
+    uni.loadFontFace({
113
+      family: 'Pacifico',
114
+      source: 'url("https://sungd.github.io/Pacifico.ttf")'
115
+    })
116
+    // #endif
117
+  },
118
+  methods: {
119
+    readOnlyChange() {
120
+      this.readOnly = !this.readOnly
121
+    },
122
+    onEditorReady() {
123
+      // #ifdef MP-BAIDU
124
+      this.editorCtx = requireDynamicLib('editorLib').createEditorContext('editor')
125
+      // #endif
126
+
127
+      // #ifdef APP-PLUS || MP-WEIXIN || H5
128
+      uni
129
+        .createSelectorQuery()
130
+        .in(this)
131
+        .select('#editor')
132
+        .context((res) => {
133
+          this.editorCtx = res.context
134
+
135
+          // setTimeout(() => {
136
+          //   this.editorCtx.setContents({
137
+          //     html: this.curContent
138
+          //   })
139
+          //   uni.pageScrollTo({
140
+          //     scrollTop: 0,
141
+          //     duration: 300
142
+          //   })
143
+          // }, 100)
144
+        })
145
+        .exec()
146
+      // #endif
147
+    },
148
+    undo() {
149
+      this.editorCtx.undo()
150
+    },
151
+    redo() {
152
+      this.editorCtx.redo()
153
+    },
154
+    format(e) {
155
+      let { name, value } = e.target.dataset
156
+      if (!name) return
157
+      this.editorCtx.format(name, value)
158
+    },
159
+    onStatusChange(e) {
160
+      const formats = e.detail
161
+      this.formats = formats
162
+      this.curContent = e.detail.html
163
+      this.$emit('update:content', e.detail.html)
164
+    },
165
+    onInput(e) {
166
+      this.curContent = e.detail.html
167
+      this.$emit('update:content', e.detail.html)
168
+    },
169
+    insertDivider() {
170
+      this.editorCtx.insertDivider({
171
+        success: function () {
172
+          console.log('insert divider success')
173
+        }
174
+      })
175
+    },
176
+    clear() {
177
+      uni.showModal({
178
+        title: '清空编辑器',
179
+        content: '确定清空编辑器全部内容?',
180
+        success: (res) => {
181
+          if (res.confirm) {
182
+            this.editorCtx.clear({
183
+              success: function (res) {
184
+                console.log('clear success')
185
+              }
186
+            })
187
+          }
188
+        }
189
+      })
190
+    },
191
+    removeFormat() {
192
+      this.editorCtx.removeFormat()
193
+    },
194
+    insertDate() {
195
+      const date = new Date()
196
+      const formatDate = `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
197
+      this.editorCtx.insertText({
198
+        text: formatDate
199
+      })
200
+    },
201
+    insertImage() {
202
+      uni.chooseImage({
203
+        count: 1,
204
+        success: (res) => {
205
+          uni.uploadFile({
206
+            url: '/ueditor?action=uploadimage&encode=utf-8',
207
+            name: 'file',
208
+            fileType: 'image',
209
+            filePath: res.tempFilePaths[0],
210
+            success: (result) => {
211
+              const params = JSON.parse(result.data)
212
+              if (params.state === 'SUCCESS') {
213
+                this.editorCtx.insertImage({
214
+                  src: params.url,
215
+                  alt: '图像',
216
+                  success: function () {
217
+                    console.log('insert image success')
218
+                  }
219
+                })
220
+              } else {
221
+                uni.showToast({ icon: 'none', title: '上传失败请重试' })
222
+              }
223
+            }
224
+          })
225
+        }
226
+      })
227
+    }
228
+  }
229
+}
230
+</script>
231
+
232
+<style>
233
+@import './editor-icon.css';
234
+
235
+.page-body {
236
+  /* height: calc(100vh - var(--window-top) - var(--status-bar-height)); */
237
+  height: 1000rpx;
238
+}
239
+
240
+.wrapper {
241
+  height: 1000rpx;
242
+}
243
+
244
+.editor-wrapper {
245
+  /* 		height: calc(100vh - var(--window-top) - var(--status-bar-height) - 140px);*/
246
+  background: #fff;
247
+  height: 680rpx;
248
+}
249
+
250
+.iconfont {
251
+  display: inline-block;
252
+  padding: 8px 8px;
253
+  width: 24px;
254
+  height: 24px;
255
+  cursor: pointer;
256
+  font-size: 20px;
257
+}
258
+
259
+.toolbar {
260
+  /* box-sizing: border-box; */
261
+  border-bottom: 0;
262
+  font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
263
+}
264
+
265
+.ql-container {
266
+  /* box-sizing: border-box; */
267
+  padding: 12px 15px;
268
+  width: 100%;
269
+  /* min-height: 30vh;
270
+  height: 100%; */
271
+  /* margin-top: 20px; */
272
+  font-size: 28rpx;
273
+  line-height: 1.5;
274
+  height: 680rpx !important;
275
+  min-height: 680rpx !important;
276
+}
277
+
278
+.ql-active {
279
+  color: #06c;
280
+}
281
+
282
+/* editor {
283
+  height: 1000rpx !important;
284
+  min-height: 1000rpx !important;
285
+} */
286
+</style>

+ 181 - 0
src/base/forget.vue

@@ -0,0 +1,181 @@
1
+<!--
2
+ * @Author: wyd
3
+ * @Date: 2024-03
4
+ * @LastEditors: wyd
5
+ * @LastEditTime: 2024-03
6
+ * @Description: 忘记密码
7
+-->
8
+<script setup lang="ts">
9
+import { ref } from 'vue'
10
+import { onLoad, onPullDownRefresh, onReady } from '@dcloudio/uni-app'
11
+import { system } from '@/stores/modules/system'
12
+import { useGlobal } from '@/composables'
13
+import { validatorMobile, verifyPwd } from '@/utils/verification'
14
+import { getSmsApi, forgetApi } from '@/api/login'
15
+
16
+const systemStore = system()
17
+// 表单信息
18
+const form = ref({
19
+  passwordAgain: '',
20
+  password: '',
21
+  phonenumber: '',
22
+  smsCode: ''
23
+})
24
+// 表单校验
25
+const rules: UniHelper.UniFormsRules = {
26
+  password: {
27
+    rules: [
28
+      {
29
+        required: true,
30
+        errorMessage: '请输入密码'
31
+      },
32
+      {
33
+        validateFunction: verifyPwd
34
+      }
35
+    ]
36
+  },
37
+  passwordAgain: {
38
+    rules: [
39
+      {
40
+        required: true,
41
+        errorMessage: '请再次输入密码'
42
+      },
43
+      {
44
+        validateFunction: function (rule, value, data, callback) {
45
+          if (value !== data.password) {
46
+            callback('两次输入密码不一致')
47
+          }
48
+          return true
49
+        }
50
+      }
51
+    ]
52
+  },
53
+  phonenumber: {
54
+    rules: [
55
+      {
56
+        required: true,
57
+        errorMessage: '请输入手机号'
58
+      },
59
+      {
60
+        validateFunction: validatorMobile
61
+      }
62
+    ]
63
+  },
64
+  smsCode: {
65
+    rules: [
66
+      {
67
+        required: true,
68
+        errorMessage: '请输入验证码'
69
+      }
70
+    ]
71
+  }
72
+}
73
+
74
+// 短信验证码
75
+let isDisabled = ref(false)
76
+let countdown = ref(0)
77
+const getSmsCode = () => {
78
+  getSmsApi({ phonenumber: form.value.phonenumber, type: 2 }).then(() => {
79
+    uni.showToast({
80
+      title: '操作成功',
81
+      icon: 'none'
82
+    })
83
+    isDisabled.value = true
84
+    countdown.value = 60
85
+    const timerInterval = setInterval(() => {
86
+      if (countdown.value > 1) {
87
+        countdown.value--
88
+      } else {
89
+        isDisabled.value = false
90
+        clearInterval(timerInterval)
91
+      }
92
+    }, 1000)
93
+  })
94
+}
95
+
96
+// 表单组件实例
97
+const formRef = ref<UniHelper.UniFormsInstance>()
98
+
99
+const handleSubmit = async () => {
100
+  formRef.value?.validate?.((err, formData) => {
101
+    if (!err) {
102
+      uni.showLoading()
103
+      forgetApi(form.value).then(() => {
104
+        // 注册成功,返回上一页
105
+        uni.showToast({
106
+          title: '操作成功',
107
+          icon: 'none'
108
+        })
109
+        setTimeout(() => {
110
+          uni.navigateBack()
111
+        }, 1000)
112
+      })
113
+    }
114
+  })
115
+}
116
+onLoad(async () => {
117
+  await systemStore.getConfigData()
118
+})
119
+onReady(() => {
120
+  formRef.value?.setRules?.(rules)
121
+})
122
+// 下拉刷新
123
+onPullDownRefresh(async () => {
124
+  await systemStore.getConfigData()
125
+  uni.stopPullDownRefresh()
126
+})
127
+</script>
128
+
129
+<template>
130
+  <view class="login-box">
131
+    <view class="center-box p15 shadow">
132
+      <view class="tc f18 weight-600 black-color mb20">重置密码</view>
133
+      <uni-forms ref="formRef" :model="form" :rules="rules">
134
+        <uni-forms-item name="phonenumber">
135
+          <uni-easyinput type="text" v-model="form.phonenumber" placeholder="请输入手机号" />
136
+        </uni-forms-item>
137
+        <uni-forms-item name="password">
138
+          <uni-easyinput type="password" v-model="form.password" placeholder="请输入密码" />
139
+        </uni-forms-item>
140
+        <uni-forms-item name="passwordAgain">
141
+          <uni-easyinput type="password" v-model="form.passwordAgain" placeholder="请再次输入密码" />
142
+        </uni-forms-item>
143
+        <uni-forms-item name="smsCode">
144
+          <view class="flex j-sb a-center w100">
145
+            <uni-easyinput type="text" v-model="form.smsCode" placeholder="请输入验证码" />
146
+            <button class="ml10 sms-btn success-bg" :disabled="isDisabled" type="primary" size="mini" @tap="getSmsCode">
147
+              {{ isDisabled ? countdown + 's后重新获取' : '获取验证码' }}
148
+            </button>
149
+          </view>
150
+        </uni-forms-item>
151
+      </uni-forms>
152
+      <view class="btn-b warning-bg" @tap="handleSubmit">确 定</view>
153
+    </view>
154
+  </view>
155
+</template>
156
+
157
+<style lang="scss" scoped>
158
+.login-box {
159
+  width: 100vw;
160
+  height: 100vh;
161
+  background: url('http://1.94.6.75:9000/zlhy-app/bg_login.jpg') center no-repeat;
162
+  background-size: 100% 100%;
163
+  position: relative;
164
+  .center-box {
165
+    width: calc(100vw - 60px);
166
+    min-height: 200px;
167
+    border-radius: 10rpx;
168
+    background: rgba($color: #fff, $alpha: 0.8);
169
+    position: absolute;
170
+    left: 15px;
171
+    top: 20%;
172
+    .btn-b {
173
+      width: 100% !important;
174
+    }
175
+    .sms-btn {
176
+      height: 35px;
177
+      line-height: 35px;
178
+    }
179
+  }
180
+}
181
+</style>

+ 791 - 0
src/base/login.vue

@@ -0,0 +1,791 @@
1
+<script setup lang="ts">
2
+import { ref } from 'vue'
3
+import { onLoad, onReady } from '@dcloudio/uni-app'
4
+import { system } from '@/stores/modules/system'
5
+import { user } from '@/stores/modules/user'
6
+import { useGlobal } from '@/composables'
7
+import { validatorMobile, verifyPwd, validatorLoginMobile } from '@/utils/verification'
8
+import { loginApi, getSmsApi, weixinApi, getUserInfoApi, registerApi, forgetApi, phoneLoginApi } from '@/api/login'
9
+import Captcha from '@/base/components/Captcha.vue'
10
+import type { captchaInfo } from '@/types/login'
11
+import { getEnterpriseAPI } from '@/api/system'
12
+import type { useInfoResult } from '@/types/login'
13
+
14
+const userStore = user()
15
+const systemStore = system()
16
+// 表单信息
17
+const form = ref({
18
+  username: '',
19
+  password: '',
20
+  code: '',
21
+  phonenumber: '',
22
+  smsCode: '',
23
+  type: 2,
24
+  uuid: '',
25
+  openId: ''
26
+})
27
+
28
+// 表单校验
29
+const rules: UniHelper.UniFormsRules = {
30
+  username: {
31
+    rules: [
32
+      {
33
+        required: true,
34
+        errorMessage: '请输入手机号'
35
+      },
36
+      {
37
+        maxLength: useGlobal().textMax,
38
+        errorMessage: '限制{maxLength}个字符'
39
+      },
40
+      {
41
+        validateFunction: validatorLoginMobile
42
+      }
43
+    ]
44
+  },
45
+  password: {
46
+    rules: [
47
+      {
48
+        required: true,
49
+        errorMessage: '请输入密码'
50
+      }
51
+    ]
52
+  },
53
+  code: {
54
+    rules: [
55
+      {
56
+        required: true,
57
+        errorMessage: '请输入验证码'
58
+      }
59
+    ]
60
+  },
61
+  phonenumber: {
62
+    rules: [
63
+      {
64
+        required: true,
65
+        errorMessage: '请输入手机号'
66
+      },
67
+      {
68
+        validateFunction: validatorMobile
69
+      }
70
+    ]
71
+  },
72
+  smsCode: {
73
+    rules: [
74
+      {
75
+        required: true,
76
+        errorMessage: '请输入验证码'
77
+      }
78
+    ]
79
+  }
80
+}
81
+// 表单组件实例
82
+const formRef = ref<UniHelper.UniFormsInstance>()
83
+// 登录方式 1短信验证码登录 2密码登录
84
+const loginTypeChange = () => {
85
+  form.value.type = form.value.type === 1 ? 2 : 1
86
+}
87
+// 是否展示验证码
88
+let isShowCaptcha: boolean = true
89
+let uuid: string = ''
90
+const captchaRef = ref()
91
+const getCaptchaInfo = (val: captchaInfo) => {
92
+  isShowCaptcha = val.captchaEnabled
93
+  if (type.value === 1) {
94
+    uuid = val.uuid
95
+    form.value.code = val.code
96
+  } else {
97
+    registerForm.value.uuid = val.uuid
98
+    registerForm.value.code = val.code
99
+  }
100
+}
101
+// 短信验证码
102
+let isDisabled = ref(false)
103
+let countdown = ref(0)
104
+const getSmsCode = (type: number) => {
105
+  if (type === 1 && !form.value.phonenumber) {
106
+    useGlobal().tips('请输入手机号')
107
+    return
108
+  }
109
+  if (type === 2 && !pwdForm.value.phonenumber) {
110
+    useGlobal().tips('请输入手机号')
111
+    return
112
+  }
113
+  getSmsApi({ phonenumber: type === 1 ? form.value.phonenumber : pwdForm.value.phonenumber, type }).then(() => {
114
+    uni.showToast({
115
+      title: '操作成功',
116
+      icon: 'none'
117
+    })
118
+    isDisabled.value = true
119
+    countdown.value = 60
120
+    const timerInterval = setInterval(() => {
121
+      if (countdown.value > 1) {
122
+        countdown.value--
123
+      } else {
124
+        isDisabled.value = false
125
+        clearInterval(timerInterval)
126
+      }
127
+    }, 1000)
128
+  })
129
+}
130
+// 存储用户信息
131
+const refreshUserInfo = () => {
132
+  // 获取用户信息
133
+  getUserInfoApi().then((response) => {
134
+    const result = {
135
+      ...response?.data?.user,
136
+      secretary: 1
137
+    }
138
+    // 党员是否为书记
139
+    if (result.type & 32) {
140
+      result.secretary = response?.data?.secretary
141
+    }
142
+    // 存储用户信息
143
+    userStore.setUserInfo(result as useInfoResult)
144
+    // 存储权限
145
+    let permissions = []
146
+    if (response?.data?.permissions) {
147
+      for (let item of response.data.permissions) {
148
+        permissions.push(item)
149
+      }
150
+    }
151
+    userStore.setPermissions(permissions)
152
+    // 登录成功返回上一页
153
+    uni.navigateBack()
154
+  }).catch(() => {
155
+    uni.navigateBack()
156
+  })
157
+}
158
+// 登录
159
+const handleSubmit = async () => {
160
+  formRef.value?.validate?.((err, formData) => {
161
+    if (!err) {
162
+      let params = ref()
163
+      form.value.uuid = isShowCaptcha && form.value.type === 2 ? uuid : ''
164
+      if (isBind.value === 1) {
165
+        // 微信绑定
166
+        params.value = {
167
+          username: form.value.username,
168
+          password: form.value.password,
169
+          uuid: form.value.uuid,
170
+          openId: openId.value,
171
+          platform: 'pc'
172
+        }
173
+      } else {
174
+        params.value = form.value
175
+      }
176
+      uni.showLoading()
177
+      loginApi(params.value, isBind.value)
178
+        .then((res) => {
179
+          // 存储token
180
+          userStore.setToken(res?.data?.token)
181
+          setTimeout(() => {
182
+            refreshUserInfo()
183
+          }, 100)
184
+        })
185
+        .catch((err) => {
186
+          captchaRef.value?.getCaptchaImage()
187
+        })
188
+    }
189
+  })
190
+  // try {
191
+  //   // 表单校验
192
+  //   await formRef.value?.validate?.()
193
+  //   // 校验通过后再发送请求
194
+  // } catch (error) {
195
+  //   //
196
+  // }
197
+}
198
+// 清空密码
199
+const handleClear = () => {
200
+  form.value.password = ''
201
+}
202
+// 注册
203
+const handleRegister = () => {
204
+  uni.navigateTo({ url: '/base/register' })
205
+}
206
+// 忘记密码
207
+const handleForget = () => {
208
+  uni.navigateTo({ url: '/base/forget' })
209
+}
210
+// 微信登录
211
+let openId = ref('')
212
+// 微信是否绑定系统用户 1未绑定 0已绑定
213
+let isBind = ref(0)
214
+const handleUserProfile = () => {
215
+  uni.showLoading()
216
+  uni.login({
217
+    provider: 'weixin',
218
+    success: function (res) {
219
+      weixinApi({ code: res.code }).then((result) => {
220
+        console.log(result, 'res.code')
221
+        if (result.code === 409) {
222
+          // 用户不存在需要用户手动绑定
223
+          uni.showToast({
224
+            title: '微信账号未绑定系统用户,请先绑定',
225
+            icon: 'none',
226
+            duration: 3000
227
+          })
228
+          isBind.value = 1
229
+          openId.value = result.msg
230
+        } else {
231
+          isBind.value = 0
232
+          // 存储token
233
+          userStore.setToken(result?.data?.token)
234
+          // 返回用户token,正常存储登录
235
+          setTimeout(() => {
236
+            refreshUserInfo()
237
+          }, 100)
238
+        }
239
+      })
240
+    },
241
+    complete: function () {
242
+      uni.hideLoading()
243
+    }
244
+  })
245
+}
246
+
247
+// 注册
248
+// 表单校验
249
+const registerRules: UniHelper.UniFormsRules = {
250
+  // username: {
251
+  //   rules: [
252
+  //     {
253
+  //       required: true,
254
+  //       errorMessage: '请输入用户名'
255
+  //     },
256
+  //     {
257
+  //       maxLength: useGlobal().textMax,
258
+  //       errorMessage: '限制{maxLength}个字符'
259
+  //     }
260
+  //   ]
261
+  // },
262
+  realname: {
263
+    rules: [
264
+      {
265
+        required: true,
266
+        errorMessage: '请输入姓名'
267
+      },
268
+      {
269
+        maxLength: useGlobal().textMax,
270
+        errorMessage: '限制{maxLength}个字符'
271
+      }
272
+    ]
273
+  },
274
+  password: {
275
+    rules: [
276
+      {
277
+        required: true,
278
+        errorMessage: '请输入密码'
279
+      },
280
+      {
281
+        validateFunction: verifyPwd
282
+      }
283
+    ]
284
+  },
285
+  passwordAgain: {
286
+    rules: [
287
+      {
288
+        required: true,
289
+        errorMessage: '请再次输入密码'
290
+      },
291
+      {
292
+        validateFunction: function (rule, value, data, callback) {
293
+          if (value !== data.password) {
294
+            callback('两次输入密码不一致')
295
+          }
296
+          return true
297
+        }
298
+      }
299
+    ]
300
+  },
301
+  code: {
302
+    rules: [
303
+      {
304
+        required: true,
305
+        errorMessage: '请输入验证码'
306
+      }
307
+    ]
308
+  },
309
+  mobile: {
310
+    rules: [
311
+      {
312
+        required: true,
313
+        errorMessage: '请输入手机号'
314
+      },
315
+      {
316
+        validateFunction: validatorMobile
317
+      }
318
+    ]
319
+  }
320
+}
321
+// 表单组件实例
322
+const registerRef = ref<UniHelper.UniFormsInstance>()
323
+// 表单信息
324
+const registerForm = ref({
325
+  // username: '',
326
+  realname: '',
327
+  password: '',
328
+  mobile: '',
329
+  uuid: '',
330
+  code: '',
331
+  passwordAgain: ''
332
+})
333
+
334
+const getRegisterCaptchaInfo = (val: captchaInfo) => {
335
+  isShowCaptcha = val.captchaEnabled
336
+  registerForm.value.uuid = val.uuid
337
+  registerForm.value.code = val.code
338
+}
339
+
340
+const handleRegisterSubmit = () => {
341
+  registerRef.value?.validate?.((err, formData) => {
342
+    if (!err) {
343
+      uni.showLoading()
344
+      registerApi(registerForm.value)
345
+        .then(() => {
346
+          // 注册成功,返回登录
347
+          uni.showToast({
348
+            title: '操作成功',
349
+            icon: 'none'
350
+          })
351
+          type.value = 1
352
+        })
353
+        .catch(() => {
354
+          captchaRef.value?.getCaptchaImage()
355
+        })
356
+    }
357
+  })
358
+}
359
+
360
+// 忘记密码
361
+// 表单信息
362
+const pwdForm = ref({
363
+  passwordAgain: '',
364
+  password: '',
365
+  phonenumber: '',
366
+  smsCode: ''
367
+})
368
+// 表单校验
369
+const pwdRules: UniHelper.UniFormsRules = {
370
+  password: {
371
+    rules: [
372
+      {
373
+        required: true,
374
+        errorMessage: '请输入密码'
375
+      },
376
+      {
377
+        validateFunction: verifyPwd
378
+      }
379
+    ]
380
+  },
381
+  passwordAgain: {
382
+    rules: [
383
+      {
384
+        required: true,
385
+        errorMessage: '请再次输入密码'
386
+      },
387
+      {
388
+        validateFunction: function (rule, value, data, callback) {
389
+          if (value !== data.password) {
390
+            callback('两次输入密码不一致')
391
+          }
392
+          return true
393
+        }
394
+      }
395
+    ]
396
+  },
397
+  phonenumber: {
398
+    rules: [
399
+      {
400
+        required: true,
401
+        errorMessage: '请输入手机号'
402
+      },
403
+      {
404
+        validateFunction: validatorMobile
405
+      }
406
+    ]
407
+  },
408
+  smsCode: {
409
+    rules: [
410
+      {
411
+        required: true,
412
+        errorMessage: '请输入验证码'
413
+      }
414
+    ]
415
+  }
416
+}
417
+const pwdFormRef = ref<UniHelper.UniFormsInstance>()
418
+const handlePwdSubmit = async () => {
419
+  pwdFormRef.value?.validate?.((err, formData) => {
420
+    if (!err) {
421
+      uni.showLoading()
422
+      forgetApi(form.value).then(() => {
423
+        // 注册成功,返回登录
424
+        uni.showToast({
425
+          title: '操作成功',
426
+          icon: 'none'
427
+        })
428
+        type.value = 1
429
+      })
430
+    }
431
+  })
432
+}
433
+
434
+let type = ref(1) // 1登录 2注册
435
+const changeType = (index: number) => {
436
+  type.value = index
437
+  if (index === 2) {
438
+    registerForm.value = {
439
+      // username: '',
440
+      realname: '',
441
+      password: '',
442
+      mobile: '',
443
+      uuid: '',
444
+      code: ''
445
+    }
446
+    registerRef.value?.setRules?.(registerRules)
447
+    registerRef.value?.clearValidate()
448
+  } else if (type.value === 3) {
449
+    pwdForm.value = {
450
+      passwordAgain: '',
451
+      password: '',
452
+      phonenumber: '',
453
+      smsCode: ''
454
+    }
455
+    pwdFormRef.value?.setRules?.(pwdRules)
456
+    pwdFormRef.value?.clearValidate()
457
+  } else {
458
+    form.value = {
459
+      username: '',
460
+      password: '',
461
+      code: '',
462
+      phonenumber: '',
463
+      smsCode: '',
464
+      type: 2,
465
+      uuid: '',
466
+      openId: ''
467
+    }
468
+    formRef.value?.clearValidate()
469
+  }
470
+}
471
+// 获取手机号
472
+const getphonenumber = (val: { detail: { code: string } }) => {
473
+  if (val.detail && val.detail.code) {
474
+    uni.showLoading()
475
+    phoneLoginApi({ code: val.detail.code }).then((res) => {
476
+      // 存储token
477
+      userStore.setToken(res?.data?.token)
478
+      setTimeout(() => {
479
+        refreshUserInfo()
480
+      }, 100)
481
+    })
482
+  }
483
+}
484
+// 取消绑定
485
+const cancelBind = () => {
486
+  isBind.value = 0
487
+}
488
+
489
+onLoad(async () => {
490
+  await systemStore.getConfigData()
491
+})
492
+onReady(() => {
493
+  formRef.value?.setRules?.(rules)
494
+})
495
+// 下拉刷新
496
+// onPullDownRefresh(async () => {
497
+//   await systemStore.getConfigData()
498
+//   uni.stopPullDownRefresh()
499
+// })
500
+</script>
501
+
502
+<template>
503
+  <view class="login-box">
504
+    <view class="login-top flex a-ccenter f20 white-color f-wrap pl20">
505
+      <view class="w100">您好,</view>
506
+      <view>欢迎使用{{ systemStore.config.name || '智慧园区管理系统' }}</view>
507
+    </view>
508
+    <view class="flex j-sb a-c w100 login-action">
509
+      <view v-if="type < 3" class="w50 tc f14" :class="[type === 1 && 'weight-600']" @click="changeType(1)">登录</view>
510
+      <view v-if="type < 3 && systemStore.config.register === 'true'" class="w50 tc f14" :class="[type === 2 && 'weight-600']" @click="changeType(2)"
511
+        >注册</view
512
+      >
513
+      <view v-if="type === 3" class="w50 tc f14 weight-600">忘记密码</view>
514
+    </view>
515
+    <view v-if="type === 1 || type === 3" class="triangle triangle-left"></view>
516
+    <view v-if="type === 2" class="triangle triangle-right"></view>
517
+    <view class="center-box">
518
+      <!-- 登录 -->
519
+      <uni-forms v-if="type === 1" ref="formRef" :model="form" :rules="rules" label-position="top" label-width="0">
520
+        <!-- 短信登录 -->
521
+        <template v-if="form.type === 1">
522
+          <uni-forms-item name="phonenumber">
523
+            <template #label>
524
+              <view class="form-title">
525
+                <uni-icons fontFamily="iconfont" color="#2E62AF" size="14">&#xe6d9;</uni-icons>
526
+                手机号
527
+              </view>
528
+            </template>
529
+            <uni-easyinput type="text" v-model="form.phonenumber" placeholder="请输入手机号" />
530
+          </uni-forms-item>
531
+          <uni-forms-item name="smsCode">
532
+            <template #label>
533
+              <view class="form-title">
534
+                <uni-icons fontFamily="iconfont" color="#2E62AF" size="14">&#xe619;</uni-icons>
535
+                验证码
536
+              </view>
537
+            </template>
538
+            <view class="flex j-sb a-center w100">
539
+              <uni-easyinput type="text" v-model="form.smsCode" placeholder="请输入验证码" />
540
+              <view v-if="isDisabled" class="sms-btn weight-600 gray-color">
541
+                {{ countdown + 's后重新获取' }}
542
+              </view>
543
+              <view v-else class="sms-btn weight-600 primary-color" @tap="getSmsCode(1)"> 获取验证码 </view>
544
+            </view>
545
+          </uni-forms-item>
546
+        </template>
547
+        <!-- 密码登录|微信绑定 -->
548
+        <template v-if="form.type === 2 || isBind">
549
+          <uni-forms-item name="username">
550
+            <template #label>
551
+              <view class="form-title">
552
+                <uni-icons fontFamily="iconfont" color="#2E62AF" size="14">&#xe6d9;</uni-icons>
553
+                手机号
554
+              </view>
555
+            </template>
556
+            <uni-easyinput type="text" v-model="form.username" placeholder="请输入手机号" @clear="handleClear" />
557
+          </uni-forms-item>
558
+          <uni-forms-item name="password">
559
+            <template #label>
560
+              <view class="form-title">
561
+                <uni-icons fontFamily="iconfont" color="#2E62AF" size="14">&#xe638;</uni-icons>
562
+                密码
563
+              </view>
564
+            </template>
565
+            <uni-easyinput type="password" v-model="form.password" placeholder="请输入密码" />
566
+          </uni-forms-item>
567
+          <uni-forms-item v-if="isShowCaptcha && systemStore.config.captcha === 'true' && !isBind" name="code">
568
+            <template #label>
569
+              <view class="form-title">
570
+                <uni-icons fontFamily="iconfont" color="#2E62AF" size="14">&#xe619;</uni-icons>
571
+                验证码
572
+              </view>
573
+            </template>
574
+            <Captcha ref="captchaRef" @getCaptchaInfo="getCaptchaInfo" class="w100" />
575
+          </uni-forms-item>
576
+        </template>
577
+      </uni-forms>
578
+      <!-- 注册 -->
579
+      <uni-forms v-if="type === 2" ref="registerRef" :model="registerForm" :rules="registerRules" label-position="top" label-width="0">
580
+        <uni-forms-item name="mobile">
581
+          <template #label>
582
+            <view class="form-title">
583
+              <uni-icons fontFamily="iconfont" color="#2E62AF" size="14">&#xe6d9;</uni-icons>
584
+              手机号
585
+            </view>
586
+          </template>
587
+          <uni-easyinput type="text" v-model="registerForm.mobile" placeholder="请输入手机号" />
588
+        </uni-forms-item>
589
+        <!-- <uni-forms-item name="username">
590
+          <template #label>
591
+            <view class="form-title">
592
+              <uni-icons type="person" color="#2E62AF" size="14" />
593
+              用户名
594
+            </view>
595
+          </template>
596
+          <uni-easyinput type="text" v-model="registerForm.username" placeholder="请输入用户名" />
597
+        </uni-forms-item> -->
598
+        <uni-forms-item name="realname">
599
+          <template #label>
600
+            <view class="form-title">
601
+              <uni-icons type="person-filled" color="#2E62AF" size="14" />
602
+              姓名
603
+            </view>
604
+          </template>
605
+          <uni-easyinput type="text" v-model="registerForm.realname" placeholder="请输入姓名" />
606
+        </uni-forms-item>
607
+        <uni-forms-item name="password">
608
+          <template #label>
609
+            <view class="form-title">
610
+              <uni-icons fontFamily="iconfont" color="#2E62AF" size="14">&#xe638;</uni-icons>
611
+              密码
612
+            </view>
613
+          </template>
614
+          <uni-easyinput type="password" v-model="registerForm.password" placeholder="请输入密码" />
615
+        </uni-forms-item>
616
+        <uni-forms-item name="passwordAgain">
617
+          <template #label>
618
+            <view class="form-title">
619
+              <uni-icons fontFamily="iconfont" color="#2E62AF" size="14">&#xe638;</uni-icons>
620
+              确认密码
621
+            </view>
622
+          </template>
623
+          <uni-easyinput type="password" v-model="registerForm.passwordAgain" placeholder="请再次输入密码" />
624
+        </uni-forms-item>
625
+        <uni-forms-item v-if="isShowCaptcha && systemStore.config.captcha === 'true'" name="code">
626
+          <template #label>
627
+            <view class="form-title">
628
+              <uni-icons fontFamily="iconfont" color="#2E62AF" size="14">&#xe619;</uni-icons>
629
+              验证码
630
+            </view>
631
+          </template>
632
+          <Captcha ref="captchaRef" @getCaptchaInfo="getCaptchaInfo" class="w100" />
633
+        </uni-forms-item>
634
+      </uni-forms>
635
+      <!-- 忘记密码 -->
636
+      <uni-forms v-if="type === 3" ref="pwdFormRef" :model="pwdForm" :rules="pwdRules" label-position="top" label-width="0">
637
+        <uni-forms-item name="phonenumber">
638
+          <template #label>
639
+            <view class="form-title">
640
+              <uni-icons fontFamily="iconfont" color="#2E62AF" size="14">&#xe6d9;</uni-icons>
641
+              手机号
642
+            </view>
643
+          </template>
644
+          <uni-easyinput type="text" v-model="pwdForm.phonenumber" placeholder="请输入手机号" />
645
+        </uni-forms-item>
646
+        <uni-forms-item name="smsCode">
647
+          <template #label>
648
+            <view class="form-title">
649
+              <uni-icons fontFamily="iconfont" color="#2E62AF" size="14">&#xe619;</uni-icons>
650
+              验证码
651
+            </view>
652
+          </template>
653
+          <view class="flex j-sb a-center w100">
654
+            <uni-easyinput type="text" v-model="pwdForm.smsCode" placeholder="请输入验证码" />
655
+            <view v-if="isDisabled" class="sms-btn weight-600 gray-color">
656
+              {{ countdown + 's后重新获取' }}
657
+            </view>
658
+            <view v-else class="sms-btn weight-600 primary-color" @tap="getSmsCode(2)"> 获取验证码 </view>
659
+          </view>
660
+        </uni-forms-item>
661
+        <uni-forms-item name="password">
662
+          <template #label>
663
+            <view class="form-title">
664
+              <uni-icons fontFamily="iconfont" color="#2E62AF" size="14">&#xe638;</uni-icons>
665
+              新密码
666
+            </view>
667
+          </template>
668
+          <uni-easyinput type="password" v-model="pwdForm.password" placeholder="请输入密码" />
669
+        </uni-forms-item>
670
+        <uni-forms-item name="passwordAgain">
671
+          <template #label>
672
+            <view class="form-title">
673
+              <uni-icons fontFamily="iconfont" color="#2E62AF" size="14">&#xe638;</uni-icons>
674
+              确认密码
675
+            </view>
676
+          </template>
677
+          <uni-easyinput type="password" v-model="pwdForm.passwordAgain" placeholder="请再次输入新密码" />
678
+        </uni-forms-item>
679
+      </uni-forms>
680
+      <!-- 登录操作 -->
681
+      <template v-if="type === 1">
682
+        <view v-if="systemStore.config.sms === 'true'" class="f12 mb20 mt10 primary-color weight-600 flex a-c j-sb">
683
+          <text @tap="loginTypeChange">{{ form.type === 2 ? '短信验证码登录' : '密码登录' }}</text>
684
+          <!-- <text @tap="handleForget">忘记密码</text> -->
685
+          <text @tap="changeType(3)">忘记密码</text>
686
+        </view>
687
+        <view class="btn-b gradual-primary" @tap="handleSubmit">{{ isBind ? '绑 定' : '登 录' }}</view>
688
+        <!-- #ifdef MP-WEIXIN -->
689
+        <view v-if="!isBind" class="tc m10 gray-color f12">OR</view>
690
+        <view v-if="!isBind" class="btn-b plain-bg primary-color mb10" @tap="handleUserProfile">微信一键登录</view>
691
+        <!-- #endif -->
692
+        <button v-if="!isBind" type="primary" open-type="getPhoneNumber" @getphonenumber="getphonenumber" class="btn-b plain-bg primary-color">
693
+          手机号登录
694
+        </button>
695
+        <view v-if="isBind" class="f12 mt20 primary-color weight-600" @click="cancelBind"
696
+          ><uni-icons type="left" size="12" color="#2e62af"></uni-icons>返回</view
697
+        >
698
+      </template>
699
+      <!-- 注册 -->
700
+      <view v-if="type === 2" class="btn-b gradual-primary" @tap="handleRegisterSubmit">确 定</view>
701
+      <!-- 忘记密码 -->
702
+      <template v-if="type === 3">
703
+        <view class="f12 mb20 mt10 primary-color weight-600 flex a-c j-sb">
704
+          <text @tap="changeType(1)">返回</text>
705
+        </view>
706
+        <view class="btn-b gradual-primary" @tap="handlePwdSubmit">确 定</view>
707
+      </template>
708
+    </view>
709
+    <!-- <view v-if="type === 1" class="gray-color f12 tc w100 agreement"> 登录即代表已阅读并同意<text class="primary-color">《用户协议》</text></view> -->
710
+  </view>
711
+</template>
712
+
713
+<style lang="scss" scoped>
714
+.login-box {
715
+  width: 100vw;
716
+  height: 100vh;
717
+  position: relative;
718
+  background: #fff;
719
+  .login-top {
720
+    height: 550rpx;
721
+    background: url('http://1.94.6.75:9000/zlhy-app/bg_activity.jpg') center no-repeat;
722
+    background-size: 100% 100%;
723
+  }
724
+  .login-action {
725
+    position: absolute;
726
+    left: 0;
727
+    top: 400rpx;
728
+    color: #fff;
729
+  }
730
+  :deep(.center-box) {
731
+    // width: calc(100vw - 100rpx);
732
+    width: 100%;
733
+    min-height: 200px;
734
+    border-radius: 50rpx;
735
+    background: #fff;
736
+    position: absolute;
737
+    left: 0;
738
+    top: 480rpx;
739
+    padding: 50rpx;
740
+    .btn-b {
741
+      width: 100% !important;
742
+    }
743
+    .is-input-border {
744
+      border-radius: 0;
745
+      border: 0;
746
+      border-bottom: 1px solid #dcdfe6;
747
+    }
748
+    .form-title {
749
+      font-size: 28rpx;
750
+      font-weight: bold;
751
+    }
752
+  }
753
+  .triangle {
754
+    width: 0;
755
+    height: 0;
756
+    border-left: 100rpx solid #fff;
757
+    border-top: 200rpx solid #fff;
758
+    border-right: 200rpx solid transparent;
759
+    border-bottom: 100rpx solid transparent;
760
+    transform: rotate(45deg);
761
+    border-top-left-radius: 5rpx;
762
+    position: absolute;
763
+    top: 520rpx;
764
+  }
765
+  .triangle-left {
766
+    left: 37rpx;
767
+  }
768
+  .triangle-right {
769
+    right: 37rpx;
770
+  }
771
+  .sms-btn {
772
+    height: 35px;
773
+    line-height: 35px;
774
+    border-bottom: 1px solid #dcdfe6;
775
+    border-radius: 0 !important;
776
+    font-size: 24rpx;
777
+  }
778
+  .forget {
779
+    .line {
780
+      width: 1px;
781
+      height: 10px;
782
+      background-color: #333;
783
+    }
784
+  }
785
+  .agreement {
786
+    position: absolute;
787
+    bottom: 50rpx;
788
+    left: 0;
789
+  }
790
+}
791
+</style>

+ 177 - 0
src/base/register.vue

@@ -0,0 +1,177 @@
1
+<!--
2
+ * @Author: wyd
3
+ * @Date: 2024-03
4
+ * @LastEditors: wyd
5
+ * @LastEditTime: 2024-05
6
+ * @Description: 注册
7
+-->
8
+<script setup lang="ts">
9
+import { ref } from 'vue'
10
+import { onLoad, onPullDownRefresh, onReady } from '@dcloudio/uni-app'
11
+import { system } from '@/stores/modules/system'
12
+import { useGlobal } from '@/composables'
13
+import { validatorMobile, verifyPwd } from '@/utils/verification'
14
+import { registerApi } from '@/api/login'
15
+import Captcha from '@/base/components/Captcha.vue'
16
+import type { captchaInfo } from '@/types/login'
17
+
18
+const systemStore = system()
19
+// 表单信息
20
+const form = ref({
21
+  // username: '',
22
+  realname: '',
23
+  password: '',
24
+  mobile: '',
25
+  uuid: '',
26
+  code: ''
27
+})
28
+// 表单校验
29
+const rules: UniHelper.UniFormsRules = {
30
+  // username: {
31
+  //   rules: [
32
+  //     {
33
+  //       required: true,
34
+  //       errorMessage: '请输入用户名'
35
+  //     },
36
+  //     {
37
+  //       maxLength: useGlobal().textMax,
38
+  //       errorMessage: '限制{maxLength}个字符'
39
+  //     }
40
+  //   ]
41
+  // },
42
+  realname: {
43
+    rules: [
44
+      {
45
+        required: true,
46
+        errorMessage: '请输入姓名'
47
+      },
48
+      {
49
+        maxLength: useGlobal().textMax,
50
+        errorMessage: '限制{maxLength}个字符'
51
+      }
52
+    ]
53
+  },
54
+  password: {
55
+    rules: [
56
+      {
57
+        required: true,
58
+        errorMessage: '请输入密码'
59
+      },
60
+      {
61
+        validateFunction: verifyPwd
62
+      }
63
+    ]
64
+  },
65
+  code: {
66
+    rules: [
67
+      {
68
+        required: true,
69
+        errorMessage: '请输入验证码'
70
+      }
71
+    ]
72
+  },
73
+  mobile: {
74
+    rules: [
75
+      {
76
+        required: true,
77
+        errorMessage: '请输入手机号'
78
+      },
79
+      {
80
+        validateFunction: validatorMobile
81
+      }
82
+    ]
83
+  }
84
+}
85
+// 表单组件实例
86
+const formRef = ref<UniHelper.UniFormsInstance>()
87
+
88
+// 是否展示验证码
89
+let isShowCaptcha: boolean = true
90
+const captchaRef = ref()
91
+const getCaptchaInfo = (val: captchaInfo) => {
92
+  isShowCaptcha = val.captchaEnabled
93
+  form.value.uuid = val.uuid
94
+  form.value.code = val.code
95
+}
96
+
97
+const handleSubmit = async () => {
98
+  formRef.value?.validate?.((err, formData) => {
99
+    if (!err) {
100
+      uni.showLoading()
101
+      registerApi(form.value)
102
+        .then(() => {
103
+          // 注册成功,返回上一页
104
+          uni.showToast({
105
+            title: '操作成功',
106
+            icon: 'none'
107
+          })
108
+          setTimeout(() => {
109
+            uni.navigateBack()
110
+          }, 1000)
111
+        })
112
+        .catch((err) => {
113
+          captchaRef.value?.getCaptchaImage()
114
+        })
115
+    }
116
+  })
117
+}
118
+onLoad(async () => {
119
+  await systemStore.getConfigData()
120
+})
121
+onReady(() => {
122
+  formRef.value?.setRules?.(rules)
123
+})
124
+// 下拉刷新
125
+onPullDownRefresh(async () => {
126
+  await systemStore.getConfigData()
127
+  uni.stopPullDownRefresh()
128
+})
129
+</script>
130
+
131
+<template>
132
+  <view class="login-box">
133
+    <view class="center-box p15 shadow">
134
+      <view class="tc f18 weight-600 black-color mb20">注册</view>
135
+      <uni-forms ref="formRef" :model="form" :rules="rules">
136
+        <uni-forms-item name="mobile">
137
+          <uni-easyinput type="text" v-model="form.mobile" placeholder="请输入手机号" />
138
+        </uni-forms-item>
139
+        <!-- <uni-forms-item name="username">
140
+          <uni-easyinput type="text" v-model="form.username" placeholder="请输入用户名" />
141
+        </uni-forms-item> -->
142
+        <uni-forms-item name="realname">
143
+          <uni-easyinput type="text" v-model="form.realname" placeholder="请输入姓名" />
144
+        </uni-forms-item>
145
+        <uni-forms-item name="password">
146
+          <uni-easyinput type="password" v-model="form.password" placeholder="请输入密码" />
147
+        </uni-forms-item>
148
+        <uni-forms-item v-if="isShowCaptcha && systemStore.config.captcha === 'true'" name="code">
149
+          <Captcha ref="captchaRef" @getCaptchaInfo="getCaptchaInfo" class="w100" />
150
+        </uni-forms-item>
151
+      </uni-forms>
152
+      <view class="btn-b warning-bg" @tap="handleSubmit">确 定</view>
153
+    </view>
154
+  </view>
155
+</template>
156
+
157
+<style lang="scss" scoped>
158
+.login-box {
159
+  width: 100vw;
160
+  height: 100vh;
161
+  background: url('http://1.94.6.75:9000/zlhy-app/bg_login.jpg') center no-repeat;
162
+  background-size: 100% 100%;
163
+  position: relative;
164
+  .center-box {
165
+    width: calc(100vw - 60px);
166
+    min-height: 200px;
167
+    border-radius: 10rpx;
168
+    background: rgba($color: #fff, $alpha: 0.8);
169
+    position: absolute;
170
+    left: 15px;
171
+    top: 20%;
172
+    .btn-b {
173
+      width: 100% !important;
174
+    }
175
+  }
176
+}
177
+</style>

+ 248 - 0
src/components/HyTabs.vue

@@ -0,0 +1,248 @@
1
+<template>
2
+  <view class="me-tabs" :class="{ 'tabs-fixed': fixed, isDouble }" :style="{ height: tabHeightVal, top: topFixed, 'margin-top': topMargin }">
3
+    <scroll-view v-if="tabs.length" :id="viewId" :scroll-left="scrollLeft" scroll-x scroll-with-animation :scroll-animation-duration="300">
4
+      <view class="tabs-item" :class="{ 'tabs-flex': !isScroll, 'tabs-scroll': isScroll }">
5
+        <!-- tab -->
6
+        <view
7
+          class="tab-item"
8
+          :style="{ width: tabWidthVal, height: tabHeightVal, 'line-height': tabHeightVal }"
9
+          v-for="(tab, i) in tabs"
10
+          :class="{ active: modelValue === i }"
11
+          :key="i"
12
+          @click="tabClick(i)"
13
+        >
14
+          {{ getTabName(tab) }}
15
+        </view>
16
+        <!-- 下划线 -->
17
+        <view class="tabs-line" :style="{ left: lineLeft }"></view>
18
+      </view>
19
+    </scroll-view>
20
+  </view>
21
+</template>
22
+
23
+<script>
24
+export default {
25
+  props: {
26
+    tabs: {
27
+      type: Array,
28
+      default() {
29
+        return []
30
+      }
31
+    },
32
+    nameKey: {
33
+      // 取name的字段
34
+      type: String,
35
+      default: 'name'
36
+    },
37
+    modelValue: {
38
+      // 当前显示的下标 (使用v-model语法糖: 1.props需为modelValue; 2.需回调input事件)
39
+      type: [String, Number],
40
+      default: 0
41
+    },
42
+    fixed: Boolean, // 是否悬浮,默认false
43
+    tabWidth: Number, // 每个tab的宽度,默认不设置值,为flex平均分配; 如果指定宽度,则不使用flex,每个tab居左,超过则水平滑动(单位默认rpx)
44
+    height: {
45
+      // 高度,单位rpx
46
+      type: Number,
47
+      default: 80
48
+    },
49
+    top: {
50
+      // 顶部偏移的距离,默认单位rpx (当fixed=true时,已加上windowTop)
51
+      type: Number,
52
+      default: 0
53
+    },
54
+    // 是否是第二层选项卡
55
+    isDouble: {
56
+      type: Boolean,
57
+      default: false
58
+    }
59
+  },
60
+  data() {
61
+    return {
62
+      viewId: 'id_' + Math.random().toString(36).substr(2, 16),
63
+      scrollLeft: 0,
64
+      windowWidth: 0,
65
+      windowTop: 0
66
+    }
67
+  },
68
+  computed: {
69
+    isScroll() {
70
+      return this.tabWidth && this.tabs.length // 指定了tabWidth的宽度,则支持水平滑动
71
+    },
72
+    tabHeightPx() {
73
+      return uni.upx2px(this.height)
74
+    },
75
+    tabHeightVal() {
76
+      return this.tabHeightPx + 'px'
77
+    },
78
+    tabWidthPx() {
79
+      return uni.upx2px(this.tabWidth)
80
+    },
81
+    tabWidthVal() {
82
+      return this.isScroll ? this.tabWidthPx + 'px' : ''
83
+    },
84
+    lineLeft() {
85
+      if (this.isScroll) {
86
+        return this.tabWidthPx * this.modelValue + this.tabWidthPx / 2 + 'px' // 需转为px (用rpx的话iOS真机显示有误差)
87
+      } else {
88
+        return (100 / this.tabs.length) * (this.modelValue + 1) - 100 / (this.tabs.length * 2) + '%'
89
+      }
90
+    },
91
+    topFixed() {
92
+      return this.fixed ? this.windowTop + uni.upx2px(this.top) + 'px' : 0
93
+    },
94
+    topMargin() {
95
+      return this.fixed ? 0 : this.top + 'rpx'
96
+    }
97
+  },
98
+  watch: {
99
+    tabs() {
100
+      // 水平滚动到中间
101
+      this.initWarpRect(() => {
102
+        this.scrollCenter()
103
+      })
104
+    },
105
+    //modelValue() {
106
+    //  this.scrollCenter() // 水平滚动到中间
107
+    //}
108
+    modelValue: {
109
+      handler(val) {
110
+        this.scrollCenter() // 水平滚动到中间
111
+      },
112
+      deep: true,
113
+      immediate: true
114
+    }
115
+  },
116
+  created() {
117
+    let sys = uni.getSystemInfoSync()
118
+    this.windowWidth = sys.windowWidth
119
+    this.windowTop = sys.windowTop
120
+  },
121
+  mounted() {
122
+    // 滚动到当前下标
123
+    this.initWarpRect(() => {
124
+      this.scrollCenter()
125
+    })
126
+  },
127
+  methods: {
128
+    getTabName(tab) {
129
+      return typeof tab === 'object' ? tab[this.nameKey] : tab
130
+    },
131
+    tabClick(i) {
132
+      if (this.modelValue != i) {
133
+        this.$emit('update:modelValue', i)
134
+        this.$emit('change', i)
135
+      }
136
+    },
137
+    scrollCenter() {
138
+      if (!this.isScroll) return
139
+      let tabLeft = this.tabWidthPx * this.modelValue + this.tabWidthPx / 2 // 当前tab中心点到左边的距离
140
+      let diff = tabLeft - this.warpWidth / 2 // 如果超过tabs容器的一半,则滚动差值
141
+      this.scrollLeft = diff
142
+    },
143
+    initWarpRect(success) {
144
+      setTimeout(() => {
145
+        // 延时确保dom已渲染, 不使用$nextclick
146
+        let query = uni.createSelectorQuery().in(this)
147
+        query
148
+          .select('#' + this.viewId)
149
+          .boundingClientRect((rect) => {
150
+            this.warpWidth = rect ? rect.width : this.windowWidth // 某些情况下取不到宽度,暂时取屏幕宽度
151
+            success()
152
+          })
153
+          .exec()
154
+      }, 20)
155
+    }
156
+  }
157
+}
158
+</script>
159
+
160
+<style lang="scss">
161
+.me-tabs.isDouble {
162
+  padding-top: 10px !important;
163
+  background-color: #f8f8f8 !important;
164
+  border: none !important;
165
+  .tabs-flex {
166
+    justify-content: center !important;
167
+  }
168
+  .tabs-line {
169
+    display: none !important;
170
+  }
171
+  .tabs-item {
172
+    .tab-item {
173
+      flex: unset !important;
174
+      width: 43% !important;
175
+      border-radius: 10rpx !important;
176
+      font-size: 24rpx !important;
177
+      background-color: #fff !important;  
178
+      height: 30px !important;
179
+      line-height: 30px !important;
180
+      margin: 0 10px;
181
+      &.active {
182
+        background-color: #00c1ab !important;
183
+        color: #fff !important;
184
+      }
185
+    }
186
+  }
187
+}
188
+.me-tabs {
189
+  position: relative;
190
+  font-size: 28rpx;
191
+  background-color: #fff;
192
+  border-bottom: 1rpx solid #eee;
193
+  box-sizing: border-box;
194
+  overflow-y: hidden;
195
+  background-color: #fff;
196
+  &.tabs-fixed {
197
+    z-index: 990;
198
+    position: fixed;
199
+    left: 0;
200
+    width: 100%;
201
+  }
202
+
203
+  .tabs-item {
204
+    position: relative;
205
+    white-space: nowrap;
206
+    padding-bottom: 30rpx; // 撑开高度,再配合me-tabs的overflow-y: hidden,以达到隐藏滚动条的目的
207
+    box-sizing: border-box;
208
+    .tab-item {
209
+      position: relative;
210
+      text-align: center;
211
+      box-sizing: border-box;
212
+      font-size: 28rpx;
213
+      &.active {
214
+        font-weight: 600;
215
+        color: #00c1ab;
216
+        font-size: 28rpx;
217
+      }
218
+    }
219
+  }
220
+
221
+  // 平分的方式显示item
222
+  .tabs-flex {
223
+    display: flex;
224
+    .tab-item {
225
+      flex: 1;
226
+    }
227
+  }
228
+  // 居左显示item,支持水平滑动
229
+  .tabs-scroll {
230
+    .tab-item {
231
+      display: inline-block;
232
+    }
233
+  }
234
+
235
+  // 选中tab的线
236
+  .tabs-line {
237
+    z-index: 1;
238
+    position: absolute;
239
+    bottom: 30rpx; // 至少与.tabs-item的padding-bottom一致,才能保证在底部边缘
240
+    width: 50rpx;
241
+    height: 6rpx;
242
+    transform: translateX(-50%);
243
+    border-radius: 4rpx;
244
+    transition: left 0.3s;
245
+    background: #00c1ab;
246
+  }
247
+}
248
+</style>

+ 463 - 0
src/composables/index.ts

@@ -0,0 +1,463 @@
1
+import { user } from '@/stores/modules/user'
2
+import { judementSameArr } from '@/utils'
3
+
4
+export const useGlobal = () => {
5
+  const userStore = user()
6
+  // 文件路径前缀
7
+  const pathPre: string = import.meta.env.VITE_OSS_URL + import.meta.env.VITE_BARREL_URL
8
+  // 整数最大值
9
+  const numberMax: number = 9999999999
10
+  // 小数最大值
11
+  const decimalMax: number = 9999999999.99
12
+  // 多行文本框最大值
13
+  const textareaMax: number = 500
14
+  // 文本框最大值
15
+  const textMax: number = 100
16
+  /**
17
+   * 单个权限验证
18
+   * @param value 权限值
19
+   * @returns 有权限,返回 `true`,反之则反false
20
+   */
21
+  const auth = (value: string): boolean => {
22
+    if (!value) return true
23
+    if (userStore.info && +userStore.info!.id === 1) return true // 系统管理员拥有所有权限
24
+    if (userStore.permissions.length) {
25
+      return userStore.permissions.some((v: string) => v === value)
26
+    }
27
+    return true
28
+  }
29
+  /**
30
+   * 多个权限验证,满足一个则为 true
31
+   * @param value 权限值
32
+   * @returns 有权限,返回 `true`,反之则反
33
+   */
34
+  const auths = (value: Array<string>): boolean => {
35
+    if (!value || !value.length) return true
36
+    if (userStore.info && +userStore.info!.id === 1) return true // 系统管理员拥有所有权限
37
+    let flag = false
38
+    userStore.permissions.map((val: string) => {
39
+      value.map((v: string) => {
40
+        if (val === v) flag = true
41
+      })
42
+    })
43
+    return flag
44
+  }
45
+
46
+  /**
47
+   * 多个权限验证,全部满足则为 true
48
+   * @param value 权限值
49
+   * @returns 有权限,返回 `true`,反之则反
50
+   */
51
+  const authAll = (value: Array<string>): boolean => {
52
+    if (!value || !value.length) return true
53
+    if (userStore.info && +userStore.info!.id === 1) return true // 系统管理员拥有所有权限
54
+    return judementSameArr(value, userStore.permissions)
55
+  }
56
+
57
+  // 页面跳转
58
+  const routeTo = (url: string, permissions?: string) => {
59
+    if (!url) return
60
+    if (permissions && !auth(permissions)) {
61
+      uni.showToast({
62
+        title: '暂无权限',
63
+        icon: 'none'
64
+      })
65
+      return
66
+    }
67
+    // #ifdef H5
68
+    url.indexOf('http') > 0 && (window.location.href = url)
69
+    // #endif
70
+    uni.navigateTo<UniApp.NavigateToOptions>({
71
+      url,
72
+      fail() {
73
+        uni.switchTab({ url })
74
+      }
75
+    })
76
+  }
77
+  // 轻提示
78
+  const tips = (content: string) => {
79
+    if (!content) return
80
+    uni.showToast({
81
+      title: content,
82
+      icon: 'none'
83
+    })
84
+  }
85
+
86
+  /* 数组匹配 */
87
+  const handleMate = (data: string | number, list: EmptyArrayType): string => {
88
+    if (!data) return ''
89
+    if (!list) return ''
90
+    const value = list.filter((item) => {
91
+      return item.id === data || item.value === data || item.dictValue === String(data)
92
+    })
93
+    if (value && value.length) {
94
+      return value[0].text || value[0].name || value[0].label || value[0].dictLabel
95
+    }
96
+    return ''
97
+  }
98
+  //  单选是否
99
+  const radio = [
100
+    {
101
+      text: '是',
102
+      value: 1
103
+    },
104
+    {
105
+      text: '否',
106
+      value: 2
107
+    }
108
+  ]
109
+
110
+  // 格式化时间
111
+  const formatDate = (time: string) => {
112
+    if (!time) return ''
113
+    const date = new Date()
114
+    const year = date.getFullYear()
115
+    const m = date.getMonth() + 1
116
+    const d = date.getDate()
117
+    const curYear = time.slice(0, 4)
118
+    const curDay = addZero(m) + '-' + addZero(d)
119
+    if (+curYear === year) {
120
+      if (time.indexOf(curDay) !== -1) {
121
+        // 当天返回 时:分
122
+        return '今日 ' + time.slice(11, 16)
123
+      }
124
+      // 当年返回 月-日 时:分
125
+      return time.slice(5, 16)
126
+    }
127
+    // 其他返回 年-月-日 时:分
128
+    return time.slice(0, 16)
129
+  }
130
+  const addZero = (s: number) => {
131
+    return s < 10 ? '0' + s : s
132
+  }
133
+  // 分享页面
134
+  const sharePage = () => {
135
+    wx.showShareMenu({
136
+      withShareTicket: true,
137
+      menus: ['shareAppMessage', 'shareTimeline']
138
+    })
139
+  }
140
+  return {
141
+    pathPre,
142
+    numberMax,
143
+    decimalMax,
144
+    textareaMax,
145
+    textMax,
146
+    radio,
147
+    auth,
148
+    auths,
149
+    authAll,
150
+    handleMate,
151
+    routeTo,
152
+    tips,
153
+    formatDate,
154
+    sharePage
155
+  }
156
+}
157
+
158
+// 图表
159
+export const useChart = () => {
160
+  //#ifdef MP-ALIPAY || MP-WEIXIN
161
+  const pixelRatio = uni.getSystemInfoSync().pixelRatio
162
+  //#endif
163
+  const optsGauge = {
164
+    title: {
165
+      name: '',
166
+      fontSize: 25,
167
+      color: '#2fc25b',
168
+      offsetY: 0
169
+    },
170
+    subtitle: {
171
+      name: '',
172
+      fontSize: 15,
173
+      color: '#1890ff',
174
+      offsetY: 0
175
+    },
176
+    extra: {
177
+      gauge: {
178
+        type: 'progress',
179
+        width: 30,
180
+        labelColor: '#666666',
181
+        startAngle: 0.75,
182
+        endAngle: 0.25,
183
+        startNumber: 0,
184
+        endNumber: 100,
185
+        labelFormat: '',
186
+        splitLine: {
187
+          fixRadius: -10,
188
+          splitNumber: 10,
189
+          width: 15,
190
+          color: '#FFFFFF',
191
+          childNumber: 5,
192
+          childWidth: 12
193
+        },
194
+        pointer: {
195
+          width: 34,
196
+          color: 'auto'
197
+        }
198
+      }
199
+    }
200
+  }
201
+  const optsArcbarPie = {
202
+    title: {
203
+      name: '',
204
+      fontSize: 26,
205
+      color: '#FA7D8D'
206
+    },
207
+    subtitle: {
208
+      name: ''
209
+    },
210
+    extra: {
211
+      arcbar: {
212
+        type: 'circle',
213
+        width: 16,
214
+        backgroundColor: '#E9E9E9',
215
+        startAngle: 1.5,
216
+        endAngle: 0.25,
217
+        gap: 2,
218
+        linearType: 'custom',
219
+        customColor: ['#EB88E2']
220
+      },
221
+      tooltip: {
222
+        showCategory: true,
223
+        showArrow: false
224
+      }
225
+    }
226
+  }
227
+  const optsRadar = {
228
+    legend: {
229
+      show: false
230
+    },
231
+    extra: {
232
+      radar: {
233
+        gridType: 'radar',
234
+        gridColor: '#CCCCCC',
235
+        gridCount: 3,
236
+        opacity: 0.2,
237
+        border: true
238
+      },
239
+      tooltip: {
240
+        showCategory: true,
241
+        showArrow: false
242
+      }
243
+    }
244
+  }
245
+  const optStack = {
246
+    padding: [15, 15, 15, 0],
247
+    dataLabel: false,
248
+    enableScroll: true,
249
+    legend: {
250
+      padding: 10,
251
+      lineHeight: 25
252
+    },
253
+    xAxis: {
254
+      scrollShow: true,
255
+      disableGrid: true,
256
+      itemCount: 3,
257
+      scrollColor: '#ccc',
258
+      format: 'xAxisDemo3'
259
+    },
260
+    yAxis: {
261
+      gridType: 'dash',
262
+      dashLength: 2,
263
+      splitNumber: 1,
264
+      min: 0,
265
+      format: 'xAxisDemo3'
266
+    },
267
+    extra: {
268
+      column: {
269
+        type: 'stack',
270
+        width: 20,
271
+        activeBgColor: '#000000',
272
+        activeBgOpacity: 0.08,
273
+        labelPosition: 'center'
274
+      }
275
+    }
276
+  }
277
+  const optBar = {
278
+    type: 'bar',
279
+    padding: [15, 50, 15, 0],
280
+    legend: {
281
+      padding: 10,
282
+      lineHeight: 25
283
+    },
284
+    dataLabel: false,
285
+    xAxis: {
286
+      boundaryGap: 'justify',
287
+      disableGrid: false,
288
+      min: 0,
289
+      axisLine: false,
290
+      splitNumber: 1,
291
+      gridType: 'dash'
292
+    },
293
+    yAxis: {
294
+      format: 'xAxisDemo3'
295
+    },
296
+    extra: {
297
+      bar: {
298
+        type: 'stack',
299
+        width: 15,
300
+        meterBorde: 1,
301
+        meterFillColor: '#FFFFFF',
302
+        activeBgColor: '#000000',
303
+        activeBgOpacity: 0.08,
304
+        linearType: 'custom',
305
+        categoryGap: 2
306
+      },
307
+      tooltip: {
308
+        showCategory: true,
309
+        showArrow: false
310
+      }
311
+    }
312
+  }
313
+  const optsPie = {
314
+    type: 'pie',
315
+    //#ifdef MP-ALIPAY || MP-WEIXIN
316
+    pixelRatio,
317
+    //#endif
318
+    animation: true,
319
+    background: '#FFFFFF',
320
+    color: ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'],
321
+    padding: [5, 5, 5, 5],
322
+    fontSize: 12,
323
+    legend: {
324
+      padding: 10,
325
+      lineHeight: 25
326
+    },
327
+    extra: {
328
+      pie: {
329
+        activeOpacity: 0.5,
330
+        activeRadius: 10,
331
+        offsetAngle: 0,
332
+        labelWidth: 5,
333
+        border: false
334
+      }
335
+    }
336
+  }
337
+  const optsRing = {
338
+    title: {
339
+      name: ''
340
+    },
341
+    subtitle: {
342
+      name: ''
343
+    },
344
+    background: '#FFFFFF',
345
+    rotate: false,
346
+    rotateLock: false,
347
+    color: ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'],
348
+    padding: [5, 5, 5, 5],
349
+    fontSize: 12,
350
+    legend: {
351
+      padding: 10,
352
+      lineHeight: 25
353
+    },
354
+    extra: {
355
+      ring: {
356
+        ringWidth: 20,
357
+        activeOpacity: 0.5,
358
+        activeRadius: 10,
359
+        offsetAngle: 0,
360
+        labelWidth: 15,
361
+        format: 'xAxisDemo3',
362
+        customRadius: 60
363
+      }
364
+    }
365
+  }
366
+  const optsRose = {
367
+    type: 'rose',
368
+    //#ifdef MP-ALIPAY || MP-WEIXIN
369
+    pixelRatio,
370
+    //#endif
371
+    animation: true,
372
+    background: '#FFFFFF',
373
+    color: ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'],
374
+    padding: [5, 5, 5, 5],
375
+    fontSize: 12,
376
+    legend: {
377
+      padding: 10,
378
+      lineHeight: 25,
379
+      position: 'bottom'
380
+    },
381
+    extra: {
382
+      rose: {
383
+        type: 'area',
384
+        minRadius: 50,
385
+        activeOpacity: 0.5,
386
+        activeRadius: 10,
387
+        offsetAngle: 0,
388
+        labelWidth: 15,
389
+        border: false,
390
+        borderWidth: 2,
391
+        borderColor: '#FFFFFF'
392
+      }
393
+    }
394
+  }
395
+  const optFunnel = {
396
+    type: 'funnel',
397
+    //#ifdef MP-ALIPAY || MP-WEIXIN
398
+    pixelRatio,
399
+    //#endif
400
+    animation: true,
401
+    background: '#FFFFFF',
402
+    color: ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'],
403
+    padding: [5, 15, 5, 5],
404
+    fontSize: 12,
405
+    legend: {
406
+      padding: 10,
407
+      lineHeight: 25
408
+    },
409
+    extra: {
410
+      funnel: {
411
+        activeOpacity: 0.3,
412
+        activeWidth: 10,
413
+        border: true,
414
+        borderWidth: 2,
415
+        borderColor: '#FFFFFF',
416
+        fillOpacity: 1,
417
+        labelAlign: 'right'
418
+      },
419
+      tooltip: {
420
+        showCategory: true
421
+      }
422
+    }
423
+  }
424
+  const optLine = {
425
+    enableScroll: true,
426
+    dataLabel: false,
427
+    xAxis: {
428
+      scrollShow: true,
429
+      disableGrid: true,
430
+      itemCount: 3,
431
+      scrollColor: '#ccc',
432
+      format: 'xAxisDemo3'
433
+    },
434
+    yAxis: {
435
+      gridType: 'dash',
436
+      dashLength: 2,
437
+      splitNumber: 1,
438
+      format: 'xAxisDemo3'
439
+    },
440
+    extra: {
441
+      line: {
442
+        type: 'straight',
443
+        width: 2
444
+      },
445
+      tooltip: {
446
+        showCategory: true,
447
+        showArrow: false
448
+      }
449
+    }
450
+  }
451
+  return {
452
+    optsGauge,
453
+    optsArcbarPie,
454
+    optsRadar,
455
+    optStack,
456
+    optBar,
457
+    optsPie,
458
+    optsRing,
459
+    optsRose,
460
+    optFunnel,
461
+    optLine
462
+  }
463
+}

+ 40 - 0
src/directive/authDirective.ts

@@ -0,0 +1,40 @@
1
+import type { App } from 'vue'
2
+import { user } from '@/stores'
3
+import { judementSameArr } from '@/utils'
4
+
5
+/**
6
+ * 用户权限指令
7
+ * @directive 单个权限验证(v-auth="xxx")
8
+ * @directive 多个权限验证,满足一个则显示(v-auths="[xxx,xxx]")
9
+ * @directive 多个权限验证,全部满足则显示(v-auth-all="[xxx,xxx]")
10
+ */
11
+export function authDirective(app: App) {
12
+  // 单个权限验证(v-auth="xxx")
13
+  app.directive('auth', {
14
+    mounted(el, binding) {
15
+      const stores = user()
16
+      if (!stores.permissions.some((v: string) => v === binding.value)) el.parentNode.removeChild(el)
17
+    }
18
+  })
19
+  // 多个权限验证,满足一个则显示(v-auths="[xxx,xxx]")
20
+  app.directive('auths', {
21
+    mounted(el, binding) {
22
+      let flag = false
23
+      const stores = user()
24
+      stores.permissions.map((val: string) => {
25
+        binding.value.map((v: string) => {
26
+          if (val === v) flag = true
27
+        })
28
+      })
29
+      if (!flag) el.parentNode.removeChild(el)
30
+    }
31
+  })
32
+  // 多个权限验证,全部满足则显示(v-auth-all="[xxx,xxx]")
33
+  app.directive('auth-all', {
34
+    mounted(el, binding) {
35
+      const stores = user()
36
+      const flag = judementSameArr(binding.value, stores.permissions)
37
+      if (!flag) el.parentNode.removeChild(el)
38
+    }
39
+  })
40
+}

+ 7 - 0
src/directive/index.ts

@@ -0,0 +1,7 @@
1
+import type { App } from 'vue'
2
+import { authDirective } from '@/directive/authDirective'
3
+
4
+export function directive(app: App) {
5
+  // 用户权限指令
6
+  authDirective(app)
7
+}

+ 22 - 0
src/main.js

@@ -0,0 +1,22 @@
1
+import { createSSRApp } from 'vue'
2
+import pinia from './stores'
3
+import App from './App.vue'
4
+import resetStore from './stores/reset'
5
+import uView from './uni_modules/vk-uview-ui'
6
+
7
+// #ifdef H5
8
+import quill from 'quill'
9
+window.Quill = quill
10
+// #endif
11
+
12
+export function createApp() {
13
+  const app = createSSRApp(App)
14
+
15
+  pinia.use(resetStore)
16
+  app.use(pinia)
17
+  app.use(uView)
18
+
19
+  return {
20
+    app
21
+  }
22
+}

+ 82 - 0
src/manifest.json

@@ -0,0 +1,82 @@
1
+{
2
+    "name" : "孵化园区",
3
+    "appid" : "__UNI__D113836",
4
+    "description" : "",
5
+    "versionName" : "1.0.0",
6
+    "versionCode" : "100",
7
+    "transformPx" : false,
8
+    /* 5+App特有相关 */
9
+    "app-plus" : {
10
+        "usingComponents" : true,
11
+        "nvueStyleCompiler" : "uni-app",
12
+        "compilerVersion" : 3,
13
+        "splashscreen" : {
14
+            "alwaysShowBeforeRender" : true,
15
+            "waiting" : true,
16
+            "autoclose" : true,
17
+            "delay" : 0
18
+        },
19
+        /* 模块配置 */
20
+        "modules" : {},
21
+        /* 应用发布信息 */
22
+        "distribute" : {
23
+            /* android打包配置 */
24
+            "android" : {
25
+                "permissions" : [
26
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
27
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
28
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
29
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
30
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
31
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
32
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
33
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
34
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
35
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
36
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
37
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
38
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
39
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
40
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
41
+                ]
42
+            },
43
+            /* ios打包配置 */
44
+            "ios" : {
45
+                "dSYMs" : false
46
+            },
47
+            /* SDK配置 */
48
+            "sdkConfigs" : {
49
+                "ad" : {}
50
+            }
51
+        }
52
+    },
53
+    /* 快应用特有相关 */
54
+    "quickapp" : {},
55
+    "h5": {
56
+      "router": {
57
+        "base": "./"
58
+      }
59
+    },
60
+    /* 小程序特有相关 */
61
+    "mp-weixin" : {
62
+        "appid" : "wxa04f62d4bb823b36",
63
+        "setting" : {
64
+            "urlCheck" : false,
65
+            "minified" : true
66
+        },
67
+        "usingComponents" : true
68
+    },
69
+    "mp-alipay" : {
70
+        "usingComponents" : true
71
+    },
72
+    "mp-baidu" : {
73
+        "usingComponents" : true
74
+    },
75
+    "mp-toutiao" : {
76
+        "usingComponents" : true
77
+    },
78
+    "uniStatistics" : {
79
+        "enable" : false
80
+    },
81
+    "vueVersion" : "3"
82
+}

文件差異過大導致無法顯示
+ 1026 - 0
src/menu/index.js


+ 95 - 0
src/pages.json

@@ -0,0 +1,95 @@
1
+{
2
+  "pages": [
3
+    {
4
+      "path": "pages/index",
5
+      "style": {
6
+        "navigationBarTitleText": "首页",
7
+        "enablePullDownRefresh": false,
8
+        "navigationStyle": "custom"
9
+      }
10
+    },
11
+    {
12
+      "path": "pages/service",
13
+      "style": {
14
+        "navigationBarTitleText": "服务区",
15
+        "enablePullDownRefresh": false
16
+      }
17
+    },
18
+    {
19
+      "path": "pages/my",
20
+      "style": {
21
+        "navigationBarTitleText": "我的",
22
+        "enablePullDownRefresh": false,
23
+        "navigationStyle": "custom"
24
+      }
25
+    }
26
+  ],
27
+  "subPackages": [
28
+    {
29
+      "root": "base",
30
+      "pages": [
31
+        {
32
+          "path": "login",
33
+          "style": {
34
+            "navigationBarTitleText": "登录",
35
+            "enablePullDownRefresh": false,
36
+            "navigationStyle": "custom"
37
+          }
38
+        },
39
+        {
40
+          "path": "register",
41
+          "style": {
42
+            "navigationBarTitleText": "注册",
43
+            "enablePullDownRefresh": true
44
+          }
45
+        },
46
+        {
47
+          "path": "forget",
48
+          "style": {
49
+            "navigationBarTitleText": "忘记密码",
50
+            "enablePullDownRefresh": false
51
+          }
52
+        }
53
+      ]
54
+    }
55
+  ],
56
+  "easycom": {
57
+    "autoscan": true,
58
+    "custom": {
59
+      "^u-(.*)": "@/uni_modules/vk-uview-ui/components/u-$1/u-$1.vue",
60
+      "^Hy(.*)": "@/components/Hy$1.vue"
61
+    }
62
+  },
63
+  "globalStyle": {
64
+    "navigationBarTextStyle": "black",
65
+    "navigationBarTitleText": "沈海高速",
66
+    "navigationBarBackgroundColor": "#fff",
67
+    "backgroundColor": "#fff"
68
+  },
69
+  "tabBar": {
70
+    "color": "#333",
71
+    "selectedColor": "#2064d1",
72
+    "backgroundColor": "#fff",
73
+    "borderStyle": "white",
74
+    "list": [
75
+      {
76
+        "text": "首页",
77
+        "pagePath": "pages/index",
78
+        "iconPath": "static/images/home.png",
79
+        "selectedIconPath": "static/images/home_active.png"
80
+      },
81
+      {
82
+        "text": "服务区",
83
+        "pagePath": "pages/service",
84
+        "iconPath": "static/images/service.png",
85
+        "selectedIconPath": "static/images/service_active.png"
86
+      },
87
+      {
88
+        "text": "我的",
89
+        "pagePath": "pages/my",
90
+        "iconPath": "static/images/my.png",
91
+        "selectedIconPath": "static/images/my_active.png"
92
+      }
93
+    ]
94
+  }
95
+}

+ 41 - 0
src/pages/components/NotLogin.vue

@@ -0,0 +1,41 @@
1
+<!--
2
+ * @page: 未登录组件
3
+ * @Author: wangyadi
4
+ * @Date: 2022-12-02 15:38:36
5
+ * @LastEditors: wyd
6
+-->
7
+<script setup>
8
+const goToLogin = () => {
9
+  uni.navigateTo({
10
+    url: '/base/login'
11
+  })
12
+}
13
+defineProps({
14
+  title: {
15
+    type: String,
16
+    default: '登录后查看更多'
17
+  }
18
+})
19
+</script>
20
+<template>
21
+  <!-- 未登录 -->
22
+  <view class="not-login">
23
+    <image src="http://1.94.6.75:9000/zlhy-app/faq-illustration.png" mode="aspectFill" class="not-login-img" />
24
+    <view class="mt20 mb20 w100 tc title-sub">{{ title }}</view>
25
+    <view size="mini" @click="goToLogin" class="btn-round gradual-primary">登录/注册</view>
26
+  </view>
27
+</template>
28
+
29
+<style lang="scss" scoped>
30
+.not-login {
31
+  min-height: 100vh;
32
+  display: flex;
33
+  flex-wrap: wrap;
34
+  justify-content: center;
35
+  align-content: center;
36
+  .not-login-img {
37
+    width: 300rpx;
38
+    height: 230rpx;
39
+  }
40
+}
41
+</style>

+ 8 - 0
src/pages/index.vue

@@ -0,0 +1,8 @@
1
+<script setup lang="ts">
2
+</script>
3
+
4
+<template>
5
+</template>
6
+
7
+<style lang="scss" scoped>
8
+</style>

+ 8 - 0
src/pages/my.vue

@@ -0,0 +1,8 @@
1
+<script setup lang="ts">
2
+</script>
3
+
4
+<template>
5
+</template>
6
+
7
+<style lang="scss" scoped>
8
+</style>

+ 7 - 0
src/pages/service.vue

@@ -0,0 +1,7 @@
1
+<script setup lang="ts"></script>
2
+
3
+<template>
4
+  <view></view>
5
+</template>
6
+
7
+<style lang="scss" scoped></style>

+ 29 - 0
src/project.config.json

@@ -0,0 +1,29 @@
1
+{
2
+  "appid": "wxa04f62d4bb823b36",
3
+  "compileType": "miniprogram",
4
+  "libVersion": "3.3.5",
5
+  "packOptions": {
6
+    "ignore": [],
7
+    "include": []
8
+  },
9
+  "setting": {
10
+    "coverView": true,
11
+    "es6": true,
12
+    "postcss": true,
13
+    "minified": true,
14
+    "enhance": true,
15
+    "showShadowRootInWxmlPanel": true,
16
+    "packNpmRelationList": [],
17
+    "babelSetting": {
18
+      "ignore": [],
19
+      "disablePlugins": [],
20
+      "outputPath": ""
21
+    },
22
+    "condition": false
23
+  },
24
+  "condition": {},
25
+  "editorSetting": {
26
+    "tabIndent": "insertSpaces",
27
+    "tabSize": 4
28
+  }
29
+}

+ 7 - 0
src/project.private.config.json

@@ -0,0 +1,7 @@
1
+{
2
+    "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
3
+    "projectname": "src",
4
+    "setting": {
5
+        "compileHotReLoad": true
6
+    }
7
+}

+ 6 - 0
src/shime-uni.d.ts

@@ -0,0 +1,6 @@
1
+export {}
2
+
3
+declare module 'vue' {
4
+  type Hooks = App.AppInstance & Page.PageInstance
5
+  interface ComponentCustomOptions extends Hooks {}
6
+}

文件差異過大導致無法顯示
+ 238 - 0
src/static/editor/editor-icon.css


二進制
src/static/editor/iconfont.ttf


+ 228 - 0
src/static/emoji.js

@@ -0,0 +1,228 @@
1
+const data = [
2
+  [
3
+    {
4
+      codes: '1F600',
5
+      char: '😀',
6
+      name: 'grinning face'
7
+    },
8
+    {
9
+      codes: '1F603',
10
+      char: '😃',
11
+      name: 'grinning face with big eyes'
12
+    },
13
+    {
14
+      codes: '1F604',
15
+      char: '😄',
16
+      name: 'grinning face with smiling eyes'
17
+    },
18
+    {
19
+      codes: '1F601',
20
+      char: '😁',
21
+      name: 'beaming face with smiling eyes'
22
+    },
23
+    {
24
+      codes: '1F606',
25
+      char: '😆',
26
+      name: 'grinning squinting face'
27
+    },
28
+    {
29
+      codes: '1F605',
30
+      char: '😅',
31
+      name: 'grinning face with sweat'
32
+    },
33
+    {
34
+      codes: '1F923',
35
+      char: '🤣',
36
+      name: 'rolling on the floor laughing'
37
+    },
38
+    {
39
+      codes: '1F602',
40
+      char: '😂',
41
+      name: 'face with tears of joy'
42
+    },
43
+    {
44
+      codes: '1F642',
45
+      char: '🙂',
46
+      name: 'slightly smiling face'
47
+    },
48
+    {
49
+      codes: '1F643',
50
+      char: '🙃',
51
+      name: 'upside-down face'
52
+    },
53
+    {
54
+      codes: '1F609',
55
+      char: '😉',
56
+      name: 'winking face'
57
+    },
58
+    {
59
+      codes: '1F60A',
60
+      char: '😊',
61
+      name: 'smiling face with smiling eyes'
62
+    },
63
+    {
64
+      codes: '1F607',
65
+      char: '😇',
66
+      name: 'smiling face with halo'
67
+    },
68
+    {
69
+      codes: '1F970',
70
+      char: '🥰',
71
+      name: 'smiling face with hearts'
72
+    },
73
+    {
74
+      codes: '1F60D',
75
+      char: '😍',
76
+      name: 'smiling face with heart-eyes'
77
+    },
78
+    {
79
+      codes: '1F929',
80
+      char: '🤩',
81
+      name: 'star-struck'
82
+    },
83
+    {
84
+      codes: '1F618',
85
+      char: '😘',
86
+      name: 'face blowing a kiss'
87
+    },
88
+    {
89
+      codes: '1F617',
90
+      char: '😗',
91
+      name: 'kissing face'
92
+    },
93
+    {
94
+      codes: '1F61A',
95
+      char: '😚',
96
+      name: 'kissing face with closed eyes'
97
+    },
98
+    {
99
+      codes: '1F619',
100
+      char: '😙',
101
+      name: 'kissing face with smiling eyes'
102
+    },
103
+    {
104
+      codes: '1F44B',
105
+      char: '👋',
106
+      name: 'waving hand'
107
+    },
108
+    {
109
+      codes: '1F91A',
110
+      char: '🤚',
111
+      name: 'raised back of hand'
112
+    },
113
+    {
114
+      codes: '1F590',
115
+      char: '🖐',
116
+      name: 'hand with fingers splayed'
117
+    },
118
+    {
119
+      codes: '270B',
120
+      char: '✋',
121
+      name: 'raised hand'
122
+    }
123
+  ],
124
+  [
125
+    {
126
+      codes: '1F596',
127
+      char: '🖖',
128
+      name: 'vulcan salute'
129
+    },
130
+    {
131
+      codes: '1F44C',
132
+      char: '👌',
133
+      name: 'OK hand'
134
+    },
135
+    {
136
+      codes: '1F90F',
137
+      char: '🤏',
138
+      name: 'pinching hand'
139
+    },
140
+    {
141
+      codes: '270C',
142
+      char: '✌',
143
+      name: 'victory hand'
144
+    },
145
+    {
146
+      codes: '1F91E',
147
+      char: '🤞',
148
+      name: 'crossed fingers'
149
+    },
150
+    {
151
+      codes: '1F91F',
152
+      char: '🤟',
153
+      name: 'love-you gesture'
154
+    },
155
+    {
156
+      codes: '1F918',
157
+      char: '🤘',
158
+      name: 'sign of the horns'
159
+    },
160
+    {
161
+      codes: '1F919',
162
+      char: '🤙',
163
+      name: 'call me hand'
164
+    },
165
+    {
166
+      codes: '1F448',
167
+      char: '👈',
168
+      name: 'backhand index pointing left'
169
+    },
170
+    {
171
+      codes: '1F449',
172
+      char: '👉',
173
+      name: 'backhand index pointing right'
174
+    },
175
+    {
176
+      codes: '1F446',
177
+      char: '👆',
178
+      name: 'backhand index pointing up'
179
+    },
180
+    {
181
+      codes: '1F595',
182
+      char: '🖕',
183
+      name: 'middle finger'
184
+    },
185
+    {
186
+      codes: '1F447',
187
+      char: '👇',
188
+      name: 'backhand index pointing down'
189
+    },
190
+    {
191
+      codes: '261D FE0F',
192
+      char: '☝️',
193
+      name: 'index pointing up'
194
+    },
195
+    {
196
+      codes: '1F44D',
197
+      char: '👍',
198
+      name: 'thumbs up'
199
+    },
200
+    {
201
+      codes: '1F44E',
202
+      char: '👎',
203
+      name: 'thumbs down'
204
+    },
205
+    {
206
+      codes: '270A',
207
+      char: '✊',
208
+      name: 'raised fist'
209
+    },
210
+    {
211
+      codes: '1F44A',
212
+      char: '👊',
213
+      name: 'oncoming fist'
214
+    },
215
+    {
216
+      codes: '1F91B',
217
+      char: '🤛',
218
+      name: 'left-facing fist'
219
+    },
220
+    {
221
+      codes: '1F91C',
222
+      char: '🤜',
223
+      name: 'right-facing fist'
224
+    }
225
+  ]
226
+]
227
+
228
+export default data

二進制
src/static/iconfont/iconfont.ttf


二進制
src/static/images/home.png


二進制
src/static/images/home_active.png


二進制
src/static/images/mescroll-empty.png


二進制
src/static/images/mescroll-totop.png


二進制
src/static/images/my.png


二進制
src/static/images/my_active.png


二進制
src/static/images/service.png


二進制
src/static/images/service_active.png


+ 19 - 0
src/stores/index.ts

@@ -0,0 +1,19 @@
1
+/*
2
+ * @Author: wyd
3
+ * @Date: 2024-02
4
+ * @LastEditors: wyd
5
+ * @LastEditTime: 2024-03
6
+ * @Description:
7
+ */
8
+import { createPinia } from 'pinia'
9
+import persist from 'pinia-plugin-persistedstate'
10
+
11
+// 创建pinia实例
12
+const pinia = createPinia()
13
+// 使用持久化存储插件
14
+pinia.use(persist)
15
+
16
+export default pinia
17
+// 模块导出
18
+export * from './modules/user'
19
+export * from './modules/system'

+ 50 - 0
src/stores/modules/system.ts

@@ -0,0 +1,50 @@
1
+import { getConfigAPI } from '@/api/system'
2
+import type { ConfigItem } from '@/types/system'
3
+import { defineStore } from 'pinia'
4
+import { ref } from 'vue'
5
+
6
+export const system = defineStore(
7
+  'system',
8
+  () => {
9
+    const config = ref<ConfigItem>({
10
+      qcc: '',
11
+      captcha: '',
12
+      background: '',
13
+      name: '',
14
+      sms: '',
15
+      logo: '',
16
+      weblogo: '',
17
+      email: '',
18
+      register: ''
19
+    })
20
+    const getConfigData = async () => {
21
+      const res = await getConfigAPI()
22
+      config.value = res.data
23
+    }
24
+    // 工作台是否已加载
25
+    const isLoad = ref(false)
26
+    const setLoad = (val: boolean) => {
27
+      isLoad.value = val
28
+    }
29
+    // 清空所有信息
30
+    const clearInfo = () => {
31
+      isLoad.value = false
32
+    }
33
+    return {
34
+      config,
35
+      isLoad,
36
+      getConfigData,
37
+      clearInfo,
38
+      setLoad
39
+    }
40
+  },
41
+  // 持久化配置
42
+  // {
43
+  //   persist: {
44
+  //     paths: [],
45
+  //     storage: {
46
+        
47
+  //     }
48
+  //   }
49
+  // }
50
+)

+ 63 - 0
src/stores/modules/user.ts

@@ -0,0 +1,63 @@
1
+/*
2
+ * @Author: wyd
3
+ * @Date: 2024-02
4
+ * @LastEditors: wyd
5
+ * @LastEditTime: 2024-04
6
+ * @Description: 用户信息
7
+ */
8
+import { defineStore } from 'pinia'
9
+import { ref } from 'vue'
10
+import type { UserInfo } from '@/types/user'
11
+import type { useInfoResult } from '@/types/login'
12
+
13
+export const user = defineStore(
14
+  'user',
15
+  () => {
16
+    const info = ref<useInfoResult>() // 用户信息
17
+    const token = ref('')
18
+    const role = ref(0) // 用户角色
19
+    const permissions = ref<string[]>([]) // 权限
20
+
21
+    // 存储用户信息
22
+    const setUserInfo = (val: useInfoResult) => {
23
+      info.value = val
24
+      role.value = val.type
25
+    }
26
+    const setToken = (val: string) => {
27
+      token.value = val
28
+    }
29
+    const setPermissions = (val: string[]) => {
30
+      permissions.value = val
31
+    }
32
+    // 清空所有信息
33
+    const clearInfo = () => {
34
+      info.value = {} as useInfoResult
35
+      token.value = ''
36
+      role.value = 0
37
+      permissions.value = []
38
+    }
39
+    return {
40
+      info,
41
+      token,
42
+      role,
43
+      permissions,
44
+      setPermissions,
45
+      setToken,
46
+      setUserInfo,
47
+      clearInfo,
48
+    }
49
+  },
50
+  // 持久化配置
51
+  {
52
+    persist: {
53
+      storage: {
54
+        getItem(key) {
55
+          return uni.getStorageSync(key)
56
+        },
57
+        setItem(key, value) {
58
+          uni.setStorageSync(key, value)
59
+        }
60
+      }
61
+    }
62
+  }
63
+)

+ 14 - 0
src/stores/reset.ts

@@ -0,0 +1,14 @@
1
+/*
2
+ * @Author: wyd
3
+ * @Date: 2024-03
4
+ * @LastEditors: wyd
5
+ * @LastEditTime: 2024-03
6
+ * @Description: 重置pinia所有状态
7
+ */
8
+import cloneDeep from 'lodash/cloneDeep'
9
+
10
+export default function resetStore(data: { store: any }) {
11
+  const initialState = cloneDeep(data.store.$state)
12
+
13
+  data.store.$reset = () => data.store.$patch(cloneDeep(initialState))
14
+}

文件差異過大導致無法顯示
+ 592 - 0
src/styles/emoji.scss


文件差異過大導致無法顯示
+ 1337 - 0
src/styles/index.scss


+ 0 - 0
src/types/components.d.ts


+ 83 - 0
src/types/global.d.ts

@@ -0,0 +1,83 @@
1
+/*
2
+ * @Author: wyd
3
+ * @Date: 2024-02
4
+ * @LastEditors: wyd
5
+ * @LastEditTime: 2024-03
6
+ * @Description: 全局类型
7
+ */
8
+
9
+declare module 'lodash/cloneDeep'
10
+declare module 'lodash'
11
+declare module 'qs'
12
+
13
+declare module '@dcloudio/uni-app'
14
+
15
+/* 数组类型 */
16
+declare type ArrayItem = {
17
+  /* id */
18
+  id: string
19
+  /* 名称 */
20
+  name: string
21
+}
22
+
23
+/* 图片 */
24
+declare type ImgItem = {
25
+  /* 地址 */
26
+  filePath: string
27
+  ossId: string
28
+  id: string
29
+}
30
+
31
+/* 园区楼宇类型 */
32
+declare type ParkItem = ArrayItem & {
33
+  /* id */
34
+  value: string
35
+  /* 名称 */
36
+  text: string
37
+  /* 图片 */
38
+  images: ImgItem[]
39
+  /* 楼宇 */
40
+  buildings: ArrayItem[]
41
+}
42
+
43
+/* 企业 */
44
+declare interface enterpriseItem extends ArrayItem {
45
+  /* 园区名称 */
46
+  parkName: string
47
+  /* 园区id */
48
+  parkId: string
49
+  /* 联系人 */
50
+  contactName: string
51
+  /* 联系方式 */
52
+  contactMobile: string
53
+}
54
+
55
+/**
56
+ * 分页查询参数
57
+ */
58
+declare interface PageQuery extends EmptyObjectType {
59
+  /* 页码 */
60
+  pageNum: number
61
+  /* 每页数量 */
62
+  pageSize: number
63
+}
64
+
65
+// children 可选
66
+declare type ChilType<T = any> = {
67
+  children?: T[]
68
+}
69
+
70
+// 数组
71
+declare type EmptyArrayType<T = any> = T[]
72
+
73
+// 对象
74
+declare type EmptyObjectType<T = any> = {
75
+  [key: string]: T
76
+}
77
+
78
+/* tabs切换标签 */
79
+declare type TabsItem = {
80
+  id: number
81
+  /* 名称 */
82
+  title: string
83
+}

+ 0 - 0
src/types/home.d.ts


+ 114 - 0
src/types/login.ts

@@ -0,0 +1,114 @@
1
+/* 登录参数 */
2
+export type loginItem = weixinItem & {
3
+  /* 用户名、手机号 */
4
+  username: string
5
+  /* 密码 */
6
+  password: string
7
+  /* 验证码 */
8
+  code: string
9
+  /* 短信登录手机号 */
10
+  phonenumber: string
11
+  /* 短息验证码 */
12
+  smsCode: string
13
+  /* 登录类型 1短信验证码登录 2密码登录 */
14
+  type: number
15
+}
16
+
17
+/* 微信登录参数 */
18
+export type weixinItem = {
19
+  /* 用户名 */
20
+  username: string
21
+  /* 密码 */
22
+  password: string
23
+  /* 验证码 */
24
+  code: string
25
+  /* openId */
26
+  openId: string
27
+}
28
+
29
+/* 登录返回值 */
30
+export type loginResult = {
31
+  token: string
32
+}
33
+
34
+export type captchaItem = {
35
+  /* 是否登录调用 */
36
+  isWeb?: boolean
37
+}
38
+
39
+/* 验证码返回值 */
40
+export type captchaResult = {
41
+  img: string
42
+  uuid: string
43
+  captchaEnabled: boolean
44
+}
45
+
46
+export type captchaInfo = captchaResult & {
47
+  code: string
48
+}
49
+
50
+/* 短信验证码 */
51
+export type smsItem = {
52
+  /* 类型 1登录 2忘记密码 */
53
+  type: number
54
+  /* 手机号 */
55
+  phonenumber: string
56
+}
57
+
58
+/* 注册 */
59
+export type registerItem = {
60
+  /* 用户名 */
61
+  // username: string
62
+  /* 姓名 */
63
+  realname: string
64
+  /* 密码 */
65
+  password: string
66
+  /* 手机号 */
67
+  mobile: string
68
+  uuid: string
69
+  /* 验证码 */
70
+  code: string
71
+}
72
+
73
+/* 忘记密码参数 */
74
+export type forgetItem = {
75
+  /* 密码 */
76
+  password: string
77
+  /* 手机号 */
78
+  phonenumber: string
79
+  /* 短信验证码 */
80
+  smsCode: string
81
+}
82
+
83
+export type userItem = {
84
+  /* 头像 */
85
+  avatar: string
86
+  /* 邮箱 */
87
+  email: string
88
+  /* 用户id */
89
+  id: string
90
+  /* 姓名 */
91
+  realname: string
92
+  /* 用户名 */
93
+  username: string
94
+  /* 角色id */
95
+  type: number
96
+  /* 手机号 */
97
+  mobile: string
98
+  /* 性别 1男 2女 */
99
+  sex: number
100
+}
101
+
102
+/* 用户信息 */
103
+export type useInfoResult = userItem & {
104
+  /* 用户信息 */
105
+  user: userItem
106
+  /* 权限 */
107
+  permissions: string[]
108
+  /* 是否为党员书记 2为书记 */
109
+  secretary: number
110
+  /* 角色 */
111
+  roles: EmptyObjectType[]
112
+  /* 角色名称 */
113
+  roleGroup: string
114
+}

+ 21 - 0
src/types/system.d.ts

@@ -0,0 +1,21 @@
1
+/* 全局配置返回值类型 */
2
+export type ConfigItem = {
3
+  /* 企查查配置是否开启 */
4
+  qcc: string
5
+  /* 是否显示验证码 */
6
+  captcha: string
7
+  /* 登录页背景图 */
8
+  background: string
9
+  /* 网站名称 */
10
+  name: string
11
+  /* 是否配置短信 */
12
+  sms: string
13
+  /* 系统logo */
14
+  logo: string
15
+  /* 官网logo */
16
+  weblogo: string
17
+  /* 是否配置邮箱 */
18
+  email: string
19
+  /* 是否开启用户注册 */
20
+  register: string
21
+}

+ 21 - 0
src/types/user.d.ts

@@ -0,0 +1,21 @@
1
+/*
2
+ * @Author: wyd
3
+ * @Date: 2024-02
4
+ * @LastEditors: wyd
5
+ * @LastEditTime: 2024-02
6
+ * @Description: 用户信息
7
+ */
8
+
9
+/* 用户信息 */
10
+export type UserInfo = {
11
+  /* 用户id */
12
+  id: string | number
13
+  /* 姓名 */
14
+  realname: string
15
+  /* 登录凭证 */
16
+  // token: string
17
+  /* 角色id */
18
+  // role: number
19
+  /* 用户角色 */
20
+  type: number
21
+}

+ 23 - 0
src/types/workflow.d.ts

@@ -0,0 +1,23 @@
1
+/*
2
+ * @page:工作流
3
+ * @Author: JiangChunMei
4
+ * @LastEditors: JiangChunMei
5
+ */
6
+
7
+export type detailItem = {
8
+  businessId: string
9
+  categoryId: string
10
+  procInsId?: string
11
+}
12
+
13
+export type formItem = {
14
+  procInsId: string
15
+  taskId: string
16
+  comment?: string
17
+  attachments?: EmptyObjectType[]
18
+  multiAddUserId?: string | number
19
+  executionUserIds?: (string | number)[]
20
+  signInfo?: string
21
+  userId?: string | number
22
+  executionUserIds?: (string | number)[]
23
+}

+ 77 - 0
src/uni.scss

@@ -0,0 +1,77 @@
1
+/**
2
+ * 这里是uni-app内置的常用样式变量
3
+ *
4
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
5
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
6
+ *
7
+ */
8
+
9
+/**
10
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
11
+ *
12
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
13
+ */
14
+@import '@/uni_modules/vk-uview-ui/theme.scss';
15
+/* 颜色变量 */
16
+
17
+/* 行为相关颜色 */
18
+$uni-color-main: #479bfa;
19
+$uni-color-primary: #2064d1;
20
+$uni-color-success: #00c1ab;
21
+$uni-color-warning: #FF822B;
22
+$uni-color-error: #ed240e;
23
+
24
+/* 文字基本颜色 */
25
+$uni-text-color: #333; // 基本色
26
+$uni-text-color-inverse: #fff; // 反色
27
+$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息
28
+$uni-text-color-placeholder: #808080;
29
+$uni-text-color-disable: #ccc;
30
+
31
+/* 背景颜色 */
32
+$uni-bg-color: #fff;
33
+$uni-bg-color-grey: #f8f8f8;
34
+$uni-bg-color-hover: #f1f1f1; // 点击状态颜色
35
+$uni-bg-color-mask: rgba(0, 0, 0, 0.4); // 遮罩颜色
36
+
37
+/* 边框颜色 */
38
+$uni-border-color: #e5e5e5;
39
+
40
+/* 尺寸变量 */
41
+
42
+/* 文字尺寸 */
43
+$uni-font-size-sm: 12px;
44
+$uni-font-size-base: 14px;
45
+$uni-font-size-lg: 16px;
46
+
47
+/* 图片尺寸 */
48
+$uni-img-size-sm: 20px;
49
+$uni-img-size-base: 26px;
50
+$uni-img-size-lg: 40px;
51
+
52
+/* Border Radius */
53
+$uni-border-radius-sm: 2px;
54
+$uni-border-radius-base: 3px;
55
+$uni-border-radius-lg: 6px;
56
+$uni-border-radius-circle: 50%;
57
+
58
+/* 水平间距 */
59
+$uni-spacing-row-sm: 5px;
60
+$uni-spacing-row-base: 10px;
61
+$uni-spacing-row-lg: 15px;
62
+
63
+/* 垂直间距 */
64
+$uni-spacing-col-sm: 4px;
65
+$uni-spacing-col-base: 8px;
66
+$uni-spacing-col-lg: 12px;
67
+
68
+/* 透明度 */
69
+$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
70
+
71
+/* 文章场景相关 */
72
+$uni-color-title: #2c405a; // 文章标题颜色
73
+$uni-font-size-title: 20px;
74
+$uni-color-subtitle: #555; // 二级标题颜色
75
+$uni-font-size-subtitle: 18px;
76
+$uni-color-paragraph: #3f536e; // 文章段落颜色
77
+$uni-font-size-paragraph: 15px;

+ 8 - 0
src/uni_modules/mescroll-uni/changelog.md

@@ -0,0 +1,8 @@
1
+## 1.3.8(2023-03-26)
2
+1. 新增useMescroll的hook, 支持vue3 script setup的写法  
3
+2. 新增vue3 script setup的示例 ( 根据vue2的示例,全部重写了一遍 )  
4
+3. mescroll-body 和 mescroll-uni 无需再写 ref="mescrollRef"  
5
+4. 解决mescroll-uni在页面渲染之后,无法动态设置height的问题  
6
+5. 解决renderjs在h5返回有时候无法正常滑动的问题  
7
+6. 修复小程序编辑器提示 Cannot read property 'nv_optDown' of undefined 的错误  
8
+-by 小瑾同学

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

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

+ 400 - 0
src/uni_modules/mescroll-uni/components/mescroll-body/mescroll-body.vue

@@ -0,0 +1,400 @@
1
+<template>
2
+	<view 
3
+	class="mescroll-body mescroll-render-touch" 
4
+	:class="{'mescorll-sticky': sticky}"
5
+	:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" 
6
+	@touchstart="wxsBiz.touchstartEvent" 
7
+	@touchmove="wxsBiz.touchmoveEvent" 
8
+	@touchend="wxsBiz.touchendEvent" 
9
+	@touchcancel="wxsBiz.touchendEvent"
10
+	:change:prop="wxsBiz.propObserver"
11
+	:prop="wxsProp"
12
+	>
13
+		<!-- 状态栏 -->
14
+		<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
15
+		
16
+		<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp">
17
+			<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
18
+			<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
19
+			<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
20
+				<view class="downwarp-content">
21
+					<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
22
+					<view class="downwarp-tip">{{downText}}</view>
23
+				</view>
24
+			</view>
25
+	
26
+			<!-- 列表内容 -->
27
+			<slot></slot>
28
+
29
+			<!-- 空布局 -->
30
+			<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
31
+
32
+			<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
33
+			<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
34
+			<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
35
+				<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
36
+				<view v-show="upLoadType===1">
37
+					<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
38
+					<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
39
+				</view>
40
+				<!-- 无数据 -->
41
+				<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
42
+			</view>
43
+		</view>
44
+		
45
+		<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
46
+		<!-- #ifdef H5 -->
47
+		<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
48
+		<!-- #endif -->
49
+		
50
+		<!-- 适配iPhoneX -->
51
+		<view v-if="safearea" class="mescroll-safearea"></view>
52
+		
53
+		<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
54
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
55
+		
56
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
57
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
58
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
59
+		<!-- #endif -->
60
+	</view>
61
+</template>
62
+
63
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
64
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
65
+<script src="../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
66
+<!-- #endif -->
67
+
68
+<!-- app, h5使用renderjs -->
69
+<!-- #ifdef APP-PLUS || H5 -->
70
+<script module="renderBiz" lang="renderjs">
71
+	import renderBiz from "../mescroll-uni/wxs/renderjs.js";
72
+	export default {
73
+		mixins: [renderBiz]
74
+	}
75
+</script>
76
+<!-- #endif -->
77
+
78
+<script>
79
+	// 引入mescroll-uni.js,处理核心逻辑
80
+	import MeScroll from "../mescroll-uni/mescroll-uni.js";
81
+	// 引入全局配置
82
+	import GlobalOption from "../mescroll-uni/mescroll-uni-option.js";
83
+	// 引入国际化工具类
84
+	import mescrollI18n from '../mescroll-uni/mescroll-i18n.js';
85
+	// 引入回到顶部组件
86
+	import MescrollTop from "../mescroll-uni/components/mescroll-top.vue";
87
+	// 引入兼容wxs(含renderjs)写法的mixins
88
+	import WxsMixin from "../mescroll-uni/wxs/mixins.js";
89
+	
90
+	/**
91
+	 * mescroll-body 基于page滚动的下拉刷新和上拉加载组件, 支持嵌套原生组件, 性能好
92
+	 * @property {Object} down 下拉刷新的参数配置
93
+	 * @property {Object} up 上拉加载的参数配置
94
+	 * @property {Object} i18n 国际化的参数配置
95
+	 * @property {String, Number} top 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
96
+	 * @property {Boolean, String} topbar 偏移量top是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
97
+	 * @property {String, Number} bottom 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
98
+	 * @property {Boolean} safearea 偏移量bottom是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
99
+	 * @property {Boolean} fixed 是否通过fixed固定mescroll的高度, 默认true
100
+	 * @property {String, Number} height 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
101
+	 * @property {Boolean} bottombar 底部是否偏移TabBar的高度 (仅在H5端的tab页生效)
102
+	 * @property {Boolean} sticky 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法隐藏
103
+	 * @event {Function} init 初始化完成的回调 
104
+	 * @event {Function} down 下拉刷新的回调
105
+	 * @event {Function} up 上拉加载的回调 
106
+	 * @event {Function} emptyclick 点击empty配置的btnText按钮回调
107
+	 * @event {Function} topclick 点击回到顶部的按钮回调
108
+	 * @event {Function} scroll 滚动监听 (需在 up 配置 onScroll:true 才生效)
109
+	 * @example <mescroll-body @init="mescrollInit" @down="downCallback" @up="upCallback"> ... </mescroll-body>
110
+	 */
111
+	export default {
112
+		name: 'mescroll-body',
113
+		mixins: [WxsMixin],
114
+		components: {
115
+			MescrollTop
116
+		},
117
+		props: {
118
+			down: Object,
119
+			up: Object,
120
+			i18n: Object,
121
+			top: [String, Number],
122
+			topbar: [Boolean, String],
123
+			bottom: [String, Number],
124
+			safearea: Boolean,
125
+			height: [String, Number],
126
+			bottombar:{
127
+				type: Boolean,
128
+				default: true
129
+			},
130
+			sticky: Boolean
131
+		},
132
+		data() {
133
+			return {
134
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
135
+				downHight: 0, //下拉刷新: 容器高度
136
+				downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
137
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
138
+				upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
139
+				isShowEmpty: false, // 是否显示空布局
140
+				isShowToTop: false, // 是否显示回到顶部按钮
141
+				windowHeight: 0, // 可使用窗口的高度
142
+				windowBottom: 0, // 可使用窗口的底部位置
143
+				statusBarHeight: 0 // 状态栏高度
144
+			};
145
+		},
146
+		computed: {
147
+			// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
148
+			minHeight(){
149
+				return this.toPx(this.height || '100%') + 'px'
150
+			},
151
+			// 下拉布局往下偏移的距离 (px)
152
+			numTop() {
153
+				return this.toPx(this.top)
154
+			},
155
+			padTop() {
156
+				return this.numTop + 'px';
157
+			},
158
+			// 上拉布局往上偏移 (px)
159
+			numBottom() {
160
+				return this.toPx(this.bottom);
161
+			},
162
+			padBottom() {
163
+				return this.numBottom + 'px';
164
+			},
165
+			// 是否为重置下拉的状态
166
+			isDownReset() {
167
+				return this.downLoadType === 3 || this.downLoadType === 4;
168
+			},
169
+			// 过渡
170
+			transition() {
171
+				return this.isDownReset ? 'transform 300ms' : '';
172
+			},
173
+			translateY() {
174
+				return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
175
+			},
176
+			// 是否在加载中
177
+			isDownLoading(){
178
+				return this.downLoadType === 3
179
+			},
180
+			// 旋转的角度
181
+			downRotate(){
182
+				return 'rotate(' + 360 * this.downRate + 'deg)'
183
+			},
184
+			// 文本提示
185
+			downText(){
186
+				if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
187
+				switch (this.downLoadType){
188
+					case 1: return this.mescroll.optDown.textInOffset;
189
+					case 2: return this.mescroll.optDown.textOutOffset;
190
+					case 3: return this.mescroll.optDown.textLoading;
191
+					case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
192
+					default: return this.mescroll.optDown.textInOffset;
193
+				}
194
+			}
195
+		},
196
+		methods: {
197
+			//number,rpx,upx,px,% --> px的数值
198
+			toPx(num) {
199
+				if (typeof num === 'string') {
200
+					if (num.indexOf('px') !== -1) {
201
+						if (num.indexOf('rpx') !== -1) {
202
+							// "10rpx"
203
+							num = num.replace('rpx', '');
204
+						} else if (num.indexOf('upx') !== -1) {
205
+							// "10upx"
206
+							num = num.replace('upx', '');
207
+						} else {
208
+							// "10px"
209
+							return Number(num.replace('px', ''));
210
+						}
211
+					} else if (num.indexOf('%') !== -1) {
212
+						// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
213
+						let rate = Number(num.replace('%', '')) / 100;
214
+						return this.windowHeight * rate;
215
+					}
216
+				}
217
+				return num ? uni.upx2px(Number(num)) : 0;
218
+			},
219
+			// 点击空布局的按钮回调
220
+			emptyClick() {
221
+				this.$emit('emptyclick', this.mescroll);
222
+			},
223
+			// 点击回到顶部的按钮回调
224
+			toTopClick() {
225
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
226
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
227
+			}
228
+		},
229
+		// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
230
+		created() {
231
+			let vm = this;
232
+
233
+			let diyOption = {
234
+				// 下拉刷新的配置
235
+				down: {
236
+					inOffset() {
237
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
238
+					},
239
+					outOffset() {
240
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
241
+					},
242
+					onMoving(mescroll, rate, downHight) {
243
+						// 下拉过程中的回调,滑动过程一直在执行;
244
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
245
+						vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
246
+					},
247
+					showLoading(mescroll, downHight) {
248
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
249
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
250
+					},
251
+					beforeEndDownScroll(mescroll){
252
+						vm.downLoadType = 4; 
253
+						return mescroll.optDown.beforeEndDelay // 延时结束的时长
254
+					},
255
+					endDownScroll() {
256
+						vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
257
+						vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
258
+						if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时
259
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset
260
+							if(vm.downLoadType === 4) vm.downLoadType = 0
261
+						},300)
262
+					},
263
+					// 派发下拉刷新的回调
264
+					callback: function(mescroll) {
265
+						vm.$emit('down', mescroll);
266
+					}
267
+				},
268
+				// 上拉加载的配置
269
+				up: {
270
+					// 显示加载中的回调
271
+					showLoading() {
272
+						vm.upLoadType = 1;
273
+					},
274
+					// 显示无更多数据的回调
275
+					showNoMore() {
276
+						vm.upLoadType = 2;
277
+					},
278
+					// 隐藏上拉加载的回调
279
+					hideUpScroll(mescroll) {
280
+						vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
281
+					},
282
+					// 空布局
283
+					empty: {
284
+						onShow(isShow) {
285
+							// 显示隐藏的回调
286
+							vm.isShowEmpty = isShow;
287
+						}
288
+					},
289
+					// 回到顶部
290
+					toTop: {
291
+						onShow(isShow) {
292
+							// 显示隐藏的回调
293
+							vm.isShowToTop = isShow;
294
+						}
295
+					},
296
+					// 派发上拉加载的回调
297
+					callback: function(mescroll) {
298
+						vm.$emit('up', mescroll);
299
+					}
300
+				}
301
+			};
302
+			
303
+			let i18nType = mescrollI18n.getType() // 当前语言类型
304
+			let i18nOption = {type: i18nType} // 国际化配置
305
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
306
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
307
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
308
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
309
+			let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响
310
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
311
+
312
+			// 初始化MeScroll对象
313
+			vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
314
+			// 挂载语言包
315
+			vm.mescroll.i18n = i18nOption;
316
+			// init回调mescroll对象
317
+			vm.$emit('init', vm.mescroll);
318
+
319
+			// 设置高度
320
+			const sys = uni.getSystemInfoSync();
321
+			if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
322
+			if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
323
+			if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
324
+			// 使down的bottomOffset生效
325
+			vm.mescroll.setBodyHeight(sys.windowHeight);
326
+
327
+			// 因为使用的是page的scroll,这里需自定义scrollTo
328
+			vm.mescroll.resetScrollTo((y, t) => {
329
+				if(typeof y === 'string'){
330
+					// 滚动到指定view (y为css选择器)
331
+					setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
332
+						let selector;
333
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
334
+							selector = '#'+y // 不带#和. 则默认为id选择器
335
+						}else{
336
+							selector = y
337
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
338
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
339
+								selector = y.split('>>>')[1].trim()
340
+							}
341
+							// #endif
342
+						}
343
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
344
+							if (rect) {
345
+								let top = rect.top
346
+								top += vm.mescroll.getScrollTop()
347
+								uni.pageScrollTo({
348
+									scrollTop: top,
349
+									duration: t
350
+								})
351
+							} else{
352
+								console.error(selector + ' does not exist');
353
+							}
354
+						}).exec()
355
+					},30)
356
+				} else{
357
+					// 滚动到指定位置 (y必须为数字)
358
+					uni.pageScrollTo({
359
+						scrollTop: y,
360
+						duration: t
361
+					})
362
+				}
363
+			});
364
+
365
+			// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
366
+			if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
367
+				vm.mescroll.optUp.toTop.safearea = vm.safearea;
368
+			}
369
+			
370
+			// 全局配置监听
371
+			uni.$on("setMescrollGlobalOption", options=>{
372
+				if(!options) return;
373
+				let i18nType = options.i18n ? options.i18n.type : null
374
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
375
+					vm.mescroll.i18n.type = i18nType
376
+					mescrollI18n.setType(i18nType)
377
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
378
+				}
379
+				if(options.down){
380
+					let down = MeScroll.extend({}, options.down)
381
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
382
+				}
383
+				if(options.up){
384
+					let up = MeScroll.extend({}, options.up)
385
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
386
+				}
387
+			})
388
+		},
389
+		destroyed() {
390
+			// 注销全局配置监听
391
+			uni.$off("setMescrollGlobalOption")
392
+		}
393
+	};
394
+</script>
395
+
396
+<style>
397
+	@import "../mescroll-body/mescroll-body.css";
398
+	@import "../mescroll-uni/components/mescroll-down.css";
399
+	@import "../mescroll-uni/components/mescroll-up.css";
400
+</style>

+ 47 - 0
src/uni_modules/mescroll-uni/components/mescroll-diy/beibei/components/mescroll-down.css

@@ -0,0 +1,47 @@
1
+/*下拉刷新--标语*/
2
+.mescroll-downwarp .downwarp-slogan{
3
+	display: block;
4
+	width: 420rpx;
5
+	height: 168rpx;
6
+	margin: auto;
7
+}
8
+/*下拉刷新--向下进度动画*/
9
+.mescroll-downwarp .downwarp-progress{
10
+	display: inline-block;
11
+	width: 40rpx;
12
+	height: 40rpx;
13
+	border: none;
14
+	margin: auto;
15
+	background-size: contain;
16
+	background-repeat: no-repeat;
17
+	background-position: center;
18
+	background-image: url(https://www.mescroll.com/img/beibei/mescroll-progress.png);
19
+	transition: all 300ms;
20
+}
21
+/*下拉刷新--进度条*/
22
+.mescroll-downwarp .downwarp-loading{
23
+	display: inline-block;
24
+	width: 32rpx;
25
+	height: 32rpx;
26
+	border-radius: 50%;
27
+	border: 2rpx solid #FF8095;
28
+	border-bottom-color: transparent;
29
+}
30
+/*下拉刷新--吉祥物*/
31
+.mescroll-downwarp .downwarp-mascot{
32
+	position: absolute;
33
+	right: 16rpx;
34
+	bottom: 0;
35
+	width: 100rpx;
36
+	height: 100rpx;
37
+	background-size: contain;
38
+	background-repeat: no-repeat;
39
+	animation: animMascot .6s steps(1,end) infinite;
40
+}
41
+@keyframes animMascot {
42
+	0% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb1.png)}
43
+	25% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb2.png)}
44
+	50% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb3.png)}
45
+	75% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb4.png)}
46
+	100% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb1.png)}
47
+}

+ 39 - 0
src/uni_modules/mescroll-uni/components/mescroll-diy/beibei/components/mescroll-down.vue

@@ -0,0 +1,39 @@
1
+<!-- 下拉刷新区域 -->
2
+<template>
3
+	<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}">
4
+		<view class="downwarp-content">
5
+			<image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/>
6
+			<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view>
7
+			<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view>
8
+			<view class="downwarp-mascot"></view>
9
+		</view>
10
+	</view>
11
+</template>
12
+
13
+<script>
14
+export default {
15
+	props: {
16
+		option: Object , // down的配置项
17
+		type: Number // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
18
+	},
19
+	computed: {
20
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
21
+		mOption(){
22
+			return this.option || {}
23
+		},
24
+		// 是否在加载中
25
+		isDownLoading(){
26
+			return this.type === 3
27
+		},
28
+		// 旋转的角度
29
+		downRotate(){
30
+			return this.type === 2 ? 'rotate(180deg)' : 'rotate(0deg)'
31
+		}
32
+	}
33
+};
34
+</script>
35
+
36
+<style>
37
+@import "../../../mescroll-uni/components/mescroll-down.css";
38
+@import "./mescroll-down.css";
39
+</style>

+ 360 - 0
src/uni_modules/mescroll-uni/components/mescroll-diy/beibei/mescroll-body.vue

@@ -0,0 +1,360 @@
1
+<template>
2
+	<view 
3
+		class="mescroll-body mescroll-render-touch" 
4
+		:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" 
5
+		:class="{'mescorll-sticky': sticky}"
6
+		@touchstart="wxsBiz.touchstartEvent" 
7
+		@touchmove="wxsBiz.touchmoveEvent" 
8
+		@touchend="wxsBiz.touchendEvent" 
9
+		@touchcancel="wxsBiz.touchendEvent"
10
+		:change:prop="wxsBiz.propObserver"
11
+		:prop="wxsProp"
12
+		>
13
+		
14
+		<!-- 状态栏 -->
15
+		<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
16
+
17
+		<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp">
18
+			<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
19
+			<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType"></mescroll-down> -->
20
+			<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
21
+				<view class="downwarp-content">
22
+					<image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/>
23
+					<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view>
24
+					<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view>
25
+					<view class="downwarp-mascot"></view>
26
+				</view>
27
+			</view>
28
+						
29
+			<!-- 列表内容 -->
30
+			<slot></slot>
31
+
32
+			<!-- 空布局 -->
33
+			<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
34
+
35
+			<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
36
+			<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
37
+			<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
38
+				<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
39
+				<view v-show="upLoadType===1">
40
+					<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
41
+					<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
42
+				</view>
43
+				<!-- 无数据 -->
44
+				<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
45
+			</view>
46
+		</view>
47
+		
48
+		<!-- 底部是否偏移TabBar的高度(仅H5端生效) -->
49
+		<!-- #ifdef H5 -->
50
+		<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
51
+		<!-- #endif -->
52
+		
53
+		<!-- 适配iPhoneX -->
54
+		<view v-if="safearea" class="mescroll-safearea"></view>
55
+		
56
+		<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
57
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
58
+
59
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
60
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
61
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
62
+		<!-- #endif -->
63
+	</view>
64
+</template>
65
+
66
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
67
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
68
+<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
69
+<!-- #endif -->
70
+
71
+<!-- app, h5使用renderjs -->
72
+<!-- #ifdef APP-PLUS || H5 -->
73
+<script module="renderBiz" lang="renderjs">
74
+	import renderBiz from '../../mescroll-uni/wxs/renderjs.js';
75
+	export default {
76
+		mixins: [renderBiz]
77
+	}
78
+</script>
79
+<!-- #endif -->
80
+
81
+<script>
82
+	import MeScroll from '../../mescroll-uni/mescroll-uni.js';
83
+	import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
84
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
85
+	import mescrollI18n from '../../mescroll-uni/mescroll-i18n.js';
86
+	import GlobalOption from './mescroll-uni-option.js';
87
+	
88
+	export default {
89
+		mixins: [WxsMixin],
90
+		components: {
91
+			MescrollTop
92
+		},
93
+		data() {
94
+			return {
95
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
96
+				downHight: 0, //下拉刷新: 容器高度
97
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
98
+				upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
99
+				isShowEmpty: false, // 是否显示空布局
100
+				isShowToTop: false, // 是否显示回到顶部按钮
101
+				windowHeight: 0, // 可使用窗口的高度
102
+				windowBottom: 0, // 可使用窗口的底部位置
103
+				statusBarHeight: 0 // 状态栏高度
104
+			};
105
+		},
106
+		props: {
107
+			down: Object, // 下拉刷新的参数配置
108
+			up: Object, // 上拉加载的参数配置
109
+			i18n: Object, // 国际化的参数配置
110
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
111
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
112
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
113
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
114
+			height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
115
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
116
+				type: Boolean,
117
+				default: true
118
+			},
119
+			sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏
120
+		},
121
+		computed: {
122
+			// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
123
+			minHeight(){
124
+				return this.toPx(this.height || '100%') + 'px'
125
+			},
126
+			// 下拉布局往下偏移的距离 (px)
127
+			numTop() {
128
+				return this.toPx(this.top)
129
+			},
130
+			padTop() {
131
+				return this.numTop + 'px';
132
+			},
133
+			// 上拉布局往上偏移 (px)
134
+			numBottom() {
135
+				return this.toPx(this.bottom);
136
+			},
137
+			padBottom() {
138
+				return this.numBottom + 'px';
139
+			},
140
+			// 是否为重置下拉的状态
141
+			isDownReset() {
142
+				return this.downLoadType === 3 || this.downLoadType === 4;
143
+			},
144
+			// 过渡
145
+			transition() {
146
+				return this.isDownReset ? 'transform 300ms' : '';
147
+			},
148
+			translateY() {
149
+				return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
150
+			},
151
+			// 是否在加载中
152
+			isDownLoading(){
153
+				return this.downLoadType === 3
154
+			},
155
+			// 旋转的角度
156
+			downRotate(){
157
+				return this.downLoadType === 2 ? 'rotate(180deg)' : 'rotate(0deg)'
158
+			}
159
+		},
160
+		methods: {
161
+			//number,rpx,upx,px,% --> px的数值
162
+			toPx(num) {
163
+				if (typeof num === 'string') {
164
+					if (num.indexOf('px') !== -1) {
165
+						if (num.indexOf('rpx') !== -1) {
166
+							// "10rpx"
167
+							num = num.replace('rpx', '');
168
+						} else if (num.indexOf('upx') !== -1) {
169
+							// "10upx"
170
+							num = num.replace('upx', '');
171
+						} else {
172
+							// "10px"
173
+							return Number(num.replace('px', ''));
174
+						}
175
+					} else if (num.indexOf('%') !== -1) {
176
+						// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
177
+						let rate = Number(num.replace('%', '')) / 100;
178
+						return this.windowHeight * rate;
179
+					}
180
+				}
181
+				return num ? uni.upx2px(Number(num)) : 0;
182
+			},
183
+			// 点击空布局的按钮回调
184
+			emptyClick() {
185
+				this.$emit('emptyclick', this.mescroll);
186
+			},
187
+			// 点击回到顶部的按钮回调
188
+			toTopClick() {
189
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
190
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
191
+			}
192
+		},
193
+		// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
194
+		created() {
195
+			let vm = this;
196
+
197
+			let diyOption = {
198
+				// 下拉刷新的配置
199
+				down: {
200
+					inOffset() {
201
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
202
+					},
203
+					outOffset() {
204
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
205
+					},
206
+					onMoving(mescroll, rate, downHight) {
207
+						// 下拉过程中的回调,滑动过程一直在执行;
208
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
209
+					},
210
+					showLoading(mescroll, downHight) {
211
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
212
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
213
+					},
214
+					endDownScroll() {
215
+						vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
216
+						vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
217
+						if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时
218
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset
219
+							if(vm.downLoadType === 4) vm.downLoadType = 0
220
+						},300)
221
+					},
222
+					// 派发下拉刷新的回调
223
+					callback: function(mescroll) {
224
+						vm.$emit('down', mescroll);
225
+					}
226
+				},
227
+				// 上拉加载的配置
228
+				up: {
229
+					// 显示加载中的回调
230
+					showLoading() {
231
+						vm.upLoadType = 1;
232
+					},
233
+					// 显示无更多数据的回调
234
+					showNoMore() {
235
+						vm.upLoadType = 2;
236
+					},
237
+					// 隐藏上拉加载的回调
238
+					hideUpScroll(mescroll) {
239
+						vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
240
+					},
241
+					// 空布局
242
+					empty: {
243
+						onShow(isShow) {
244
+							// 显示隐藏的回调
245
+							vm.isShowEmpty = isShow;
246
+						}
247
+					},
248
+					// 回到顶部
249
+					toTop: {
250
+						onShow(isShow) {
251
+							// 显示隐藏的回调
252
+							vm.isShowToTop = isShow;
253
+						}
254
+					},
255
+					// 派发上拉加载的回调
256
+					callback: function(mescroll) {
257
+						vm.$emit('up', mescroll);
258
+					}
259
+				}
260
+			};
261
+
262
+			let i18nType = mescrollI18n.getType() // 当前语言类型
263
+			let i18nOption = {type: i18nType} // 国际化配置
264
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
265
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
266
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
267
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
268
+			let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响
269
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
270
+
271
+			// 初始化MeScroll对象
272
+			vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
273
+			// 挂载语言包
274
+			vm.mescroll.i18n = i18nOption;
275
+			// init回调mescroll对象
276
+			vm.$emit('init', vm.mescroll);
277
+
278
+			// 设置高度
279
+			const sys = uni.getSystemInfoSync();
280
+			if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
281
+			if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
282
+			if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
283
+			// 使down的bottomOffset生效
284
+			vm.mescroll.setBodyHeight(sys.windowHeight);
285
+
286
+			// 因为使用的是page的scroll,这里需自定义scrollTo
287
+			vm.mescroll.resetScrollTo((y, t) => {
288
+				if(typeof y === 'string'){
289
+					// 滚动到指定view (y为css选择器)
290
+					setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
291
+						let selector;
292
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
293
+							selector = '#'+y // 不带#和. 则默认为id选择器
294
+						}else{
295
+							selector = y
296
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
297
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
298
+								selector = y.split('>>>')[1].trim()
299
+							}
300
+							// #endif
301
+						}
302
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
303
+							if (rect) {
304
+								let top = rect.top
305
+								top += vm.mescroll.getScrollTop()
306
+								uni.pageScrollTo({
307
+									scrollTop: top,
308
+									duration: t
309
+								})
310
+							} else{
311
+								console.error(selector + ' does not exist');
312
+							}
313
+						}).exec()
314
+					},30)
315
+				} else{
316
+					// 滚动到指定位置 (y必须为数字)
317
+					uni.pageScrollTo({
318
+						scrollTop: y,
319
+						duration: t
320
+					})
321
+				}
322
+			});
323
+
324
+			// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
325
+			if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
326
+				vm.mescroll.optUp.toTop.safearea = vm.safearea;
327
+			}
328
+			
329
+			// 全局配置监听
330
+			uni.$on("setMescrollGlobalOption", options=>{
331
+				if(!options) return;
332
+				let i18nType = options.i18n ? options.i18n.type : null
333
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
334
+					vm.mescroll.i18n.type = i18nType
335
+					mescrollI18n.setType(i18nType)
336
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
337
+				}
338
+				if(options.down){
339
+					let down = MeScroll.extend({}, options.down)
340
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
341
+				}
342
+				if(options.up){
343
+					let up = MeScroll.extend({}, options.up)
344
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
345
+				}
346
+			})
347
+		},
348
+		destroyed() {
349
+			// 注销全局配置监听
350
+			uni.$off("setMescrollGlobalOption")
351
+		}
352
+	};
353
+</script>
354
+
355
+<style>
356
+	@import "../../mescroll-body/mescroll-body.css";
357
+	@import "../../mescroll-uni/components/mescroll-down.css";
358
+	@import "../../mescroll-uni/components/mescroll-up.css";
359
+	@import "./components/mescroll-down.css";
360
+</style>

+ 49 - 0
src/uni_modules/mescroll-uni/components/mescroll-diy/beibei/mescroll-uni-option.js

@@ -0,0 +1,49 @@
1
+// mescroll-uni和mescroll-body 的全局配置
2
+const GlobalOption = {
3
+	down: {
4
+		// 其他down的配置参数也可以写,这里只展示了常用的配置:
5
+		offset: uni.upx2px(140), // 在列表顶部,下拉大于140upx,松手即可触发下拉刷新的回调
6
+		native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
7
+	},
8
+	up: {
9
+		// 其他up的配置参数也可以写,这里只展示了常用的配置:
10
+		offset: 150, // 距底部多远时,触发upCallback
11
+		toTop: {
12
+			// 回到顶部按钮,需配置src才显示
13
+			src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
14
+			offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
15
+			right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
16
+			bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
17
+			width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
18
+		},
19
+		empty: {
20
+			use: true, // 是否显示空布局
21
+			icon: "https://www.mescroll.com/img/mescroll-empty.png" // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
22
+		}
23
+	},
24
+	// 国际化配置
25
+	i18n: {
26
+		// 中文
27
+		zh: {
28
+			up: {
29
+				textLoading: '加载中 ...', // 加载中的提示文本
30
+				textNoMore: '-- END --', // 没有更多数据的提示文本
31
+				empty: {
32
+					tip: '~ 暂无相关数据 ~' // 空提示
33
+				}
34
+			}
35
+		},
36
+		// 英文
37
+		en: {
38
+			up: {
39
+				textLoading: 'loading ...',
40
+				textNoMore: '-- END --',
41
+				empty: {
42
+					tip: '~ absolutely empty ~'
43
+				}
44
+			}
45
+		}
46
+	}
47
+}
48
+
49
+export default GlobalOption

+ 434 - 0
src/uni_modules/mescroll-uni/components/mescroll-diy/beibei/mescroll-uni.vue

@@ -0,0 +1,434 @@
1
+<template>
2
+	<view class="mescroll-uni-warp">
3
+		<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">
4
+			<view class="mescroll-uni-content mescroll-render-touch"
5
+			@touchstart="wxsBiz.touchstartEvent" 
6
+			@touchmove="wxsBiz.touchmoveEvent" 
7
+			@touchend="wxsBiz.touchendEvent" 
8
+			@touchcancel="wxsBiz.touchendEvent"
9
+			:change:prop="wxsBiz.propObserver"
10
+			:prop="wxsProp">
11
+						
12
+				<!-- 状态栏 -->
13
+				<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
14
+							
15
+				<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp">
16
+					<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
17
+					<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType"></mescroll-down> -->
18
+					<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
19
+						<view class="downwarp-content">
20
+							<image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/>
21
+							<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view>
22
+							<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view>
23
+							<view class="downwarp-mascot"></view>
24
+						</view>
25
+					</view>
26
+
27
+					<!-- 列表内容 -->
28
+					<slot></slot>
29
+
30
+					<!-- 空布局 -->
31
+					<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
32
+
33
+					<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
34
+					<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
35
+					<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
36
+						<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
37
+						<view v-show="upLoadType===1">
38
+							<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
39
+							<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
40
+						</view>
41
+						<!-- 无数据 -->
42
+						<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
43
+					</view>
44
+				</view>
45
+				
46
+				<!-- 底部是否偏移TabBar的高度(仅H5端生效) -->
47
+				<!-- #ifdef H5 -->
48
+				<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
49
+				<!-- #endif -->
50
+				
51
+				<!-- 适配iPhoneX -->
52
+				<view v-if="safearea" class="mescroll-safearea"></view>
53
+			</view>
54
+		</scroll-view>
55
+
56
+		<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
57
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
58
+
59
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
60
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
61
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
62
+		<!-- #endif -->
63
+	</view>
64
+</template>
65
+
66
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
67
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
68
+<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
69
+<!-- #endif -->
70
+
71
+<!-- app, h5使用renderjs -->
72
+<!-- #ifdef APP-PLUS || H5 -->
73
+<script module="renderBiz" lang="renderjs">
74
+	import renderBiz from '../../mescroll-uni/wxs/renderjs.js';
75
+	export default {
76
+		mixins: [renderBiz]
77
+	}
78
+</script>
79
+<!-- #endif -->
80
+
81
+<script>
82
+	import MeScroll from '../../mescroll-uni/mescroll-uni.js';
83
+	import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
84
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
85
+	import mescrollI18n from '../../mescroll-uni/mescroll-i18n.js';
86
+	import GlobalOption from './mescroll-uni-option.js';
87
+	
88
+	export default {
89
+		mixins: [WxsMixin],
90
+		components: {
91
+			MescrollTop
92
+		},
93
+		data() {
94
+			return {
95
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
96
+				viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
97
+				downHight: 0, //下拉刷新: 容器高度
98
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
99
+				upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
100
+				isShowEmpty: false, // 是否显示空布局
101
+				isShowToTop: false, // 是否显示回到顶部按钮
102
+				scrollTop: 0, // 滚动条的位置
103
+				scrollAnim: false, // 是否开启滚动动画
104
+				windowTop: 0, // 可使用窗口的顶部位置
105
+				windowBottom: 0, // 可使用窗口的底部位置
106
+				windowHeight: 0, // 可使用窗口的高度
107
+				statusBarHeight: 0 // 状态栏高度
108
+			}
109
+		},
110
+		props: {
111
+			down: Object, // 下拉刷新的参数配置
112
+			up: Object, // 上拉加载的参数配置
113
+			i18n: Object, // 国际化的参数配置
114
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
115
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
116
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
117
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
118
+			fixed: { // 是否通过fixed固定mescroll的高度, 默认true
119
+				type: Boolean,
120
+				default: true
121
+			},
122
+			height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
123
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
124
+				type: Boolean,
125
+				default: true
126
+			},
127
+			disableScroll: Boolean // 是否禁止滚动
128
+		},
129
+		computed: {
130
+			// 是否使用fixed定位 (当height有值,则不使用)
131
+			isFixed(){
132
+				return !this.height && this.fixed
133
+			},
134
+			// mescroll的高度
135
+			scrollHeight(){
136
+				if (this.isFixed) {
137
+					return "auto"
138
+				} else if(this.height){
139
+					return this.toPx(this.height) + 'px'
140
+				}else{
141
+					return "100%"
142
+				}
143
+			},
144
+			// 下拉布局往下偏移的距离 (px)
145
+			numTop() {
146
+				return this.toPx(this.top)
147
+			},
148
+			fixedTop() {
149
+				return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
150
+			},
151
+			padTop() {
152
+				return !this.isFixed ? this.numTop + 'px' : 0
153
+			},
154
+			// 上拉布局往上偏移 (px)
155
+			numBottom() {
156
+				return this.toPx(this.bottom)
157
+			},
158
+			fixedBottom() {
159
+				return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
160
+			},
161
+			padBottom() {
162
+				return !this.isFixed ? this.numBottom + 'px' : 0
163
+			},
164
+			// 是否为重置下拉的状态
165
+			isDownReset(){
166
+				return this.downLoadType===3 || this.downLoadType===4
167
+			},
168
+			// 过渡
169
+			transition() {
170
+				return this.isDownReset ? 'transform 300ms' : ''
171
+			},
172
+			translateY() {
173
+				return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : '' // transform会使fixed失效,需注意把fixed元素写在mescroll之外
174
+			},
175
+			// 列表是否可滑动
176
+			scrollable(){
177
+				if(this.disableScroll) return false
178
+				return this.downLoadType===0 || this.isDownReset
179
+			},
180
+			// 是否在加载中
181
+			isDownLoading(){
182
+				return this.downLoadType === 3
183
+			},
184
+			// 旋转的角度
185
+			downRotate(){
186
+				return this.downLoadType === 2 ? 'rotate(180deg)' : 'rotate(0deg)'
187
+			}
188
+		},
189
+		methods: {
190
+			//number,rpx,upx,px,% --> px的数值
191
+			toPx(num){
192
+				if(typeof num === "string"){
193
+					if (num.indexOf('px') !== -1) {
194
+						if(num.indexOf('rpx') !== -1) { // "10rpx"
195
+							num = num.replace('rpx', '');
196
+						} else if(num.indexOf('upx') !== -1) { // "10upx"
197
+							num = num.replace('upx', '');
198
+						} else { // "10px"
199
+							return Number(num.replace('px', ''))
200
+						}
201
+					}else if (num.indexOf('%') !== -1){
202
+						// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
203
+						let rate = Number(num.replace("%","")) / 100
204
+						return this.windowHeight * rate
205
+					}
206
+				}
207
+				return num ? uni.upx2px(Number(num)) : 0
208
+			},
209
+			//注册列表滚动事件,用于下拉刷新和上拉加载
210
+			scroll(e) {
211
+				this.mescroll.scroll(e.detail, () => {
212
+					this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
213
+				})
214
+			},
215
+			// 点击空布局的按钮回调
216
+			emptyClick() {
217
+				this.$emit('emptyclick', this.mescroll)
218
+			},
219
+			// 点击回到顶部的按钮回调
220
+			toTopClick() {
221
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
222
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
223
+			},
224
+			// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
225
+			setClientHeight() {
226
+				if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
227
+					this.isExec = true; // 避免多次获取
228
+					this.$nextTick(() => { // 确保dom已渲染
229
+						this.getClientInfo(data=>{
230
+							this.isExec = false;
231
+							if (data) {
232
+								this.mescroll.setClientHeight(data.height);
233
+							} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
234
+								this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
235
+								setTimeout(() => {
236
+									this.setClientHeight()
237
+								}, this.clientNum * 100)
238
+							}
239
+						})
240
+					})
241
+				}
242
+			},
243
+			// 获取滚动区域的信息
244
+			getClientInfo(success){
245
+				let query = uni.createSelectorQuery().in(this);
246
+				let view = query.select('#' + this.viewId);
247
+				view.boundingClientRect(data => {
248
+					success(data)
249
+				}).exec();
250
+			}
251
+		},
252
+		// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
253
+		created() {
254
+			let vm = this;
255
+
256
+			let diyOption = {
257
+				// 下拉刷新的配置
258
+				down: {
259
+					inOffset() {
260
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
261
+					},
262
+					outOffset() {
263
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
264
+					},
265
+					onMoving(mescroll, rate, downHight) {
266
+						// 下拉过程中的回调,滑动过程一直在执行;
267
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
268
+					},
269
+					showLoading(mescroll, downHight) {
270
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
271
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
272
+					},
273
+					endDownScroll() {
274
+						vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
275
+						vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
276
+						vm.downResetTimer && clearTimeout(vm.downResetTimer)
277
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
278
+							if(vm.downLoadType===4) vm.downLoadType = 0
279
+						},300)
280
+					},
281
+					// 派发下拉刷新的回调
282
+					callback: function(mescroll) {
283
+						vm.$emit('down', mescroll)
284
+					}
285
+				},
286
+				// 上拉加载的配置
287
+				up: {
288
+					// 显示加载中的回调
289
+					showLoading() {
290
+						vm.upLoadType = 1;
291
+					},
292
+					// 显示无更多数据的回调
293
+					showNoMore() {
294
+						vm.upLoadType = 2;
295
+					},
296
+					// 隐藏上拉加载的回调
297
+					hideUpScroll(mescroll) {
298
+						vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
299
+					},
300
+					// 空布局
301
+					empty: {
302
+						onShow(isShow) { // 显示隐藏的回调
303
+							vm.isShowEmpty = isShow;
304
+						}
305
+					},
306
+					// 回到顶部
307
+					toTop: {
308
+						onShow(isShow) { // 显示隐藏的回调
309
+							vm.isShowToTop = isShow;
310
+						}
311
+					},
312
+					// 派发上拉加载的回调
313
+					callback: function(mescroll) {
314
+						vm.$emit('up', mescroll);
315
+						// 更新容器的高度 (多mescroll的情况)
316
+						vm.setClientHeight()
317
+					}
318
+				}
319
+			}
320
+
321
+			let i18nType = mescrollI18n.getType() // 当前语言类型
322
+			let i18nOption = {type: i18nType} // 国际化配置
323
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
324
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
325
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
326
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
327
+			let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
328
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
329
+
330
+			// 初始化MeScroll对象
331
+			vm.mescroll = new MeScroll(myOption);
332
+			vm.mescroll.viewId = vm.viewId; // 附带id
333
+			// 挂载语言包
334
+			vm.mescroll.i18n = i18nOption;
335
+			// init回调mescroll对象
336
+			vm.$emit('init', vm.mescroll);
337
+			
338
+			// 设置高度
339
+			const sys = uni.getSystemInfoSync();
340
+			if(sys.windowTop) vm.windowTop = sys.windowTop;
341
+			if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
342
+			if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
343
+			if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
344
+			// 使down的bottomOffset生效
345
+			vm.mescroll.setBodyHeight(sys.windowHeight);
346
+
347
+			// 因为使用的是scrollview,这里需自定义scrollTo
348
+			vm.mescroll.resetScrollTo((y, t) => {
349
+				vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
350
+				if(typeof y === 'string'){
351
+					// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现
352
+					vm.getClientInfo(function(rect){
353
+						let mescrollTop = rect.top // mescroll到顶部的距离
354
+						let selector;
355
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
356
+							selector = '#'+y // 不带#和. 则默认为id选择器
357
+						}else{
358
+							selector = y
359
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
360
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
361
+								selector = y.split('>>>')[1].trim()
362
+							}
363
+							// #endif
364
+						}
365
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
366
+							if (rect) {
367
+								let curY = vm.mescroll.getScrollTop()
368
+								let top = rect.top - mescrollTop
369
+								top += curY
370
+								if(!vm.isFixed) top -= vm.numTop
371
+								vm.scrollTop = curY;
372
+								vm.$nextTick(function() {
373
+									vm.scrollTop = top
374
+								})
375
+							} else{
376
+								console.error(selector + ' does not exist');
377
+							}
378
+						}).exec()
379
+					})
380
+					return;
381
+				}
382
+				let curY = vm.mescroll.getScrollTop()
383
+				if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
384
+					vm.scrollTop = curY;
385
+					vm.$nextTick(function() {
386
+						vm.scrollTop = y
387
+					})
388
+				} else {
389
+					vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
390
+						vm.scrollTop = step
391
+					}, t)
392
+				}
393
+			})
394
+			
395
+			// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
396
+			if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
397
+				vm.mescroll.optUp.toTop.safearea = vm.safearea;
398
+			}
399
+			// 全局配置监听
400
+			uni.$on("setMescrollGlobalOption", options=>{
401
+				if(!options) return;
402
+				let i18nType = options.i18n ? options.i18n.type : null
403
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
404
+					vm.mescroll.i18n.type = i18nType
405
+					mescrollI18n.setType(i18nType)
406
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
407
+				}
408
+				if(options.down){
409
+					let down = MeScroll.extend({}, options.down)
410
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
411
+				}
412
+				if(options.up){
413
+					let up = MeScroll.extend({}, options.up)
414
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
415
+				}
416
+			})
417
+		},
418
+		mounted() {
419
+			// 设置容器的高度
420
+			this.setClientHeight()
421
+		},
422
+		destroyed() {
423
+			// 注销全局配置监听
424
+			uni.$off("setMescrollGlobalOption")
425
+		}
426
+	}
427
+</script>
428
+
429
+<style>
430
+	@import "../../mescroll-uni/mescroll-uni.css";
431
+	@import "../../mescroll-uni/components/mescroll-down.css";
432
+	@import "../../mescroll-uni/components/mescroll-up.css";
433
+	@import "./components/mescroll-down.css";
434
+</style>

+ 44 - 0
src/uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-down.css

@@ -0,0 +1,44 @@
1
+/*下拉刷新--上下箭头*/
2
+.mescroll-downwarp .downwarp-arrow {
3
+	display: inline-block;
4
+	width: 20px;
5
+	height: 20px;
6
+	margin: 10px;
7
+	background-image: url(https://www.mescroll.com/img/xinlang/mescroll-arrow.png);
8
+	background-size: contain;
9
+	vertical-align: middle;
10
+	transition: all 300ms;
11
+}
12
+
13
+/*下拉刷新--旋转进度条*/
14
+.mescroll-downwarp .downwarp-progress{
15
+	width: 36px;
16
+	height: 36px;
17
+	border: none;
18
+	margin: auto;
19
+	background-size: contain;
20
+	animation: progressRotate 0.6s steps(6, start) infinite;
21
+}
22
+@keyframes progressRotate {
23
+	0% {
24
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png);
25
+	}
26
+	16% {
27
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress2.png);
28
+	}
29
+	32% {
30
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress3.png);
31
+	}
32
+	48% {
33
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress4.png);
34
+	}
35
+	64% {
36
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress5.png);
37
+	}
38
+	80% {
39
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress6.png);
40
+	}
41
+	100% {
42
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png);
43
+	}
44
+}

+ 53 - 0
src/uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-down.vue

@@ -0,0 +1,53 @@
1
+<!-- 下拉刷新区域 -->
2
+<template>
3
+	<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}">
4
+		<view class="downwarp-content">
5
+			<view v-if="isDownLoading" class="downwarp-progress"></view>
6
+			<view v-else class="downwarp-arrow" :style="{ transform: downRotate }"></view>
7
+			<view class="downwarp-tip">{{ downText }}</view>
8
+		</view>
9
+	</view>
10
+</template>
11
+
12
+<script>
13
+export default {
14
+	props: {
15
+		option: Object, // down的配置项
16
+		type: Number // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
17
+	},
18
+	computed: {
19
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
20
+		mOption() {
21
+			return this.option || {};
22
+		},
23
+		// 是否在加载中
24
+		isDownLoading() {
25
+			return this.type === 3;
26
+		},
27
+		// 旋转的角度
28
+		downRotate() {
29
+			return this.type === 2 ? 'rotate(-180deg)' : 'rotate(0deg)';
30
+		},
31
+		// 文本提示
32
+		downText() {
33
+			switch (this.type) {
34
+				case 1:
35
+					return this.mOption.textInOffset;
36
+				case 2:
37
+					return this.mOption.textOutOffset;
38
+				case 3:
39
+					return this.mOption.textLoading;
40
+				case 4:
41
+					return this.mOption.textLoading;
42
+				default:
43
+					return this.mOption.textInOffset;
44
+			}
45
+		}
46
+	}
47
+};
48
+</script>
49
+
50
+<style>
51
+@import '../../../mescroll-uni/components/mescroll-down.css';
52
+@import './mescroll-down.css';
53
+</style>

+ 32 - 0
src/uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-up.css

@@ -0,0 +1,32 @@
1
+/*上拉加载--旋转进度条*/
2
+.mescroll-upwarp .upwarp-progress {
3
+	width: 36px;
4
+	height: 36px;
5
+	border: none;
6
+	margin: auto;
7
+	background-size: contain;
8
+	animation: progressRotate 0.6s steps(6, start) infinite;
9
+}
10
+@keyframes progressRotate {
11
+	0% {
12
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png);
13
+	}
14
+	16% {
15
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress2.png);
16
+	}
17
+	32% {
18
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress3.png);
19
+	}
20
+	48% {
21
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress4.png);
22
+	}
23
+	64% {
24
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress5.png);
25
+	}
26
+	80% {
27
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress6.png);
28
+	}
29
+	100% {
30
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png);
31
+	}
32
+}

+ 40 - 0
src/uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-up.vue

@@ -0,0 +1,40 @@
1
+<!-- 上拉加载区域 -->
2
+<template>
3
+	<view class="mescroll-upwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}">
4
+		<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
5
+		<view v-show="isUpLoading">
6
+			<view class="upwarp-progress mescroll-rotate"></view>
7
+			<view class="upwarp-tip">{{ mOption.textLoading }}</view>
8
+		</view>
9
+		<!-- 无数据 -->
10
+		<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
11
+	</view>
12
+</template>
13
+
14
+<script>
15
+export default {
16
+	props: {
17
+		option: Object, // up的配置项
18
+		type: Number // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
19
+	},
20
+	computed: {
21
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
22
+		mOption() {
23
+			return this.option || {};
24
+		},
25
+		// 加载中
26
+		isUpLoading() {
27
+			return this.type === 1;
28
+		},
29
+		// 没有更多了
30
+		isUpNoMore() {
31
+			return this.type === 2;
32
+		}
33
+	}
34
+};
35
+</script>
36
+
37
+<style>
38
+@import '../../../mescroll-uni/components/mescroll-up.css';
39
+@import './mescroll-up.css';
40
+</style>

+ 380 - 0
src/uni_modules/mescroll-uni/components/mescroll-diy/xinlang/mescroll-body.vue

@@ -0,0 +1,380 @@
1
+<template>
2
+	<view 
3
+		class="mescroll-body mescroll-render-touch" 
4
+		:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" 
5
+		:class="{'mescorll-sticky': sticky}"
6
+		@touchstart="wxsBiz.touchstartEvent" 
7
+		@touchmove="wxsBiz.touchmoveEvent" 
8
+		@touchend="wxsBiz.touchendEvent" 
9
+		@touchcancel="wxsBiz.touchendEvent"
10
+		:change:prop="wxsBiz.propObserver"
11
+		:prop="wxsProp"
12
+		>
13
+		
14
+		<!-- 状态栏 -->
15
+		<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
16
+		
17
+		<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp">
18
+			<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
19
+			<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType"></mescroll-down> -->
20
+			<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
21
+				<view class="downwarp-content">
22
+					<view v-if="isDownLoading" class="downwarp-progress"></view>
23
+					<view v-else class="downwarp-arrow" :style="{ transform: downRotate }"></view>
24
+					<view class="downwarp-tip">{{ downText }}</view>
25
+				</view>
26
+			</view>
27
+			
28
+			<!-- 列表内容 -->
29
+			<slot></slot>
30
+
31
+			<!-- 空布局 -->
32
+			<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
33
+
34
+			<!-- 上拉加载区域 (下拉刷新时不显示,支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
35
+			<!-- <mescroll-up v-if="mescroll.optUp.use && downLoadType !== 3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
36
+			<view class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
37
+				<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
38
+				<view v-show="upLoadType===1">
39
+					<view class="upwarp-progress mescroll-rotate"></view>
40
+					<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
41
+				</view>
42
+				<!-- 无数据 -->
43
+				<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
44
+			</view>
45
+		</view>
46
+		
47
+		<!-- 底部是否偏移TabBar的高度(仅H5端生效) -->
48
+		<!-- #ifdef H5 -->
49
+		<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
50
+		<!-- #endif -->
51
+		
52
+		<!-- 适配iPhoneX -->
53
+		<view v-if="safearea" class="mescroll-safearea"></view>
54
+		
55
+		<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
56
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
57
+		
58
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
59
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
60
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
61
+		<!-- #endif -->
62
+	</view>
63
+</template>
64
+
65
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
66
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
67
+<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
68
+<!-- #endif -->
69
+
70
+<!-- app, h5使用renderjs -->
71
+<!-- #ifdef APP-PLUS || H5 -->
72
+<script module="renderBiz" lang="renderjs">
73
+	import renderBiz from '../../mescroll-uni/wxs/renderjs.js';
74
+	export default {
75
+		mixins: [renderBiz]
76
+	}
77
+</script>
78
+<!-- #endif -->
79
+
80
+<script>
81
+	import MeScroll from '../../mescroll-uni/mescroll-uni.js';
82
+	import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
83
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
84
+	import mescrollI18n from '../../mescroll-uni/mescroll-i18n.js';
85
+	import GlobalOption from './mescroll-uni-option.js';
86
+
87
+	export default {
88
+		mixins: [WxsMixin],
89
+		components: {
90
+			MescrollTop
91
+		},
92
+		data() {
93
+			return {
94
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
95
+				downHight: 0, //下拉刷新: 容器高度
96
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
97
+				upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
98
+				isShowEmpty: false, // 是否显示空布局
99
+				isShowToTop: false, // 是否显示回到顶部按钮
100
+				windowHeight: 0, // 可使用窗口的高度
101
+				windowBottom: 0, // 可使用窗口的底部位置
102
+				statusBarHeight: 0 // 状态栏高度
103
+			};
104
+		},
105
+		props: {
106
+			down: Object, // 下拉刷新的参数配置
107
+			up: Object, // 上拉加载的参数配置
108
+			i18n: Object, // 国际化的参数配置
109
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
110
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
111
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
112
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
113
+			height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
114
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
115
+				type: Boolean,
116
+				default: true
117
+			},
118
+			sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏
119
+		},
120
+		computed: {
121
+			// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
122
+			minHeight(){
123
+				return this.toPx(this.height || '100%') + 'px'
124
+			},
125
+			// 下拉布局往下偏移的距离 (px)
126
+			numTop() {
127
+				return this.toPx(this.top)
128
+			},
129
+			padTop() {
130
+				return this.numTop + 'px';
131
+			},
132
+			// 上拉布局往上偏移 (px)
133
+			numBottom() {
134
+				return this.toPx(this.bottom);
135
+			},
136
+			padBottom() {
137
+				return this.numBottom + 'px';
138
+			},
139
+			// 是否为重置下拉的状态
140
+			isDownReset() {
141
+				return this.downLoadType === 3 || this.downLoadType === 4;
142
+			},
143
+			// 过渡
144
+			transition() {
145
+				return this.isDownReset ? 'transform 300ms' : '';
146
+			},
147
+			translateY() {
148
+				return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
149
+			},
150
+			// 是否在加载中
151
+			isDownLoading() {
152
+				return this.downLoadType === 3;
153
+			},
154
+			// 旋转的角度
155
+			downRotate() {
156
+				return this.downLoadType === 2 ? 'rotate(-180deg)' : 'rotate(0deg)';
157
+			},
158
+			// 文本提示
159
+			downText() {
160
+				if(!this.mescroll) return "";
161
+				switch (this.downLoadType) {
162
+					case 1:
163
+						return this.mescroll.optDown.textInOffset;
164
+					case 2:
165
+						return this.mescroll.optDown.textOutOffset;
166
+					case 3:
167
+						return this.mescroll.optDown.textLoading;
168
+					case 4:
169
+						return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
170
+					default:
171
+						return this.mescroll.optDown.textInOffset;
172
+				}
173
+			}
174
+		},
175
+		methods: {
176
+			//number,rpx,upx,px,% --> px的数值
177
+			toPx(num) {
178
+				if (typeof num === 'string') {
179
+					if (num.indexOf('px') !== -1) {
180
+						if (num.indexOf('rpx') !== -1) {
181
+							// "10rpx"
182
+							num = num.replace('rpx', '');
183
+						} else if (num.indexOf('upx') !== -1) {
184
+							// "10upx"
185
+							num = num.replace('upx', '');
186
+						} else {
187
+							// "10px"
188
+							return Number(num.replace('px', ''));
189
+						}
190
+					} else if (num.indexOf('%') !== -1) {
191
+						// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
192
+						let rate = Number(num.replace('%', '')) / 100;
193
+						return this.windowHeight * rate;
194
+					}
195
+				}
196
+				return num ? uni.upx2px(Number(num)) : 0;
197
+			},
198
+			// 点击空布局的按钮回调
199
+			emptyClick() {
200
+				this.$emit('emptyclick', this.mescroll);
201
+			},
202
+			// 点击回到顶部的按钮回调
203
+			toTopClick() {
204
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
205
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
206
+			}
207
+		},
208
+		// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
209
+		created() {
210
+			let vm = this;
211
+
212
+			let diyOption = {
213
+				// 下拉刷新的配置
214
+				down: {
215
+					inOffset() {
216
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
217
+					},
218
+					outOffset() {
219
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
220
+					},
221
+					onMoving(mescroll, rate, downHight) {
222
+						// 下拉过程中的回调,滑动过程一直在执行;
223
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
224
+					},
225
+					showLoading(mescroll, downHight) {
226
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
227
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
228
+					},
229
+					beforeEndDownScroll(mescroll){
230
+						vm.downLoadType = 4; 
231
+						return mescroll.optDown.beforeEndDelay // 延时结束的时长
232
+					},
233
+					endDownScroll() {
234
+						vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
235
+						vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
236
+						if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时
237
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset
238
+							if(vm.downLoadType === 4) vm.downLoadType = 0
239
+						},300)
240
+					},
241
+					// 派发下拉刷新的回调
242
+					callback: function(mescroll) {
243
+						vm.$emit('down', mescroll);
244
+					}
245
+				},
246
+				// 上拉加载的配置
247
+				up: {
248
+					// 显示加载中的回调
249
+					showLoading() {
250
+						vm.upLoadType = 1;
251
+					},
252
+					// 显示无更多数据的回调
253
+					showNoMore() {
254
+						vm.upLoadType = 2;
255
+					},
256
+					// 隐藏上拉加载的回调
257
+					hideUpScroll(mescroll) {
258
+						vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
259
+					},
260
+					// 空布局
261
+					empty: {
262
+						onShow(isShow) {
263
+							// 显示隐藏的回调
264
+							vm.isShowEmpty = isShow;
265
+						}
266
+					},
267
+					// 回到顶部
268
+					toTop: {
269
+						onShow(isShow) {
270
+							// 显示隐藏的回调
271
+							vm.isShowToTop = isShow;
272
+						}
273
+					},
274
+					// 派发上拉加载的回调
275
+					callback: function(mescroll) {
276
+						vm.$emit('up', mescroll);
277
+					}
278
+				}
279
+			};
280
+
281
+			let i18nType = mescrollI18n.getType() // 当前语言类型
282
+			let i18nOption = {type: i18nType} // 国际化配置
283
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
284
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
285
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
286
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
287
+			let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响
288
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
289
+
290
+			// 初始化MeScroll对象
291
+			vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
292
+			// 挂载语言包
293
+			vm.mescroll.i18n = i18nOption;
294
+			// init回调mescroll对象
295
+			vm.$emit('init', vm.mescroll);
296
+
297
+			// 设置高度
298
+			const sys = uni.getSystemInfoSync();
299
+			if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
300
+			if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
301
+			if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
302
+			// 使down的bottomOffset生效
303
+			vm.mescroll.setBodyHeight(sys.windowHeight);
304
+			
305
+			// 因为使用的是page的scroll,这里需自定义scrollTo
306
+			vm.mescroll.resetScrollTo((y, t) => {
307
+				if(typeof y === 'string'){
308
+					// 滚动到指定view (y为css选择器)
309
+					setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
310
+						let selector;
311
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
312
+							selector = '#'+y // 不带#和. 则默认为id选择器
313
+						}else{
314
+							selector = y
315
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
316
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
317
+								selector = y.split('>>>')[1].trim()
318
+							}
319
+							// #endif
320
+						}
321
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
322
+							if (rect) {
323
+								let top = rect.top
324
+								top += vm.mescroll.getScrollTop()
325
+								uni.pageScrollTo({
326
+									scrollTop: top,
327
+									duration: t
328
+								})
329
+							} else{
330
+								console.error(selector + ' does not exist');
331
+							}
332
+						}).exec()
333
+					},30)
334
+				} else{
335
+					// 滚动到指定位置 (y必须为数字)
336
+					uni.pageScrollTo({
337
+						scrollTop: y,
338
+						duration: t
339
+					})
340
+				}
341
+			});
342
+			
343
+			// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
344
+			if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
345
+				vm.mescroll.optUp.toTop.safearea = vm.safearea;
346
+			}
347
+			
348
+			// 全局配置监听
349
+			uni.$on("setMescrollGlobalOption", options=>{
350
+				if(!options) return;
351
+				let i18nType = options.i18n ? options.i18n.type : null
352
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
353
+					vm.mescroll.i18n.type = i18nType
354
+					mescrollI18n.setType(i18nType)
355
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
356
+				}
357
+				if(options.down){
358
+					let down = MeScroll.extend({}, options.down)
359
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
360
+				}
361
+				if(options.up){
362
+					let up = MeScroll.extend({}, options.up)
363
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
364
+				}
365
+			})
366
+		},
367
+		destroyed() {
368
+			// 注销全局配置监听
369
+			uni.$off("setMescrollGlobalOption")
370
+		}
371
+	};
372
+</script>
373
+
374
+<style>
375
+	@import "../../mescroll-uni/mescroll-uni.css";
376
+	@import "../../mescroll-uni/components/mescroll-down.css";
377
+	@import "../../mescroll-uni/components/mescroll-up.css";
378
+	@import "./components/mescroll-down.css";
379
+	@import "./components/mescroll-up.css";
380
+</style>

+ 71 - 0
src/uni_modules/mescroll-uni/components/mescroll-diy/xinlang/mescroll-uni-option.js

@@ -0,0 +1,71 @@
1
+/*
2
+ * @Author: wyd
3
+ * @Date: 2024-03
4
+ * @LastEditors: wyd
5
+ * @LastEditTime: 2024-03
6
+ * @Description:
7
+ */
8
+// 全局配置
9
+// mescroll-body 和 mescroll-uni 通用
10
+const GlobalOption = {
11
+  down: {
12
+    // 其他down的配置参数也可以写,这里只展示了常用的配置:
13
+    offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
14
+    native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
15
+  },
16
+  up: {
17
+    // 其他up的配置参数也可以写,这里只展示了常用的配置:
18
+    offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
19
+    toTop: {
20
+      // 回到顶部按钮,需配置src才显示
21
+      src: 'https://www.mescroll.com/img/mescroll-totop.png', // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
22
+      offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
23
+      right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
24
+      bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
25
+      width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
26
+    },
27
+    empty: {
28
+      use: true, // 是否显示空布局
29
+      icon: 'https://www.mescroll.com/img/mescroll-empty.png' // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
30
+    }
31
+  },
32
+  // 国际化配置
33
+  i18n: {
34
+    // 中文
35
+    zh: {
36
+      down: {
37
+        textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
38
+        textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
39
+        textLoading: '加载中 ...', // 加载中的提示文本
40
+        textSuccess: '加载成功', // 加载成功的文本
41
+        textErr: '加载失败' // 加载失败的文本
42
+      },
43
+      up: {
44
+        textLoading: '加载中 ...', // 加载中的提示文本
45
+        textNoMore: '-- END --', // 没有更多数据的提示文本
46
+        empty: {
47
+          tip: '暂无数据' // 空提示
48
+        }
49
+      }
50
+    },
51
+    // 英文
52
+    en: {
53
+      down: {
54
+        textInOffset: 'drop down refresh',
55
+        textOutOffset: 'release updates',
56
+        textLoading: 'loading ...',
57
+        textSuccess: 'loaded successfully',
58
+        textErr: 'loading failed'
59
+      },
60
+      up: {
61
+        textLoading: 'loading ...',
62
+        textNoMore: '-- END --',
63
+        empty: {
64
+          tip: '~ absolutely empty ~'
65
+        }
66
+      }
67
+    }
68
+  }
69
+}
70
+
71
+export default GlobalOption

+ 459 - 0
src/uni_modules/mescroll-uni/components/mescroll-diy/xinlang/mescroll-uni.vue

@@ -0,0 +1,459 @@
1
+<template>
2
+	<view class="mescroll-uni-warp">
3
+		<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">
4
+			<view class="mescroll-uni-content mescroll-render-touch"
5
+			@touchstart="wxsBiz.touchstartEvent" 
6
+			@touchmove="wxsBiz.touchmoveEvent" 
7
+			@touchend="wxsBiz.touchendEvent" 
8
+			@touchcancel="wxsBiz.touchendEvent"
9
+			:change:prop="wxsBiz.propObserver"
10
+			:prop="wxsProp">
11
+			
12
+				<!-- 状态栏 -->
13
+				<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
14
+				
15
+				<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp">
16
+					<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
17
+					<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType"></mescroll-down> -->
18
+					<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
19
+						<view class="downwarp-content">
20
+							<view v-if="isDownLoading" class="downwarp-progress"></view>
21
+							<view v-else class="downwarp-arrow" :style="{ transform: downRotate }"></view>
22
+							<view class="downwarp-tip">{{ downText }}</view>
23
+						</view>
24
+					</view>
25
+
26
+					<!-- 列表内容 -->
27
+					<slot></slot>
28
+
29
+					<!-- 空布局 -->
30
+					<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
31
+
32
+					<!-- 上拉加载区域 (下拉刷新时不显示,支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
33
+					<!-- <mescroll-up v-if="mescroll.optUp.use && downLoadType !== 3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
34
+					<view class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
35
+						<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
36
+						<view v-show="upLoadType===1">
37
+							<view class="upwarp-progress mescroll-rotate"></view>
38
+							<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
39
+						</view>
40
+						<!-- 无数据 -->
41
+						<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
42
+					</view>
43
+				</view>
44
+				
45
+				<!-- 底部是否偏移TabBar的高度(仅H5端生效) -->
46
+				<!-- #ifdef H5 -->
47
+				<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
48
+				<!-- #endif -->
49
+				
50
+				<!-- 适配iPhoneX -->
51
+				<view v-if="safearea" class="mescroll-safearea"></view>
52
+			
53
+			</view>
54
+		</scroll-view>
55
+
56
+		<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
57
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
58
+		
59
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
60
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
61
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
62
+		<!-- #endif -->
63
+	</view>
64
+</template>
65
+
66
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
67
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
68
+<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
69
+<!-- #endif -->
70
+
71
+<!-- app, h5使用renderjs -->
72
+<!-- #ifdef APP-PLUS || H5 -->
73
+<script module="renderBiz" lang="renderjs">
74
+	import renderBiz from '../../mescroll-uni/wxs/renderjs.js';
75
+	export default {
76
+		mixins: [renderBiz]
77
+	}
78
+</script>
79
+<!-- #endif -->
80
+
81
+<script>
82
+	import MeScroll from '../../mescroll-uni/mescroll-uni.js';
83
+	import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
84
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
85
+	import mescrollI18n from '../../mescroll-uni/mescroll-i18n.js';
86
+	import GlobalOption from './mescroll-uni-option.js';
87
+	
88
+	export default {
89
+		mixins: [WxsMixin],
90
+		components: {
91
+			MescrollTop
92
+		},
93
+		data() {
94
+			return {
95
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
96
+				viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
97
+				downHight: 0, //下拉刷新: 容器高度
98
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
99
+				upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
100
+				isShowEmpty: false, // 是否显示空布局
101
+				isShowToTop: false, // 是否显示回到顶部按钮
102
+				scrollTop: 0, // 滚动条的位置
103
+				scrollAnim: false, // 是否开启滚动动画
104
+				windowTop: 0, // 可使用窗口的顶部位置
105
+				windowBottom: 0, // 可使用窗口的底部位置
106
+				windowHeight: 0, // 可使用窗口的高度
107
+				statusBarHeight: 0 // 状态栏高度
108
+			}
109
+		},
110
+		props: {
111
+			down: Object, // 下拉刷新的参数配置
112
+			up: Object, // 上拉加载的参数配置
113
+			i18n: Object, // 国际化的参数配置
114
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
115
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
116
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
117
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
118
+			fixed: { // 是否通过fixed固定mescroll的高度, 默认true
119
+				type: Boolean,
120
+				default: true
121
+			},
122
+			height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
123
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
124
+				type: Boolean,
125
+				default: true
126
+			},
127
+			disableScroll: Boolean // 是否禁止滚动
128
+		},
129
+		computed: {
130
+			// 是否使用fixed定位 (当height有值,则不使用)
131
+			isFixed(){
132
+				return !this.height && this.fixed
133
+			},
134
+			// mescroll的高度
135
+			scrollHeight(){
136
+				if (this.isFixed) {
137
+					return "auto"
138
+				} else if(this.height){
139
+					return this.toPx(this.height) + 'px'
140
+				}else{
141
+					return "100%"
142
+				}
143
+			},
144
+			// 下拉布局往下偏移的距离 (px)
145
+			numTop() {
146
+				return this.toPx(this.top)
147
+			},
148
+			fixedTop() {
149
+				return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
150
+			},
151
+			padTop() {
152
+				return !this.isFixed ? this.numTop + 'px' : 0
153
+			},
154
+			// 上拉布局往上偏移 (px)
155
+			numBottom() {
156
+				return this.toPx(this.bottom)
157
+			},
158
+			fixedBottom() {
159
+				return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
160
+			},
161
+			padBottom() {
162
+				return !this.isFixed ? this.numBottom + 'px' : 0
163
+			},
164
+			// 是否为重置下拉的状态
165
+			isDownReset(){
166
+				return this.downLoadType===3 || this.downLoadType===4
167
+			},
168
+			// 过渡
169
+			transition() {
170
+				return this.isDownReset ? 'transform 300ms' : ''
171
+			},
172
+			translateY() {
173
+				return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : '' // transform会使fixed失效,需注意把fixed元素写在mescroll之外
174
+			},
175
+			// 列表是否可滑动
176
+			scrollable(){
177
+				if(this.disableScroll) return false
178
+				return this.downLoadType===0 || this.isDownReset
179
+			},
180
+			// 是否在加载中
181
+			isDownLoading() {
182
+				return this.downLoadType === 3;
183
+			},
184
+			// 旋转的角度
185
+			downRotate() {
186
+				return this.downLoadType === 2 ? 'rotate(-180deg)' : 'rotate(0deg)';
187
+			},
188
+			// 文本提示
189
+			downText() {
190
+				if(!this.mescroll) return "";
191
+				switch (this.downLoadType) {
192
+					case 1:
193
+						return this.mescroll.optDown.textInOffset;
194
+					case 2:
195
+						return this.mescroll.optDown.textOutOffset;
196
+					case 3:
197
+						return this.mescroll.optDown.textLoading;
198
+					case 4:
199
+						return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
200
+					default:
201
+						return this.mescroll.optDown.textInOffset;
202
+				}
203
+			}
204
+		},
205
+		methods: {
206
+			//number,rpx,upx,px,% --> px的数值
207
+			toPx(num){
208
+				if(typeof num === "string"){
209
+					if (num.indexOf('px') !== -1) {
210
+						if(num.indexOf('rpx') !== -1) { // "10rpx"
211
+							num = num.replace('rpx', '');
212
+						} else if(num.indexOf('upx') !== -1) { // "10upx"
213
+							num = num.replace('upx', '');
214
+						} else { // "10px"
215
+							return Number(num.replace('px', ''))
216
+						}
217
+					}else if (num.indexOf('%') !== -1){
218
+						// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
219
+						let rate = Number(num.replace("%","")) / 100
220
+						return this.windowHeight * rate
221
+					}
222
+				}
223
+				return num ? uni.upx2px(Number(num)) : 0
224
+			},
225
+			//注册列表滚动事件,用于下拉刷新和上拉加载
226
+			scroll(e) {
227
+				this.mescroll.scroll(e.detail, () => {
228
+					this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
229
+				})
230
+			},
231
+			// 点击空布局的按钮回调
232
+			emptyClick() {
233
+				this.$emit('emptyclick', this.mescroll)
234
+			},
235
+			// 点击回到顶部的按钮回调
236
+			toTopClick() {
237
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
238
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
239
+			},
240
+			// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
241
+			setClientHeight() {
242
+				if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
243
+					this.isExec = true; // 避免多次获取
244
+					this.$nextTick(() => { // 确保dom已渲染
245
+						this.getClientInfo(data=>{
246
+							this.isExec = false;
247
+							if (data) {
248
+								this.mescroll.setClientHeight(data.height);
249
+							} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
250
+								this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
251
+								setTimeout(() => {
252
+									this.setClientHeight()
253
+								}, this.clientNum * 100)
254
+							}
255
+						})
256
+					})
257
+				}
258
+			},
259
+			// 获取滚动区域的信息
260
+			getClientInfo(success){
261
+				let query = uni.createSelectorQuery().in(this);
262
+				let view = query.select('#' + this.viewId);
263
+				view.boundingClientRect(data => {
264
+					success(data)
265
+				}).exec();
266
+			}
267
+		},
268
+		// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
269
+		created() {
270
+			let vm = this;
271
+
272
+			let diyOption = {
273
+				// 下拉刷新的配置
274
+				down: {
275
+					inOffset() {
276
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
277
+					},
278
+					outOffset() {
279
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
280
+					},
281
+					onMoving(mescroll, rate, downHight) {
282
+						// 下拉过程中的回调,滑动过程一直在执行;
283
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
284
+					},
285
+					showLoading(mescroll, downHight) {
286
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
287
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
288
+					},
289
+					beforeEndDownScroll(mescroll){
290
+						vm.downLoadType = 4; 
291
+						return mescroll.optDown.beforeEndDelay // 延时结束的时长
292
+					},
293
+					endDownScroll() {
294
+						vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
295
+						vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
296
+						vm.downResetTimer && clearTimeout(vm.downResetTimer)
297
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
298
+							if(vm.downLoadType===4) vm.downLoadType = 0
299
+						},300)
300
+					},
301
+					// 派发下拉刷新的回调
302
+					callback: function(mescroll) {
303
+						vm.$emit('down', mescroll)
304
+					}
305
+				},
306
+				// 上拉加载的配置
307
+				up: {
308
+					// 显示加载中的回调
309
+					showLoading() {
310
+						vm.upLoadType = 1;
311
+					},
312
+					// 显示无更多数据的回调
313
+					showNoMore() {
314
+						vm.upLoadType = 2;
315
+					},
316
+					// 隐藏上拉加载的回调
317
+					hideUpScroll(mescroll) {
318
+						vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
319
+					},
320
+					// 空布局
321
+					empty: {
322
+						onShow(isShow) { // 显示隐藏的回调
323
+							vm.isShowEmpty = isShow;
324
+						}
325
+					},
326
+					// 回到顶部
327
+					toTop: {
328
+						onShow(isShow) { // 显示隐藏的回调
329
+							vm.isShowToTop = isShow;
330
+						}
331
+					},
332
+					// 派发上拉加载的回调
333
+					callback: function(mescroll) {
334
+						vm.$emit('up', mescroll);
335
+						// 更新容器的高度 (多mescroll的情况)
336
+						vm.setClientHeight()
337
+					}
338
+				}
339
+			}
340
+
341
+			let i18nType = mescrollI18n.getType() // 当前语言类型
342
+			let i18nOption = {type: i18nType} // 国际化配置
343
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
344
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
345
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
346
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
347
+			let myOption = JSON.parse(JSON.stringify({
348
+				'down': vm.down,
349
+				'up': vm.up
350
+			})) // 深拷贝,避免对props的影响
351
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
352
+
353
+			// 初始化MeScroll对象
354
+			vm.mescroll = new MeScroll(myOption);
355
+			vm.mescroll.viewId = vm.viewId; // 附带id
356
+			// 挂载语言包
357
+			vm.mescroll.i18n = i18nOption;
358
+			// init回调mescroll对象
359
+			vm.$emit('init', vm.mescroll);
360
+			
361
+			// 设置高度
362
+			const sys = uni.getSystemInfoSync();
363
+			if(sys.windowTop) vm.windowTop = sys.windowTop;
364
+			if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
365
+			if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
366
+			if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
367
+			// 使down的bottomOffset生效
368
+			vm.mescroll.setBodyHeight(sys.windowHeight);
369
+
370
+			// 因为使用的是scrollview,这里需自定义scrollTo
371
+			vm.mescroll.resetScrollTo((y, t) => {
372
+				vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
373
+				if(typeof y === 'string'){
374
+					// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现
375
+					vm.getClientInfo(function(rect){
376
+						let mescrollTop = rect.top // mescroll到顶部的距离
377
+						let selector;
378
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
379
+							selector = '#'+y // 不带#和. 则默认为id选择器
380
+						}else{
381
+							selector = y
382
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
383
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
384
+								selector = y.split('>>>')[1].trim()
385
+							}
386
+							// #endif
387
+						}
388
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
389
+							if (rect) {
390
+								let curY = vm.mescroll.getScrollTop()
391
+								let top = rect.top - mescrollTop
392
+								top += curY
393
+								if(!vm.isFixed) top -= vm.numTop
394
+								vm.scrollTop = curY;
395
+								vm.$nextTick(function() {
396
+									vm.scrollTop = top
397
+								})
398
+							} else{
399
+								console.error(selector + ' does not exist');
400
+							}
401
+						}).exec()
402
+					})
403
+					return;
404
+				}
405
+				let curY = vm.mescroll.getScrollTop()
406
+				if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
407
+					vm.scrollTop = curY;
408
+					vm.$nextTick(function() {
409
+						vm.scrollTop = y
410
+					})
411
+				} else {
412
+					vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
413
+						vm.scrollTop = step
414
+					}, t)
415
+				}
416
+			})
417
+			
418
+			// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
419
+			if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
420
+				vm.mescroll.optUp.toTop.safearea = vm.safearea;
421
+			}
422
+			
423
+			// 全局配置监听
424
+			uni.$on("setMescrollGlobalOption", options=>{
425
+				if(!options) return;
426
+				let i18nType = options.i18n ? options.i18n.type : null
427
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
428
+					vm.mescroll.i18n.type = i18nType
429
+					mescrollI18n.setType(i18nType)
430
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
431
+				}
432
+				if(options.down){
433
+					let down = MeScroll.extend({}, options.down)
434
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
435
+				}
436
+				if(options.up){
437
+					let up = MeScroll.extend({}, options.up)
438
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
439
+				}
440
+			})
441
+		},
442
+		mounted() {
443
+			// 设置容器的高度
444
+			this.setClientHeight()
445
+		},
446
+		destroyed() {
447
+			// 注销全局配置监听
448
+			uni.$off("setMescrollGlobalOption")
449
+		}
450
+	}
451
+</script>
452
+
453
+<style>
454
+	@import "../../mescroll-uni/mescroll-uni.css";
455
+	@import "../../mescroll-uni/components/mescroll-down.css";
456
+	@import "../../mescroll-uni/components/mescroll-up.css";
457
+	@import "./components/mescroll-down.css";
458
+	@import "./components/mescroll-up.css";
459
+</style>

+ 116 - 0
src/uni_modules/mescroll-uni/components/mescroll-empty/mescroll-empty.vue

@@ -0,0 +1,116 @@
1
+<!--空布局:
2
+遵循easycom规范, 可作为独立的组件, 不使用mescroll的页面也能使用:
3
+<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
4
+-->
5
+<template>
6
+	<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
7
+		<view> <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" /> </view>
8
+		<view v-if="tip" class="empty-tip">{{ tip }}</view>
9
+		<view v-if="btnText" class="empty-btn" @click="emptyClick">{{ btnText }}</view>
10
+	</view>
11
+</template>
12
+
13
+<script>
14
+// 引入全局配置
15
+import GlobalOption from '../mescroll-uni/mescroll-uni-option.js';
16
+// 引入国际化工具类
17
+import mescrollI18n from '../mescroll-uni/mescroll-i18n.js';
18
+export default {
19
+	props: {
20
+		// empty的配置项: 默认为GlobalOption.up.empty
21
+		option: {
22
+			type: Object,
23
+			default() {
24
+				return {};
25
+			}
26
+		}
27
+	},
28
+	// 使用computed获取配置,用于支持option的动态配置
29
+	computed: {
30
+		// 图标
31
+		icon() {
32
+			if (this.option.icon != null) { // 此处不使用短路求值, 用于支持传空串不显示图标
33
+				return this.option.icon
34
+			} else{
35
+				let i18nType = mescrollI18n.getType() // 国际化配置
36
+				if (this.option.i18n) {
37
+					return this.option.i18n[i18nType].icon
38
+				} else{
39
+					return GlobalOption.i18n[i18nType].up.empty.icon || GlobalOption.up.empty.icon
40
+				}
41
+			}
42
+		},
43
+		// 文本提示
44
+		tip() {
45
+			if (this.option.tip != null) { // 支持传空串不显示文本提示
46
+				return this.option.tip
47
+			} else{
48
+				let i18nType = mescrollI18n.getType() // 国际化配置
49
+				if (this.option.i18n) {
50
+					return this.option.i18n[i18nType].tip
51
+				} else{
52
+					return GlobalOption.i18n[i18nType].up.empty.tip || GlobalOption.up.empty.tip
53
+				}
54
+			}
55
+		},
56
+		// 按钮文本
57
+		btnText() {
58
+			if (this.option.i18n) {
59
+				let i18nType = mescrollI18n.getType() // 国际化配置
60
+				return this.option.i18n[i18nType].btnText
61
+			} else{
62
+				return this.option.btnText
63
+			}
64
+		}
65
+	},
66
+	methods: {
67
+		// 点击按钮
68
+		emptyClick() {
69
+			this.$emit('emptyclick');
70
+		}
71
+	}
72
+};
73
+</script>
74
+
75
+<style>
76
+/* 无任何数据的空布局 */
77
+.mescroll-empty {
78
+	box-sizing: border-box;
79
+	width: 100%;
80
+	padding: 100rpx 50rpx;
81
+	text-align: center;
82
+}
83
+
84
+.mescroll-empty.empty-fixed {
85
+	z-index: 99;
86
+	position: absolute; /*transform会使fixed失效,最终会降级为absolute */
87
+	top: 100rpx;
88
+	left: 0;
89
+}
90
+
91
+.mescroll-empty .empty-icon {
92
+	width: 280rpx;
93
+	height: 280rpx;
94
+}
95
+
96
+.mescroll-empty .empty-tip {
97
+	margin-top: 20rpx;
98
+	font-size: 24rpx;
99
+	color: gray;
100
+}
101
+
102
+.mescroll-empty .empty-btn {
103
+	display: inline-block;
104
+	margin-top: 40rpx;
105
+	min-width: 200rpx;
106
+	padding: 18rpx;
107
+	font-size: 28rpx;
108
+	border: 1rpx solid #e04b28;
109
+	border-radius: 60rpx;
110
+	color: #e04b28;
111
+}
112
+
113
+.mescroll-empty .empty-btn:active {
114
+	opacity: 0.75;
115
+}
116
+</style>

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

@@ -0,0 +1,55 @@
1
+/* 下拉刷新区域 */
2
+.mescroll-downwarp {
3
+	position: absolute;
4
+	top: -100%;
5
+	left: 0;
6
+	width: 100%;
7
+	height: 100%;
8
+	text-align: center;
9
+}
10
+
11
+/* 下拉刷新--内容区,定位于区域底部 */
12
+.mescroll-downwarp .downwarp-content {
13
+	position: absolute;
14
+	left: 0;
15
+	bottom: 0;
16
+	width: 100%;
17
+	min-height: 60rpx;
18
+	padding: 20rpx 0;
19
+	text-align: center;
20
+}
21
+
22
+/* 下拉刷新--提示文本 */
23
+.mescroll-downwarp .downwarp-tip {
24
+	display: inline-block;
25
+	font-size: 28rpx;
26
+	vertical-align: middle;
27
+	margin-left: 16rpx;
28
+	/* color: gray; 已在style设置color,此处删去*/
29
+}
30
+
31
+/* 下拉刷新--旋转进度条 */
32
+.mescroll-downwarp .downwarp-progress {
33
+	display: inline-block;
34
+	width: 32rpx;
35
+	height: 32rpx;
36
+	border-radius: 50%;
37
+	border: 2rpx solid gray;
38
+	border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
39
+	vertical-align: middle;
40
+}
41
+
42
+/* 旋转动画 */
43
+.mescroll-downwarp .mescroll-rotate {
44
+	animation: mescrollDownRotate 0.6s linear infinite;
45
+}
46
+
47
+@keyframes mescrollDownRotate {
48
+	0% {
49
+		transform: rotate(0deg);
50
+	}
51
+
52
+	100% {
53
+		transform: rotate(360deg);
54
+	}
55
+}

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

@@ -0,0 +1,47 @@
1
+<!-- 下拉刷新区域 -->
2
+<template>
3
+	<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
4
+		<view class="downwarp-content">
5
+			<view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mOption.textColor, 'transform':downRotate}"></view>
6
+			<view class="downwarp-tip">{{downText}}</view>
7
+		</view>
8
+	</view>
9
+</template>
10
+
11
+<script>
12
+export default {
13
+	props: {
14
+		option: Object , // down的配置项
15
+		type: Number, // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
16
+		rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1)
17
+	},
18
+	computed: {
19
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
20
+		mOption(){
21
+			return this.option || {}
22
+		},
23
+		// 是否在加载中
24
+		isDownLoading(){
25
+			return this.type === 3
26
+		},
27
+		// 旋转的角度
28
+		downRotate(){
29
+			return 'rotate(' + 360 * this.rate + 'deg)'
30
+		},
31
+		// 文本提示
32
+		downText(){
33
+			switch (this.type){
34
+				case 1: return this.mOption.textInOffset;
35
+				case 2: return this.mOption.textOutOffset;
36
+				case 3: return this.mOption.textLoading;
37
+				case 4: return this.mOption.textLoading;
38
+				default: return this.mOption.textInOffset;
39
+			}
40
+		}
41
+	}
42
+};
43
+</script>
44
+
45
+<style>
46
+@import "./mescroll-down.css";
47
+</style>

+ 99 - 0
src/uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-top.vue

@@ -0,0 +1,99 @@
1
+<!-- 回到顶部的按钮 -->
2
+<template>
3
+	<image
4
+		v-if="option.src"
5
+		class="mescroll-totop"
6
+		:class="[isShow ? 'mescroll-totop-in' : 'mescroll-totop-out', {'mescroll-totop-safearea': option.safearea}]"
7
+		:style="{'z-index':option.zIndex, 'left': left, 'right': right, 'bottom':addUnit(option.bottom), 'width':addUnit(option.width), 'border-radius':addUnit(option.radius)}"
8
+		:src="option.src"
9
+		mode="widthFix"
10
+		@click="toTopClick"
11
+	/>
12
+</template>
13
+
14
+<script>
15
+export default {
16
+	props: {
17
+		// up.toTop的配置项
18
+		option: {
19
+			type: Object,
20
+			default(){
21
+				return {}
22
+			}
23
+		},
24
+		// 是否显示
25
+		value: false, // vue2
26
+		modelValue: false // vue3
27
+	},
28
+	computed: {
29
+		// 优先显示左边
30
+		left(){
31
+			return this.option.left ? this.addUnit(this.option.left) : 'auto';
32
+		},
33
+		// 右边距离 (优先显示左边)
34
+		right() {
35
+			return this.option.left ? 'auto' : this.addUnit(this.option.right);
36
+		},
37
+		// 是否显示
38
+		isShow(){
39
+			// #ifdef VUE3
40
+			return this.modelValue
41
+			// #endif
42
+			// #ifdef VUE2
43
+			return this.value
44
+			// #endif
45
+		}
46
+	},
47
+	methods: {
48
+		addUnit(num){
49
+			if(!num) return 0;
50
+			if(typeof num === 'number') return num + 'rpx';
51
+			return num
52
+		},
53
+		toTopClick() {
54
+			// #ifdef VUE3
55
+			this.$emit("update:modelValue", false); // 使v-model生效 vue3
56
+			// #endif
57
+			// #ifdef VUE2
58
+			this.$emit('input', false); // 使v-model生效 vue2
59
+			// #endif
60
+			this.$emit('click'); // 派发点击事件
61
+		}
62
+	}
63
+};
64
+</script>
65
+
66
+<style>
67
+/* 回到顶部的按钮 */
68
+.mescroll-totop {
69
+	z-index: 9990;
70
+	position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */
71
+	right: 20rpx;
72
+	bottom: 120rpx;
73
+	width: 72rpx;
74
+	height: auto;
75
+	border-radius: 50%;
76
+	opacity: 0;
77
+	transition: opacity 0.5s; /* 过渡 */
78
+	margin-bottom: var(--window-bottom); /* css变量 */
79
+}
80
+
81
+/* 适配 iPhoneX */
82
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
83
+	.mescroll-totop-safearea {
84
+		margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom)); /* window-bottom + 适配 iPhoneX */
85
+		margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom));
86
+	}
87
+}
88
+
89
+/* 显示 -- 淡入 */
90
+.mescroll-totop-in {
91
+	opacity: 1;
92
+}
93
+
94
+/* 隐藏 -- 淡出且不接收事件*/
95
+.mescroll-totop-out {
96
+	opacity: 0;
97
+	pointer-events: none;
98
+}
99
+</style>

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

@@ -0,0 +1,47 @@
1
+/* 上拉加载区域 */
2
+.mescroll-upwarp {
3
+	box-sizing: border-box;
4
+	min-height: 110rpx;
5
+	padding: 30rpx 0;
6
+	text-align: center;
7
+	clear: both;
8
+}
9
+
10
+/*提示文本 */
11
+.mescroll-upwarp .upwarp-tip,
12
+.mescroll-upwarp .upwarp-nodata {
13
+	display: inline-block;
14
+	font-size: 24rpx;
15
+	vertical-align: middle;
16
+	/* color: gray; 已在style设置color,此处删去*/
17
+}
18
+
19
+.mescroll-upwarp .upwarp-tip {
20
+	margin-left: 16rpx;
21
+}
22
+
23
+/*旋转进度条 */
24
+.mescroll-upwarp .upwarp-progress {
25
+	display: inline-block;
26
+	width: 32rpx;
27
+	height: 32rpx;
28
+	border-radius: 50%;
29
+	border: 2rpx solid gray;
30
+	border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
31
+	vertical-align: middle;
32
+}
33
+
34
+/* 旋转动画 */
35
+.mescroll-upwarp .mescroll-rotate {
36
+	animation: mescrollUpRotate 0.6s linear infinite;
37
+}
38
+
39
+@keyframes mescrollUpRotate {
40
+	0% {
41
+		transform: rotate(0deg);
42
+	}
43
+
44
+	100% {
45
+		transform: rotate(360deg);
46
+	}
47
+}

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

@@ -0,0 +1,39 @@
1
+<!-- 上拉加载区域 -->
2
+<template>
3
+	<view class="mescroll-upwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
4
+		<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
5
+		<view v-show="isUpLoading">
6
+			<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mOption.textColor}"></view>
7
+			<view class="upwarp-tip">{{ mOption.textLoading }}</view>
8
+		</view>
9
+		<!-- 无数据 -->
10
+		<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
11
+	</view>
12
+</template>
13
+
14
+<script>
15
+export default {
16
+	props: {
17
+		option: Object, // up的配置项
18
+		type: Number // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了)
19
+	},
20
+	computed: {
21
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
22
+		mOption() {
23
+			return this.option || {};
24
+		},
25
+		// 加载中
26
+		isUpLoading() {
27
+			return this.type === 1;
28
+		},
29
+		// 没有更多了
30
+		isUpNoMore() {
31
+			return this.type === 2;
32
+		}
33
+	}
34
+};
35
+</script>
36
+
37
+<style>
38
+@import './mescroll-up.css';
39
+</style>

+ 15 - 0
src/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-i18n.js

@@ -0,0 +1,15 @@
1
+// 国际化工具类
2
+const mescrollI18n = {
3
+	// 默认语言
4
+	def: "zh",
5
+	// 获取当前语言类型
6
+	getType(){
7
+		return uni.getStorageSync("mescroll-i18n") || this.def
8
+	},
9
+	// 设置当前语言类型
10
+	setType(type){
11
+		uni.setStorageSync("mescroll-i18n", type)
12
+	}
13
+}
14
+
15
+export default mescrollI18n

+ 46 - 0
src/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js

@@ -0,0 +1,46 @@
1
+// mescroll-body 和 mescroll-uni 通用
2
+const MescrollMixin = {
3
+	data() {
4
+		return {
5
+			mescroll: null //mescroll实例对象
6
+		}
7
+	},
8
+	// 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
9
+	onPullDownRefresh(){
10
+		this.mescroll && this.mescroll.onPullDownRefresh();
11
+	},
12
+	// 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
13
+	onPageScroll(e) {
14
+		this.mescroll && this.mescroll.onPageScroll(e);
15
+	},
16
+	// 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
17
+	onReachBottom() {
18
+		this.mescroll && this.mescroll.onReachBottom();
19
+	},
20
+	methods: {
21
+		// mescroll组件初始化的回调,可获取到mescroll对象
22
+		mescrollInit(mescroll) {
23
+			this.mescroll = mescroll;
24
+		},
25
+		// 下拉刷新的回调 (mixin默认resetUpScroll)
26
+		downCallback() {
27
+			if(this.mescroll.optUp.use){
28
+				this.mescroll.resetUpScroll()
29
+			}else{
30
+				setTimeout(()=>{
31
+					this.mescroll.endSuccess();
32
+				}, 500)
33
+			}
34
+		},
35
+		// 上拉加载的回调
36
+		upCallback() {
37
+			// mixin默认延时500自动结束加载
38
+			setTimeout(()=>{
39
+				this.mescroll.endErr();
40
+			}, 500)
41
+		}
42
+	}
43
+	
44
+}
45
+
46
+export default MescrollMixin;

+ 71 - 0
src/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni-option.js

@@ -0,0 +1,71 @@
1
+/*
2
+ * @Author: wyd
3
+ * @Date: 2024-03
4
+ * @LastEditors: wyd
5
+ * @LastEditTime: 2024-03
6
+ * @Description:
7
+ */
8
+// 全局配置
9
+// mescroll-body 和 mescroll-uni 通用
10
+const GlobalOption = {
11
+  down: {
12
+    // 其他down的配置参数也可以写,这里只展示了常用的配置:
13
+    offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
14
+    native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
15
+  },
16
+  up: {
17
+    // 其他up的配置参数也可以写,这里只展示了常用的配置:
18
+    offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
19
+    toTop: {
20
+      // 回到顶部按钮,需配置src才显示
21
+      src: '/static/images/mescroll-totop.png', // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
22
+      offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
23
+      right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
24
+      bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
25
+      width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
26
+    },
27
+    empty: {
28
+      use: true, // 是否显示空布局
29
+      icon: '/static/images/mescroll-empty.png' // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
30
+    }
31
+  },
32
+  // 国际化配置
33
+  i18n: {
34
+    // 中文
35
+    zh: {
36
+      down: {
37
+        textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
38
+        textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
39
+        textLoading: '加载中 ...', // 加载中的提示文本
40
+        textSuccess: '加载成功', // 加载成功的文本
41
+        textErr: '加载失败' // 加载失败的文本
42
+      },
43
+      up: {
44
+        textLoading: '加载中 ...', // 加载中的提示文本
45
+        textNoMore: '-- 已加载全部 --', // 没有更多数据的提示文本
46
+        empty: {
47
+          tip: '暂无数据' // 空提示
48
+        }
49
+      }
50
+    },
51
+    // 英文
52
+    en: {
53
+      down: {
54
+        textInOffset: 'drop down refresh',
55
+        textOutOffset: 'release updates',
56
+        textLoading: 'loading ...',
57
+        textSuccess: 'loaded successfully',
58
+        textErr: 'loading failed'
59
+      },
60
+      up: {
61
+        textLoading: 'loading ...',
62
+        textNoMore: '-- END --',
63
+        empty: {
64
+          tip: '~ absolutely empty ~'
65
+        }
66
+      }
67
+    }
68
+  }
69
+}
70
+
71
+export default GlobalOption

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

@@ -0,0 +1,36 @@
1
+.mescroll-uni-warp{
2
+	height: 100%;
3
+}
4
+
5
+.mescroll-uni-content{
6
+	height: 100%;
7
+}
8
+
9
+.mescroll-uni {
10
+	position: relative;
11
+	width: 100%;
12
+	height: 100%;
13
+	min-height: 200rpx;
14
+	overflow-y: auto;
15
+	box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
16
+}
17
+
18
+/* 定位的方式固定高度 */
19
+.mescroll-uni-fixed{
20
+	z-index: 1;
21
+	position: fixed;
22
+	top: 0;
23
+	left: 0;
24
+	right: 0;
25
+	bottom: 0;
26
+	width: auto; /* 使right生效 */
27
+	height: auto; /* 使bottom生效 */
28
+}
29
+
30
+/* 适配 iPhoneX */
31
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
32
+	.mescroll-safearea {
33
+		padding-bottom: constant(safe-area-inset-bottom);
34
+		padding-bottom: env(safe-area-inset-bottom);
35
+	}
36
+}

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

@@ -0,0 +1,799 @@
1
+/* mescroll
2
+ * version 1.3.7
3
+ * 2021-04-12 wenju
4
+ * https://www.mescroll.com
5
+ */
6
+
7
+export default function MeScroll(options, isScrollBody) {
8
+	let me = this;
9
+	me.version = '1.3.7'; // mescroll版本号
10
+	me.options = options || {}; // 配置
11
+	me.isScrollBody = isScrollBody || false; // 滚动区域是否为原生页面滚动; 默认为scroll-view
12
+
13
+	me.isDownScrolling = false; // 是否在执行下拉刷新的回调
14
+	me.isUpScrolling = false; // 是否在执行上拉加载的回调
15
+	let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
16
+
17
+	// 初始化下拉刷新
18
+	me.initDownScroll();
19
+	// 初始化上拉加载,则初始化
20
+	me.initUpScroll();
21
+
22
+	// 自动加载
23
+	setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
24
+		// 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
25
+		if ((me.optDown.use || me.optDown.native) && me.optDown.auto && hasDownCallback) {
26
+			if (me.optDown.autoShowLoading) {
27
+				me.triggerDownScroll(); // 显示下拉进度,执行下拉回调
28
+			} else {
29
+				me.optDown.callback && me.optDown.callback(me); // 不显示下拉进度,直接执行下拉回调
30
+			}
31
+		}
32
+		// 自动触发上拉加载
33
+		if(!me.isUpAutoLoad){ // 部分小程序(头条小程序)emit是异步, 会导致isUpAutoLoad判断有误, 先延时确保先执行down的callback,再执行up的callback
34
+			setTimeout(function(){
35
+				me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll();
36
+			},100)
37
+		}
38
+	}, 30); // 需让me.optDown.inited和me.optUp.inited先执行
39
+}
40
+
41
+/* 配置参数:下拉刷新 */
42
+MeScroll.prototype.extendDownScroll = function(optDown) {
43
+	// 下拉刷新的配置
44
+	MeScroll.extend(optDown, {
45
+		use: true, // 是否启用下拉刷新; 默认true
46
+		auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
47
+		native: false, // 是否使用系统自带的下拉刷新; 默认false; 仅mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
48
+		autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
49
+		isLock: false, // 是否锁定下拉刷新,默认false;
50
+		offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
51
+		startTop: 100, // scroll-view快速滚动到顶部时,此时的scroll-top可能大于0, 此值用于控制最大的误差
52
+		inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
53
+		outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
54
+		bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
55
+		minAngle: 45, // 向下滑动最少偏移的角度,取值区间  [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
56
+		textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
57
+		textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
58
+		textLoading: '加载中 ...', // 加载中的提示文本
59
+		textSuccess: '加载成功', // 加载成功的文本
60
+		textErr: '加载失败', // 加载失败的文本
61
+		beforeEndDelay: 0, // 延时结束的时长 (显示加载成功/失败的时长, android小程序设置此项结束下拉会卡顿, 配置后请注意测试)
62
+		bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
63
+		textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
64
+		inited: null, // 下拉刷新初始化完毕的回调
65
+		inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
66
+		outOffset: null, // 下拉的距离大于offset那一刻的回调
67
+		onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
68
+		beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
69
+		showLoading: null, // 显示下拉刷新进度的回调
70
+		afterLoading: null, // 显示下拉刷新进度的回调之后,马上要执行的代码 (如: 在wxs中使用)
71
+		beforeEndDownScroll: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
72
+		endDownScroll: null, // 结束下拉刷新的回调
73
+		afterEndDownScroll: null, // 结束下拉刷新的回调,马上要执行的代码 (如: 在wxs中使用)
74
+		callback: function(mescroll) {
75
+			// 下拉刷新的回调;默认重置上拉加载列表为第一页
76
+			mescroll.resetUpScroll();
77
+		}
78
+	})
79
+}
80
+
81
+/* 配置参数:上拉加载 */
82
+MeScroll.prototype.extendUpScroll = function(optUp) {
83
+	// 上拉加载的配置
84
+	MeScroll.extend(optUp, {
85
+		use: true, // 是否启用上拉加载; 默认true
86
+		auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
87
+		isLock: false, // 是否锁定上拉加载,默认false;
88
+		isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
89
+		callback: null, // 上拉加载的回调;function(page,mescroll){ }
90
+		page: {
91
+			num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
92
+			size: 10, // 每页数据的数量
93
+			time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
94
+		},
95
+		noMoreSize: 1, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
96
+		offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
97
+		textLoading: '加载中 ...', // 加载中的提示文本
98
+		textNoMore: '-- END --', // 没有更多数据的提示文本
99
+		bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
100
+		textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
101
+		inited: null, // 初始化完毕的回调
102
+		showLoading: null, // 显示加载中的回调
103
+		showNoMore: null, // 显示无更多数据的回调
104
+		hideUpScroll: null, // 隐藏上拉加载的回调
105
+		errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
106
+		toTop: {
107
+			// 回到顶部按钮,需配置src才显示
108
+			src: null, // 图片路径,默认null (绝对路径或网络图)
109
+			offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
110
+			duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
111
+			btnClick: null, // 点击按钮的回调
112
+			onShow: null, // 是否显示的回调
113
+			zIndex: 9990, // fixed定位z-index值
114
+			left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
115
+			right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
116
+			bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
117
+			safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
118
+			width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
119
+			radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
120
+		},
121
+		empty: {
122
+			use: true, // 是否显示空布局
123
+			icon: null, // 图标路径
124
+			tip: '~ 暂无相关数据 ~', // 提示
125
+			btnText: '', // 按钮
126
+			btnClick: null, // 点击按钮的回调
127
+			onShow: null, // 是否显示的回调
128
+			fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
129
+			top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
130
+			zIndex: 99 // fixed定位z-index值
131
+		},
132
+		onScroll: false // 是否监听滚动事件
133
+	})
134
+}
135
+
136
+/* 配置参数 */
137
+MeScroll.extend = function(userOption, defaultOption) {
138
+	if (!userOption) return defaultOption;
139
+	for (let key in defaultOption) {
140
+		if (userOption[key] == null) {
141
+			let def = defaultOption[key];
142
+			if (def != null && typeof def === 'object') {
143
+				userOption[key] = MeScroll.extend({}, def); // 深度匹配
144
+			} else {
145
+				userOption[key] = def;
146
+			}
147
+		} else if (typeof userOption[key] === 'object') {
148
+			MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
149
+		}
150
+	}
151
+	return userOption;
152
+}
153
+
154
+/* 简单判断是否配置了颜色 (非透明,非白色) */
155
+MeScroll.prototype.hasColor = function(color) {
156
+	if(!color) return false;
157
+	let c = color.toLowerCase();
158
+	return c != "#fff" && c != "#ffffff" && c != "transparent" && c != "white"
159
+}
160
+
161
+/* -------初始化下拉刷新------- */
162
+MeScroll.prototype.initDownScroll = function() {
163
+	let me = this;
164
+	// 配置参数
165
+	me.optDown = me.options.down || {};
166
+	if(!me.optDown.textColor && me.hasColor(me.optDown.bgColor)) me.optDown.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
167
+	me.extendDownScroll(me.optDown);
168
+	
169
+	// 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
170
+	if(me.isScrollBody && me.optDown.native){
171
+		me.optDown.use = false
172
+	}else{
173
+		me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
174
+	}
175
+	
176
+	me.downHight = 0; // 下拉区域的高度
177
+
178
+	// 在页面中加入下拉布局
179
+	if (me.optDown.use && me.optDown.inited) {
180
+		// 初始化完毕的回调
181
+		setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
182
+			me.optDown.inited(me);
183
+		}, 0)
184
+	}
185
+}
186
+
187
+/* 列表touchstart事件 */
188
+MeScroll.prototype.touchstartEvent = function(e) {
189
+	if (!this.optDown.use) return;
190
+
191
+	this.startPoint = this.getPoint(e); // 记录起点
192
+	this.startTop = this.getScrollTop(); // 记录此时的滚动条位置
193
+	this.startAngle = 0; // 初始角度
194
+	this.lastPoint = this.startPoint; // 重置上次move的点
195
+	this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
196
+	this.inTouchend = false; // 标记不是touchend
197
+}
198
+
199
+/* 列表touchmove事件 */
200
+MeScroll.prototype.touchmoveEvent = function(e) {
201
+	if (!this.optDown.use) return;
202
+	let me = this;
203
+
204
+	let scrollTop = me.getScrollTop(); // 当前滚动条的距离
205
+	let curPoint = me.getPoint(e); // 当前点
206
+
207
+	let moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
208
+
209
+	// 向下拉 && 在顶部
210
+	// mescroll-body,直接判定在顶部即可
211
+	// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
212
+	// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
213
+	if (moveY > 0 && (
214
+			(me.isScrollBody && scrollTop <= 0)
215
+			||
216
+			(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
217
+		)) {
218
+		// 可下拉的条件
219
+		if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
220
+				me.optUp.isBoth))) {
221
+
222
+			// 下拉的初始角度是否在配置的范围内
223
+			if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
224
+			if (me.startAngle < me.optDown.minAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
225
+
226
+			// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
227
+			if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
228
+				me.inTouchend = true; // 标记执行touchend
229
+				me.touchendEvent(); // 提前触发touchend
230
+				return;
231
+			}
232
+			
233
+			me.preventDefault(e); // 阻止默认事件
234
+
235
+			let diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
236
+
237
+			// 下拉距离  < 指定距离
238
+			if (me.downHight < me.optDown.offset) {
239
+				if (me.movetype !== 1) {
240
+					me.movetype = 1; // 加入标记,保证只执行一次
241
+					me.isDownEndSuccess = null; // 重置是否加载成功的状态 (wxs执行的是wxs.wxs)
242
+					me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
243
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
244
+				}
245
+				me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
246
+
247
+				// 指定距离  <= 下拉距离
248
+			} else {
249
+				if (me.movetype !== 2) {
250
+					me.movetype = 2; // 加入标记,保证只执行一次
251
+					me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
252
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
253
+				}
254
+				if (diff > 0) { // 向下拉
255
+					me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
256
+				} else { // 向上收
257
+					me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
258
+				}
259
+			}
260
+			
261
+			me.downHight = Math.round(me.downHight) // 取整
262
+			let rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
263
+			me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
264
+		}
265
+	}
266
+
267
+	me.lastPoint = curPoint; // 记录本次移动的点
268
+}
269
+
270
+/* 列表touchend事件 */
271
+MeScroll.prototype.touchendEvent = function(e) {
272
+	if (!this.optDown.use) return;
273
+	// 如果下拉区域高度已改变,则需重置回来
274
+	if (this.isMoveDown) {
275
+		if (this.downHight >= this.optDown.offset) {
276
+			// 符合触发刷新的条件
277
+			this.triggerDownScroll();
278
+		} else {
279
+			// 不符合的话 则重置
280
+			this.downHight = 0;
281
+			this.endDownScrollCall(this);
282
+		}
283
+		this.movetype = 0;
284
+		this.isMoveDown = false;
285
+	} else if (!this.isScrollBody && this.getScrollTop() === this.startTop) { // scroll-view到顶/左/右/底的滑动事件
286
+		let isScrollUp = this.getPoint(e).y - this.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
287
+		// 上滑
288
+		if (isScrollUp) {
289
+			// 需检查滑动的角度
290
+			let angle = this.getAngle(this.getPoint(e), this.startPoint); // 两点之间的角度,区间 [0,90]
291
+			if (angle > 80) {
292
+				// 检查并触发上拉
293
+				this.triggerUpScroll(true);
294
+			}
295
+		}
296
+	}
297
+}
298
+
299
+/* 根据点击滑动事件获取第一个手指的坐标 */
300
+MeScroll.prototype.getPoint = function(e) {
301
+	if (!e) {
302
+		return {
303
+			x: 0,
304
+			y: 0
305
+		}
306
+	}
307
+	if (e.touches && e.touches[0]) {
308
+		return {
309
+			x: e.touches[0].pageX,
310
+			y: e.touches[0].pageY
311
+		}
312
+	} else if (e.changedTouches && e.changedTouches[0]) {
313
+		return {
314
+			x: e.changedTouches[0].pageX,
315
+			y: e.changedTouches[0].pageY
316
+		}
317
+	} else {
318
+		return {
319
+			x: e.clientX,
320
+			y: e.clientY
321
+		}
322
+	}
323
+}
324
+
325
+/* 计算两点之间的角度: 区间 [0,90]*/
326
+MeScroll.prototype.getAngle = function(p1, p2) {
327
+	let x = Math.abs(p1.x - p2.x);
328
+	let y = Math.abs(p1.y - p2.y);
329
+	let z = Math.sqrt(x * x + y * y);
330
+	let angle = 0;
331
+	if (z !== 0) {
332
+		angle = Math.asin(y / z) / Math.PI * 180;
333
+	}
334
+	return angle
335
+}
336
+
337
+/* 触发下拉刷新 */
338
+MeScroll.prototype.triggerDownScroll = function() {
339
+	if (this.optDown.beforeLoading && this.optDown.beforeLoading(this)) {
340
+		//return true则处于完全自定义状态
341
+	} else {
342
+		this.showDownScroll(); // 下拉刷新中...
343
+		!this.optDown.native && this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
344
+	}
345
+}
346
+
347
+/* 显示下拉进度布局 */
348
+MeScroll.prototype.showDownScroll = function() {
349
+	this.isDownScrolling = true; // 标记下拉中
350
+	if (this.optDown.native) {
351
+		uni.startPullDownRefresh(); // 系统自带的下拉刷新
352
+		this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
353
+	} else{
354
+		this.downHight = this.optDown.offset; // 更新下拉区域高度
355
+		this.showDownLoadingCall(this.downHight); // 下拉刷新中...
356
+	}
357
+}
358
+
359
+MeScroll.prototype.showDownLoadingCall = function(downHight) {
360
+	this.optDown.showLoading && this.optDown.showLoading(this, downHight); // 下拉刷新中...
361
+	this.optDown.afterLoading && this.optDown.afterLoading(this, downHight); // 下拉刷新中...触发之后马上要执行的代码
362
+}
363
+
364
+/* 显示系统自带的下拉刷新时需要处理的业务 */
365
+MeScroll.prototype.onPullDownRefresh = function() {
366
+	this.isDownScrolling = true; // 标记下拉中
367
+	this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
368
+	this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
369
+}
370
+
371
+/* 结束下拉刷新 */
372
+MeScroll.prototype.endDownScroll = function() {
373
+	if (this.optDown.native) { // 结束原生下拉刷新
374
+		this.isDownScrolling = false;
375
+		this.endDownScrollCall(this);
376
+		uni.stopPullDownRefresh();
377
+		return
378
+	}
379
+	let me = this;
380
+	// 结束下拉刷新的方法
381
+	let endScroll = function() {
382
+		me.downHight = 0;
383
+		me.isDownScrolling = false;
384
+		me.endDownScrollCall(me);
385
+		if(!me.isScrollBody){
386
+			me.setScrollHeight(0) // scroll-view重置滚动区域,使数据不满屏时仍可检查触发翻页
387
+			me.scrollTo(0,0) // scroll-view需重置滚动条到顶部,避免startTop大于0时,对下拉刷新的影响
388
+		}
389
+	}
390
+	// 结束下拉刷新时的回调
391
+	let delay = 0;
392
+	if (me.optDown.beforeEndDownScroll) {
393
+		delay = me.optDown.beforeEndDownScroll(me); // 结束下拉刷新的延时,单位ms
394
+		if(me.isDownEndSuccess == null) delay = 0; // 没有执行加载中,则不延时
395
+	}
396
+	if (typeof delay === 'number' && delay > 0) {
397
+		setTimeout(endScroll, delay);
398
+	} else {
399
+		endScroll();
400
+	}
401
+}
402
+
403
+MeScroll.prototype.endDownScrollCall = function() {
404
+	this.optDown.endDownScroll && this.optDown.endDownScroll(this);
405
+	this.optDown.afterEndDownScroll && this.optDown.afterEndDownScroll(this);
406
+}
407
+
408
+/* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */
409
+MeScroll.prototype.lockDownScroll = function(isLock) {
410
+	if (isLock == null) isLock = true;
411
+	this.optDown.isLock = isLock;
412
+}
413
+
414
+/* 锁定上拉加载:isLock=ture,null锁定;isLock=false解锁 */
415
+MeScroll.prototype.lockUpScroll = function(isLock) {
416
+	if (isLock == null) isLock = true;
417
+	this.optUp.isLock = isLock;
418
+}
419
+
420
+/* -------初始化上拉加载------- */
421
+MeScroll.prototype.initUpScroll = function() {
422
+	let me = this;
423
+	// 配置参数
424
+	me.optUp = me.options.up || {use: false}
425
+	if(!me.optUp.textColor && me.hasColor(me.optUp.bgColor)) me.optUp.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
426
+	me.extendUpScroll(me.optUp);
427
+
428
+	if (me.optUp.use === false) return; // 配置不使用上拉加载时,则不初始化上拉布局
429
+	me.optUp.hasNext = true; // 如果使用上拉,则默认有下一页
430
+	me.startNum = me.optUp.page.num + 1; // 记录page开始的页码
431
+
432
+	// 初始化完毕的回调
433
+	if (me.optUp.inited) {
434
+		setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
435
+			me.optUp.inited(me);
436
+		}, 0)
437
+	}
438
+}
439
+
440
+/*滚动到底部的事件 (仅mescroll-body生效)*/
441
+MeScroll.prototype.onReachBottom = function() {
442
+	if (this.isScrollBody && !this.isUpScrolling) { // 只能支持下拉刷新的时候同时可以触发上拉加载,否则滚动到底部就需要上滑一点才能触发onReachBottom
443
+		if (!this.optUp.isLock && this.optUp.hasNext) {
444
+			this.triggerUpScroll();
445
+		}
446
+	}
447
+}
448
+
449
+/*列表滚动事件 (仅mescroll-body生效)*/
450
+MeScroll.prototype.onPageScroll = function(e) {
451
+	if (!this.isScrollBody) return;
452
+	
453
+	// 更新滚动条的位置 (主要用于判断下拉刷新时,滚动条是否在顶部)
454
+	this.setScrollTop(e.scrollTop);
455
+
456
+	// 顶部按钮的显示隐藏
457
+	if (e.scrollTop >= this.optUp.toTop.offset) {
458
+		this.showTopBtn();
459
+	} else {
460
+		this.hideTopBtn();
461
+	}
462
+}
463
+
464
+/*列表滚动事件*/
465
+MeScroll.prototype.scroll = function(e, onScroll) {
466
+	// 更新滚动条的位置
467
+	this.setScrollTop(e.scrollTop);
468
+	// 更新滚动内容高度
469
+	this.setScrollHeight(e.scrollHeight);
470
+
471
+	// 向上滑还是向下滑动
472
+	if (this.preScrollY == null) this.preScrollY = 0;
473
+	this.isScrollUp = e.scrollTop - this.preScrollY > 0;
474
+	this.preScrollY = e.scrollTop;
475
+
476
+	// 上滑 && 检查并触发上拉
477
+	this.isScrollUp && this.triggerUpScroll(true);
478
+
479
+	// 顶部按钮的显示隐藏
480
+	if (e.scrollTop >= this.optUp.toTop.offset) {
481
+		this.showTopBtn();
482
+	} else {
483
+		this.hideTopBtn();
484
+	}
485
+
486
+	// 滑动监听
487
+	this.optUp.onScroll && onScroll && onScroll()
488
+}
489
+
490
+/* 触发上拉加载 */
491
+MeScroll.prototype.triggerUpScroll = function(isCheck) {
492
+	if (!this.isUpScrolling && this.optUp.use && this.optUp.callback) {
493
+		// 是否校验在底部; 默认不校验
494
+		if (isCheck === true) {
495
+			let canUp = false;
496
+			// 还有下一页 && 没有锁定 && 不在下拉中
497
+			if (this.optUp.hasNext && !this.optUp.isLock && !this.isDownScrolling) {
498
+				if (this.getScrollBottom() <= this.optUp.offset) { // 到底部
499
+					canUp = true; // 标记可上拉
500
+				}
501
+			}
502
+			if (canUp === false) return;
503
+		}
504
+		this.showUpScroll(); // 上拉加载中...
505
+		this.optUp.page.num++; // 预先加一页,如果失败则减回
506
+		this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
507
+		this.num = this.optUp.page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
508
+		this.size = this.optUp.page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
509
+		this.time = this.optUp.page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
510
+		this.optUp.callback(this); // 执行回调,联网加载数据
511
+	}
512
+}
513
+
514
+/* 显示上拉加载中 */
515
+MeScroll.prototype.showUpScroll = function() {
516
+	this.isUpScrolling = true; // 标记上拉加载中
517
+	this.optUp.showLoading && this.optUp.showLoading(this); // 回调
518
+}
519
+
520
+/* 显示上拉无更多数据 */
521
+MeScroll.prototype.showNoMore = function() {
522
+	this.optUp.hasNext = false; // 标记无更多数据
523
+	this.optUp.showNoMore && this.optUp.showNoMore(this); // 回调
524
+}
525
+
526
+/* 隐藏上拉区域**/
527
+MeScroll.prototype.hideUpScroll = function() {
528
+	this.optUp.hideUpScroll && this.optUp.hideUpScroll(this); // 回调
529
+}
530
+
531
+/* 结束上拉加载 */
532
+MeScroll.prototype.endUpScroll = function(isShowNoMore) {
533
+	if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
534
+		if (isShowNoMore) {
535
+			this.showNoMore(); // isShowNoMore=true,显示无更多数据
536
+		} else {
537
+			this.hideUpScroll(); // isShowNoMore=false,隐藏上拉加载
538
+		}
539
+	}
540
+	this.isUpScrolling = false; // 标记结束上拉加载
541
+}
542
+
543
+/* 重置上拉加载列表为第一页
544
+ *isShowLoading 是否显示进度布局;
545
+ * 1.默认null,不传参,则显示上拉加载的进度布局
546
+ * 2.传参true, 则显示下拉刷新的进度布局
547
+ * 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据)
548
+ */
549
+MeScroll.prototype.resetUpScroll = function(isShowLoading) {
550
+	if (this.optUp && this.optUp.use) {
551
+		let page = this.optUp.page;
552
+		this.prePageNum = page.num; // 缓存重置前的页码,加载失败可退回
553
+		this.prePageTime = page.time; // 缓存重置前的时间,加载失败可退回
554
+		page.num = this.startNum; // 重置为第一页
555
+		page.time = null; // 重置时间为空
556
+		if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
557
+			if (isShowLoading == null) {
558
+				this.removeEmpty(); // 移除空布局
559
+				this.showUpScroll(); // 不传参,默认显示上拉加载的进度布局
560
+			} else {
561
+				this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
562
+			}
563
+		}
564
+		this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
565
+		this.num = page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
566
+		this.size = page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
567
+		this.time = page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
568
+		this.optUp.callback && this.optUp.callback(this); // 执行上拉回调
569
+	}
570
+}
571
+
572
+/* 设置page.num的值 */
573
+MeScroll.prototype.setPageNum = function(num) {
574
+	this.optUp.page.num = num - 1;
575
+}
576
+
577
+/* 设置page.size的值 */
578
+MeScroll.prototype.setPageSize = function(size) {
579
+	this.optUp.page.size = size;
580
+}
581
+
582
+/* 联网回调成功,结束下拉刷新和上拉加载
583
+ * dataSize: 当前页的数据量(必传)
584
+ * totalPage: 总页数(必传)
585
+ * systime: 服务器时间 (可空)
586
+ */
587
+MeScroll.prototype.endByPage = function(dataSize, totalPage, systime) {
588
+	let hasNext;
589
+	if (this.optUp.use && totalPage != null) hasNext = this.optUp.page.num < totalPage; // 是否还有下一页
590
+	this.endSuccess(dataSize, hasNext, systime);
591
+}
592
+
593
+/* 联网回调成功,结束下拉刷新和上拉加载
594
+ * dataSize: 当前页的数据量(必传)
595
+ * totalSize: 列表所有数据总数量(必传)
596
+ * systime: 服务器时间 (可空)
597
+ */
598
+MeScroll.prototype.endBySize = function(dataSize, totalSize, systime) {
599
+	let hasNext;
600
+	if (this.optUp.use && totalSize != null) {
601
+		let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
602
+		hasNext = loadSize < totalSize; // 是否还有下一页
603
+	}
604
+	this.endSuccess(dataSize, hasNext, systime);
605
+}
606
+
607
+/* 联网回调成功,结束下拉刷新和上拉加载
608
+ * dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页
609
+ * hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
610
+ * systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录
611
+ */
612
+MeScroll.prototype.endSuccess = function(dataSize, hasNext, systime) {
613
+	let me = this;
614
+	// 结束下拉刷新
615
+	if (me.isDownScrolling) {
616
+		me.isDownEndSuccess = true
617
+		me.endDownScroll();
618
+	}
619
+
620
+	// 结束上拉加载
621
+	if (me.optUp.use) {
622
+		let isShowNoMore; // 是否已无更多数据
623
+		if (dataSize != null) {
624
+			let pageNum = me.optUp.page.num; // 当前页码
625
+			let pageSize = me.optUp.page.size; // 每页长度
626
+			// 如果是第一页
627
+			if (pageNum === 1) {
628
+				if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
629
+			}
630
+			if (dataSize < pageSize || hasNext === false) {
631
+				// 返回的数据不满一页时,则说明已无更多数据
632
+				me.optUp.hasNext = false;
633
+				if (dataSize === 0 && pageNum === 1) {
634
+					// 如果第一页无任何数据且配置了空布局
635
+					isShowNoMore = false;
636
+					me.showEmpty();
637
+				} else {
638
+					// 总列表数少于配置的数量,则不显示无更多数据
639
+					let allDataSize = (pageNum - 1) * pageSize + dataSize;
640
+					if (allDataSize < me.optUp.noMoreSize) {
641
+						isShowNoMore = false;
642
+					} else {
643
+						isShowNoMore = true;
644
+					}
645
+					me.removeEmpty(); // 移除空布局
646
+				}
647
+			} else {
648
+				// 还有下一页
649
+				isShowNoMore = false;
650
+				me.optUp.hasNext = true;
651
+				me.removeEmpty(); // 移除空布局
652
+			}
653
+		}
654
+
655
+		// 隐藏上拉
656
+		me.endUpScroll(isShowNoMore);
657
+	}
658
+}
659
+
660
+/* 回调失败,结束下拉刷新和上拉加载 */
661
+MeScroll.prototype.endErr = function(errDistance) {
662
+	// 结束下拉,回调失败重置回原来的页码和时间
663
+	if (this.isDownScrolling) {
664
+		this.isDownEndSuccess = false
665
+		let page = this.optUp.page;
666
+		if (page && this.prePageNum) {
667
+			page.num = this.prePageNum;
668
+			page.time = this.prePageTime;
669
+		}
670
+		this.endDownScroll();
671
+	}
672
+	// 结束上拉,回调失败重置回原来的页码
673
+	if (this.isUpScrolling) {
674
+		this.optUp.page.num--;
675
+		this.endUpScroll(false);
676
+		// 如果是mescroll-body,则需往回滚一定距离
677
+		if(this.isScrollBody && errDistance !== 0){ // 不处理0
678
+			if(!errDistance) errDistance = this.optUp.errDistance; // 不传,则取默认
679
+			this.scrollTo(this.getScrollTop() - errDistance, 0) // 往上回滚的距离
680
+		}
681
+	}
682
+}
683
+
684
+/* 显示空布局 */
685
+MeScroll.prototype.showEmpty = function() {
686
+	this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(true)
687
+}
688
+
689
+/* 移除空布局 */
690
+MeScroll.prototype.removeEmpty = function() {
691
+	this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(false)
692
+}
693
+
694
+/* 显示回到顶部的按钮 */
695
+MeScroll.prototype.showTopBtn = function() {
696
+	if (!this.topBtnShow) {
697
+		this.topBtnShow = true;
698
+		this.optUp.toTop.onShow && this.optUp.toTop.onShow(true);
699
+	}
700
+}
701
+
702
+/* 隐藏回到顶部的按钮 */
703
+MeScroll.prototype.hideTopBtn = function() {
704
+	if (this.topBtnShow) {
705
+		this.topBtnShow = false;
706
+		this.optUp.toTop.onShow && this.optUp.toTop.onShow(false);
707
+	}
708
+}
709
+
710
+/* 获取滚动条的位置 */
711
+MeScroll.prototype.getScrollTop = function() {
712
+	return this.scrollTop || 0
713
+}
714
+
715
+/* 记录滚动条的位置 */
716
+MeScroll.prototype.setScrollTop = function(y) {
717
+	this.scrollTop = y;
718
+}
719
+
720
+/* 滚动到指定位置 */
721
+MeScroll.prototype.scrollTo = function(y, t) {
722
+	this.myScrollTo && this.myScrollTo(y, t) // scrollview需自定义回到顶部方法
723
+}
724
+
725
+/* 自定义scrollTo */
726
+MeScroll.prototype.resetScrollTo = function(myScrollTo) {
727
+	this.myScrollTo = myScrollTo
728
+}
729
+
730
+/* 滚动条到底部的距离 */
731
+MeScroll.prototype.getScrollBottom = function() {
732
+	return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop()
733
+}
734
+
735
+/* 计步器
736
+ star: 开始值
737
+ end: 结束值
738
+ callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器;
739
+ t: 计步时长,传0则直接回调end值;不传则默认300ms
740
+ rate: 周期;不传则默认30ms计步一次
741
+ * */
742
+MeScroll.prototype.getStep = function(star, end, callback, t, rate) {
743
+	let diff = end - star; // 差值
744
+	if (t === 0 || diff === 0) {
745
+		callback && callback(end);
746
+		return;
747
+	}
748
+	t = t || 300; // 时长 300ms
749
+	rate = rate || 30; // 周期 30ms
750
+	let count = t / rate; // 次数
751
+	let step = diff / count; // 步长
752
+	let i = 0; // 计数
753
+	let timer = setInterval(function() {
754
+		if (i < count - 1) {
755
+			star += step;
756
+			callback && callback(star, timer);
757
+			i++;
758
+		} else {
759
+			callback && callback(end, timer); // 最后一次直接设置end,避免计算误差
760
+			clearInterval(timer);
761
+		}
762
+	}, rate);
763
+}
764
+
765
+/* 滚动容器的高度 */
766
+MeScroll.prototype.getClientHeight = function(isReal) {
767
+	let h = this.clientHeight || 0
768
+	if (h === 0 && isReal !== true) { // 未获取到容器的高度,可临时取body的高度 (可能会有误差)
769
+		h = this.getBodyHeight()
770
+	}
771
+	return h
772
+}
773
+MeScroll.prototype.setClientHeight = function(h) {
774
+	this.clientHeight = h;
775
+}
776
+
777
+/* 滚动内容的高度 */
778
+MeScroll.prototype.getScrollHeight = function() {
779
+	return this.scrollHeight || 0;
780
+}
781
+MeScroll.prototype.setScrollHeight = function(h) {
782
+	this.scrollHeight = h;
783
+}
784
+
785
+/* body的高度 */
786
+MeScroll.prototype.getBodyHeight = function() {
787
+	return this.bodyHeight || 0;
788
+}
789
+MeScroll.prototype.setBodyHeight = function(h) {
790
+	this.bodyHeight = h;
791
+}
792
+
793
+/* 阻止浏览器默认滚动事件 */
794
+MeScroll.prototype.preventDefault = function(e) {
795
+	// 小程序不支持e.preventDefault, 已在wxs中禁止
796
+	// app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止, 或使用renderjs禁止
797
+	// cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
798
+	if (e && e.cancelable && !e.defaultPrevented) e.preventDefault()
799
+}

+ 480 - 0
src/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni.vue

@@ -0,0 +1,480 @@
1
+<template>
2
+	<view class="mescroll-uni-warp">
3
+		<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">
4
+			<view class="mescroll-uni-content mescroll-render-touch"
5
+			@touchstart="wxsBiz.touchstartEvent" 
6
+			@touchmove="wxsBiz.touchmoveEvent" 
7
+			@touchend="wxsBiz.touchendEvent" 
8
+			@touchcancel="wxsBiz.touchendEvent"
9
+			:change:prop="wxsBiz.propObserver"
10
+			:prop="wxsProp">
11
+				<!-- 状态栏 -->
12
+				<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
13
+		
14
+				<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp">
15
+					<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
16
+					<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
17
+					<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
18
+						<view class="downwarp-content">
19
+							<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
20
+							<view class="downwarp-tip">{{downText}}</view>
21
+						</view>
22
+					</view>
23
+
24
+					<!-- 列表内容 -->
25
+					<slot></slot>
26
+
27
+					<!-- 空布局 -->
28
+					<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
29
+
30
+					<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
31
+					<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
32
+					<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
33
+						<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
34
+						<view v-show="upLoadType===1">
35
+							<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
36
+							<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
37
+						</view>
38
+						<!-- 无数据 -->
39
+						<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
40
+					</view>
41
+				</view>
42
+			
43
+				<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
44
+				<!-- #ifdef H5 -->
45
+				<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
46
+				<!-- #endif -->
47
+				
48
+				<!-- 适配iPhoneX -->
49
+				<view v-if="safearea" class="mescroll-safearea"></view>
50
+			</view>
51
+		</scroll-view>
52
+
53
+		<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
54
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
55
+		
56
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
57
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
58
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
59
+		<!-- #endif -->
60
+	</view>
61
+</template>
62
+
63
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
64
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
65
+<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
66
+<!-- #endif -->
67
+
68
+<!-- app, h5使用renderjs -->
69
+<!-- #ifdef APP-PLUS || H5 -->
70
+<script module="renderBiz" lang="renderjs">
71
+	import renderBiz from './wxs/renderjs.js';
72
+	export default {
73
+		mixins:[renderBiz]
74
+	}
75
+</script>
76
+<!-- #endif -->
77
+
78
+<script>
79
+	// 引入mescroll-uni.js,处理核心逻辑
80
+	import MeScroll from './mescroll-uni.js';
81
+	// 引入全局配置
82
+	import GlobalOption from './mescroll-uni-option.js';
83
+	// 引入国际化工具类
84
+	import mescrollI18n from './mescroll-i18n.js';
85
+	// 引入回到顶部组件
86
+	import MescrollTop from './components/mescroll-top.vue';
87
+	// 引入兼容wxs(含renderjs)写法的mixins
88
+	import WxsMixin from './wxs/mixins.js';
89
+	
90
+	/**
91
+	 * mescroll-uni 嵌在页面某个区域的下拉刷新和上拉加载组件, 如嵌在弹窗,浮层,swiper中...
92
+	 * @property {Object} down 下拉刷新的参数配置
93
+	 * @property {Object} up 上拉加载的参数配置
94
+	 * @property {Object} i18n 国际化的参数配置
95
+	 * @property {String, Number} top 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
96
+	 * @property {Boolean, String} topbar 偏移量top是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
97
+	 * @property {String, Number} bottom 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
98
+	 * @property {Boolean} safearea 偏移量bottom是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
99
+	 * @property {Boolean} fixed 是否通过fixed固定mescroll的高度, 默认true
100
+	 * @property {String, Number} height 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
101
+	 * @property {Boolean} bottombar 底部是否偏移TabBar的高度 (仅在H5端的tab页生效)
102
+	 * @property {Boolean} disableScroll 是否禁止滚动, 默认false
103
+	 * @event {Function} init 初始化完成的回调 
104
+	 * @event {Function} down 下拉刷新的回调
105
+	 * @event {Function} up 上拉加载的回调 
106
+	 * @event {Function} emptyclick 点击empty配置的btnText按钮回调
107
+	 * @event {Function} topclick 点击回到顶部的按钮回调
108
+	 * @event {Function} scroll 滚动监听 (需在 up 配置 onScroll:true 才生效)
109
+	 * @example <mescroll-uni @init="mescrollInit" @down="downCallback" @up="upCallback"> ... </mescroll-uni>
110
+	 */
111
+	export default {
112
+		name: 'mescroll-uni',
113
+		mixins: [WxsMixin],
114
+		components: {
115
+			MescrollTop
116
+		},
117
+		props: {
118
+			down: Object,
119
+			up: Object,
120
+			i18n: Object,
121
+			top: [String, Number],
122
+			topbar: [Boolean, String],
123
+			bottom: [String, Number],
124
+			safearea: Boolean,
125
+			fixed: {
126
+				type: Boolean,
127
+				default: true
128
+			},
129
+			height: [String, Number],
130
+			bottombar:{
131
+				type: Boolean,
132
+				default: true
133
+			},
134
+			disableScroll: Boolean
135
+		},
136
+		data() {
137
+			return {
138
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
139
+				viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
140
+				downHight: 0, //下拉刷新: 容器高度
141
+				downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
142
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
143
+				upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
144
+				isShowEmpty: false, // 是否显示空布局
145
+				isShowToTop: false, // 是否显示回到顶部按钮
146
+				scrollTop: 0, // 滚动条的位置
147
+				scrollAnim: false, // 是否开启滚动动画
148
+				windowTop: 0, // 可使用窗口的顶部位置
149
+				windowBottom: 0, // 可使用窗口的底部位置
150
+				windowHeight: 0, // 可使用窗口的高度
151
+				statusBarHeight: 0 // 状态栏高度
152
+			}
153
+		},
154
+		watch: {
155
+			height() {
156
+				// 设置容器的高度
157
+				this.setClientHeight()
158
+			}
159
+		},
160
+		computed: {
161
+			// 是否使用fixed定位 (当height有值,则不使用)
162
+			isFixed(){
163
+				return !this.height && this.fixed
164
+			},
165
+			// mescroll的高度
166
+			scrollHeight(){
167
+				if (this.isFixed) {
168
+					return "auto"
169
+				} else if(this.height){
170
+					return this.toPx(this.height) + 'px'
171
+				}else{
172
+					return "100%"
173
+				}
174
+			},
175
+			// 下拉布局往下偏移的距离 (px)
176
+			numTop() {
177
+				return this.toPx(this.top)
178
+			},
179
+			fixedTop() {
180
+				return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
181
+			},
182
+			padTop() {
183
+				return !this.isFixed ? this.numTop + 'px' : 0
184
+			},
185
+			// 上拉布局往上偏移 (px)
186
+			numBottom() {
187
+				return this.toPx(this.bottom)
188
+			},
189
+			fixedBottom() {
190
+				return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
191
+			},
192
+			padBottom() {
193
+				return !this.isFixed ? this.numBottom + 'px' : 0
194
+			},
195
+			// 是否为重置下拉的状态
196
+			isDownReset(){
197
+				return this.downLoadType===3 || this.downLoadType===4
198
+			},
199
+			// 过渡
200
+			transition() {
201
+				return this.isDownReset ? 'transform 300ms' : '';
202
+			},
203
+			translateY() {
204
+				return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
205
+			},
206
+			// 列表是否可滑动
207
+			scrollable(){
208
+				if(this.disableScroll) return false
209
+				return this.downLoadType===0 || this.isDownReset
210
+			},
211
+			// 是否在加载中
212
+			isDownLoading(){
213
+				return this.downLoadType === 3
214
+			},
215
+			// 旋转的角度
216
+			downRotate(){
217
+				return 'rotate(' + 360 * this.downRate + 'deg)'
218
+			},
219
+			// 文本提示
220
+			downText(){
221
+				if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
222
+				switch (this.downLoadType){
223
+					case 1: return this.mescroll.optDown.textInOffset;
224
+					case 2: return this.mescroll.optDown.textOutOffset;
225
+					case 3: return this.mescroll.optDown.textLoading;
226
+					case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
227
+					default: return this.mescroll.optDown.textInOffset;
228
+				}
229
+			}
230
+		},
231
+		methods: {
232
+			//number,rpx,upx,px,% --> px的数值
233
+			toPx(num){
234
+				if(typeof num === "string"){
235
+					if (num.indexOf('px') !== -1) {
236
+						if(num.indexOf('rpx') !== -1) { // "10rpx"
237
+							num = num.replace('rpx', '');
238
+						} else if(num.indexOf('upx') !== -1) { // "10upx"
239
+							num = num.replace('upx', '');
240
+						} else { // "10px"
241
+							return Number(num.replace('px', ''))
242
+						}
243
+					}else if (num.indexOf('%') !== -1){
244
+						// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
245
+						let rate = Number(num.replace("%","")) / 100
246
+						return this.windowHeight * rate
247
+					}
248
+				}
249
+				return num ? uni.upx2px(Number(num)) : 0
250
+			},
251
+			//注册列表滚动事件,用于下拉刷新和上拉加载
252
+			scroll(e) {
253
+				this.mescroll.scroll(e.detail, () => {
254
+					this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
255
+				})
256
+			},
257
+			// 点击空布局的按钮回调
258
+			emptyClick() {
259
+				this.$emit('emptyclick', this.mescroll)
260
+			},
261
+			// 点击回到顶部的按钮回调
262
+			toTopClick() {
263
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
264
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
265
+			},
266
+			// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
267
+			setClientHeight() {
268
+				if (!this.isExec) {
269
+					this.isExec = true; // 避免多次获取
270
+					this.$nextTick(() => { // 确保dom已渲染
271
+						this.getClientInfo(data=>{
272
+							this.isExec = false;
273
+							if (data) {
274
+								this.mescroll.setClientHeight(data.height);
275
+							} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
276
+								this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
277
+								setTimeout(() => {
278
+									this.setClientHeight()
279
+								}, this.clientNum * 100)
280
+							}
281
+						})
282
+					})
283
+				}
284
+			},
285
+			// 获取滚动区域的信息
286
+			getClientInfo(success){
287
+				let query = uni.createSelectorQuery().in(this);
288
+				let view = query.select('#' + this.viewId);
289
+				view.boundingClientRect(data => {
290
+					success(data)
291
+				}).exec();
292
+			}
293
+		},
294
+		// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
295
+		created() {
296
+			let vm = this;
297
+
298
+			let diyOption = {
299
+				// 下拉刷新的配置
300
+				down: {
301
+					inOffset() {
302
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
303
+					},
304
+					outOffset() {
305
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
306
+					},
307
+					onMoving(mescroll, rate, downHight) {
308
+						// 下拉过程中的回调,滑动过程一直在执行;
309
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
310
+						vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
311
+					},
312
+					showLoading(mescroll, downHight) {
313
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
314
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
315
+					},
316
+					beforeEndDownScroll(mescroll){
317
+						vm.downLoadType = 4; 
318
+						return mescroll.optDown.beforeEndDelay // 延时结束的时长
319
+					},
320
+					endDownScroll() {
321
+						vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
322
+						vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
323
+						vm.downResetTimer && clearTimeout(vm.downResetTimer)
324
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
325
+							if(vm.downLoadType===4) vm.downLoadType = 0
326
+						},300)
327
+					},
328
+					// 派发下拉刷新的回调
329
+					callback: function(mescroll) {
330
+						vm.$emit('down', mescroll)
331
+					}
332
+				},
333
+				// 上拉加载的配置
334
+				up: {
335
+					// 显示加载中的回调
336
+					showLoading() {
337
+						vm.upLoadType = 1;
338
+					},
339
+					// 显示无更多数据的回调
340
+					showNoMore() {
341
+						vm.upLoadType = 2;
342
+					},
343
+					// 隐藏上拉加载的回调
344
+					hideUpScroll(mescroll) {
345
+						vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
346
+					},
347
+					// 空布局
348
+					empty: {
349
+						onShow(isShow) { // 显示隐藏的回调
350
+							vm.isShowEmpty = isShow;
351
+						}
352
+					},
353
+					// 回到顶部
354
+					toTop: {
355
+						onShow(isShow) { // 显示隐藏的回调
356
+							vm.isShowToTop = isShow;
357
+						}
358
+					},
359
+					// 派发上拉加载的回调
360
+					callback: function(mescroll) {
361
+						vm.$emit('up', mescroll);
362
+						// 更新容器的高度 (多mescroll的情况)
363
+						vm.setClientHeight()
364
+					}
365
+				}
366
+			}
367
+
368
+			let i18nType = mescrollI18n.getType() // 当前语言类型
369
+			let i18nOption = {type: i18nType} // 国际化配置
370
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
371
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
372
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
373
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
374
+			let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
375
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
376
+
377
+			// 初始化MeScroll对象
378
+			vm.mescroll = new MeScroll(myOption);
379
+			vm.mescroll.viewId = vm.viewId; // 附带id
380
+			vm.mescroll.i18n = i18nOption; // 挂载语言包
381
+			// init回调mescroll对象
382
+			vm.$emit('init', vm.mescroll);
383
+			
384
+			// 设置高度
385
+			const sys = uni.getSystemInfoSync();
386
+			if(sys.windowTop) vm.windowTop = sys.windowTop;
387
+			if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
388
+			if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
389
+			if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
390
+			// 使down的bottomOffset生效
391
+			vm.mescroll.setBodyHeight(sys.windowHeight);
392
+
393
+			// 因为使用的是scrollview,这里需自定义scrollTo
394
+			vm.mescroll.resetScrollTo((y, t) => {
395
+				vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
396
+				if(typeof y === 'string'){
397
+					// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现
398
+					vm.getClientInfo(function(rect){
399
+						let mescrollTop = rect.top // mescroll到顶部的距离
400
+						let selector;
401
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
402
+							selector = '#'+y // 不带#和. 则默认为id选择器
403
+						}else{
404
+							selector = y
405
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
406
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
407
+								selector = y.split('>>>')[1].trim()
408
+							}
409
+							// #endif
410
+						}
411
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
412
+							if (rect) {
413
+								let curY = vm.mescroll.getScrollTop()
414
+								let top = rect.top - mescrollTop
415
+								top += curY
416
+								if(!vm.isFixed) top -= vm.numTop
417
+								vm.scrollTop = curY;
418
+								vm.$nextTick(function() {
419
+									vm.scrollTop = top
420
+								})
421
+							} else{
422
+								console.error(selector + ' does not exist');
423
+							}
424
+						}).exec()
425
+					})
426
+					return;
427
+				}
428
+				let curY = vm.mescroll.getScrollTop()
429
+				if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
430
+					vm.scrollTop = curY;
431
+					vm.$nextTick(function() {
432
+						vm.scrollTop = y
433
+					})
434
+				} else {
435
+					vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
436
+						vm.scrollTop = step
437
+					}, t)
438
+				}
439
+			})
440
+			
441
+			// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
442
+			if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
443
+				vm.mescroll.optUp.toTop.safearea = vm.safearea;
444
+			}
445
+			
446
+			// 全局配置监听
447
+			uni.$on("setMescrollGlobalOption", options=>{
448
+				if(!options) return;
449
+				let i18nType = options.i18n ? options.i18n.type : null
450
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
451
+					vm.mescroll.i18n.type = i18nType
452
+					mescrollI18n.setType(i18nType)
453
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
454
+				}
455
+				if(options.down){
456
+					let down = MeScroll.extend({}, options.down)
457
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
458
+				}
459
+				if(options.up){
460
+					let up = MeScroll.extend({}, options.up)
461
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
462
+				}
463
+			})
464
+		},
465
+		mounted() {
466
+			// 设置容器的高度
467
+			this.setClientHeight()
468
+		},
469
+		destroyed() {
470
+			// 注销全局配置监听
471
+			uni.$off("setMescrollGlobalOption")
472
+		}
473
+	}
474
+</script>
475
+
476
+<style>
477
+	@import "./mescroll-uni.css";
478
+	@import "./components/mescroll-down.css";
479
+	@import './components/mescroll-up.css';
480
+</style>

+ 47 - 0
src/uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-comp.js

@@ -0,0 +1,47 @@
1
+/**
2
+ * mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期
3
+ */
4
+const MescrollCompMixin = {
5
+	// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件 (一级)
6
+	onPageScroll(e) {
7
+		this.handlePageScroll(e)
8
+	},
9
+	onReachBottom() {
10
+		this.handleReachBottom()
11
+	},
12
+	// 当down的native: true时, 还需传递此方法进到子组件
13
+	onPullDownRefresh(){
14
+		this.handlePullDownRefresh()
15
+	},
16
+	data() {
17
+		return {
18
+			mescroll: { // mescroll-body写在子子子...组件的情况 (多级)
19
+				onPageScroll: e=>{
20
+					this.handlePageScroll(e)
21
+				},
22
+				onReachBottom: ()=>{
23
+					this.handleReachBottom()
24
+				},
25
+				onPullDownRefresh: ()=>{
26
+					this.handlePullDownRefresh()
27
+				}
28
+			}
29
+		}
30
+	},
31
+	methods:{
32
+		handlePageScroll(e){
33
+			let item = this.$refs["mescrollItem"];
34
+			if(item && item.mescroll) item.mescroll.onPageScroll(e);
35
+		},
36
+		handleReachBottom(){
37
+			let item = this.$refs["mescrollItem"];
38
+			if(item && item.mescroll) item.mescroll.onReachBottom();
39
+		},
40
+		handlePullDownRefresh(){
41
+			let item = this.$refs["mescrollItem"];
42
+			if(item && item.mescroll) item.mescroll.onPullDownRefresh();
43
+		}
44
+	}
45
+}
46
+
47
+export default MescrollCompMixin;

+ 57 - 0
src/uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-more-item.js

@@ -0,0 +1,57 @@
1
+/**
2
+ * mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
3
+ */
4
+const MescrollMoreItemMixin = {
5
+	// 支付宝小程序不支持props的mixin,需写在具体的页面中
6
+	// #ifndef MP-ALIPAY || MP-DINGTALK
7
+	props:{
8
+		i: Number, // 每个tab页的专属下标
9
+		index: { // 当前tab的下标
10
+			type: Number,
11
+			default(){
12
+				return 0
13
+			}
14
+		}
15
+	},
16
+	// #endif
17
+	data() {
18
+		return {
19
+			downOption:{
20
+				auto:false // 不自动加载
21
+			},
22
+			upOption:{
23
+				auto:false // 不自动加载
24
+			},
25
+			isInit: false // 当前tab是否已初始化
26
+		}
27
+	},
28
+	watch:{
29
+		// 监听下标的变化
30
+		index(val){
31
+			if (this.i === val && !this.isInit) this.mescrollTrigger()
32
+		}
33
+	},
34
+	methods: {
35
+		// mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
36
+		mescrollInit(mescroll) {
37
+			this.mescroll = mescroll;
38
+			// 自动加载当前tab的数据
39
+			if(this.i === this.index){
40
+				this.mescrollTrigger()
41
+			}
42
+		},
43
+		// 主动触发加载
44
+		mescrollTrigger(){
45
+			this.isInit = true; // 标记为true
46
+			if (this.mescroll) {
47
+				if (this.mescroll.optDown.use) {
48
+					this.mescroll.triggerDownScroll();
49
+				} else{
50
+					this.mescroll.triggerUpScroll();
51
+				}
52
+			}
53
+		}
54
+	}
55
+}
56
+
57
+export default MescrollMoreItemMixin;

+ 77 - 0
src/uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-more.js

@@ -0,0 +1,77 @@
1
+/**
2
+ * mescroll-body写在子组件时, 需通过mescroll的mixins补充子组件缺少的生命周期
3
+ */
4
+const MescrollMoreMixin = {
5
+	data() {
6
+		return {
7
+			tabIndex: 0, // 当前tab下标
8
+			mescroll: { // mescroll-body写在子子子...组件的情况 (多级)
9
+				onPageScroll: e=>{
10
+					this.handlePageScroll(e)
11
+				},
12
+				onReachBottom: ()=>{
13
+					this.handleReachBottom()
14
+				},
15
+				onPullDownRefresh: ()=>{
16
+					this.handlePullDownRefresh()
17
+				}
18
+			}
19
+		}
20
+	},
21
+	// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件
22
+	onPageScroll(e) {
23
+		this.handlePageScroll(e)
24
+	},
25
+	onReachBottom() {
26
+		this.handleReachBottom()
27
+	},
28
+	// 当down的native: true时, 还需传递此方法进到子组件
29
+	onPullDownRefresh(){
30
+		this.handlePullDownRefresh()
31
+	},
32
+	methods:{
33
+		handlePageScroll(e){
34
+			let mescroll = this.getMescroll(this.tabIndex);
35
+			mescroll && mescroll.onPageScroll(e);
36
+		},
37
+		handleReachBottom(){
38
+			let mescroll = this.getMescroll(this.tabIndex);
39
+			mescroll && mescroll.onReachBottom();
40
+		},
41
+		handlePullDownRefresh(){
42
+			let mescroll = this.getMescroll(this.tabIndex);
43
+			mescroll && mescroll.onPullDownRefresh();
44
+		},
45
+		// 根据下标获取对应子组件的mescroll
46
+		getMescroll(i){
47
+			if(!this.mescrollItems) this.mescrollItems = [];
48
+			if(!this.mescrollItems[i]) {
49
+				// v-for中的refs
50
+				let vForItem = this.$refs["mescrollItem"];
51
+				if(vForItem){
52
+					this.mescrollItems[i] = vForItem[i]
53
+				}else{
54
+					// 普通的refs,不可重复
55
+					this.mescrollItems[i] = this.$refs["mescrollItem"+i];
56
+				}
57
+			}
58
+			let item = this.mescrollItems[i]
59
+			return item ? item.mescroll : null
60
+		},
61
+		// 切换tab,恢复滚动条位置
62
+		tabChange(i){
63
+			let mescroll = this.getMescroll(i);
64
+			if(mescroll){
65
+				// 恢复上次滚动条的位置
66
+				let y = mescroll.getScrollTop()
67
+				mescroll.scrollTo(y, 0)
68
+				// 再次恢复上次滚动条的位置, 确保元素已渲染
69
+				setTimeout(()=>{
70
+					mescroll.scrollTo(y, 0)
71
+				},30)
72
+			}
73
+		}
74
+	}
75
+}
76
+
77
+export default MescrollMoreMixin;

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

@@ -0,0 +1,109 @@
1
+// 定义在wxs (含renderjs) 逻辑层的数据和方法, 与视图层相互通信
2
+const WxsMixin = {
3
+	data() {
4
+		return {
5
+			// 传入wxs视图层的数据 (响应式)
6
+			wxsProp: {
7
+				optDown:{}, // 下拉刷新的配置
8
+				scrollTop:0, // 滚动条的距离
9
+				bodyHeight:0, // body的高度
10
+				isDownScrolling:false, // 是否正在下拉刷新中
11
+				isUpScrolling:false, // 是否正在上拉加载中
12
+				isScrollBody:true, // 是否为mescroll-body滚动
13
+				isUpBoth:true, // 上拉加载时,是否同时可以下拉刷新
14
+				t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
15
+			},
16
+			
17
+			// 标记调用wxs视图层的方法
18
+			callProp: {
19
+				callType: '', // 方法名
20
+				t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
21
+			},
22
+			
23
+			// 不用wxs的平台使用此处的wxsBiz对象,抹平wxs的写法 (微信小程序和APP使用的wxsBiz对象是./wxs/wxs.wxs)
24
+			// #ifndef MP-WEIXIN || MP-QQ || APP-PLUS || H5
25
+			wxsBiz: {
26
+				//注册列表touchstart事件,用于下拉刷新
27
+				touchstartEvent: e=> {
28
+					this.mescroll.touchstartEvent(e);
29
+				},
30
+				//注册列表touchmove事件,用于下拉刷新
31
+				touchmoveEvent: e=> {
32
+					this.mescroll.touchmoveEvent(e);
33
+				},
34
+				//注册列表touchend事件,用于下拉刷新
35
+				touchendEvent: e=> {
36
+					this.mescroll.touchendEvent(e);
37
+				},
38
+				propObserver(){}, // 抹平wxs的写法
39
+				callObserver(){} // 抹平wxs的写法
40
+			},
41
+			// #endif
42
+			
43
+			// 不用renderjs的平台使用此处的renderBiz对象,抹平renderjs的写法 (app 和 h5 使用的renderBiz对象是./wxs/renderjs.js)
44
+			// #ifndef APP-PLUS || H5
45
+			renderBiz: {
46
+				propObserver(){} // 抹平renderjs的写法
47
+			}
48
+			// #endif
49
+		}
50
+	},
51
+	methods: {
52
+		// wxs视图层调用逻辑层的回调
53
+		wxsCall(msg){
54
+			if(msg.type === 'setWxsProp'){
55
+				// 更新wxsProp数据 (值改变才触发更新)
56
+				this.wxsProp = {
57
+					optDown: this.mescroll.optDown,
58
+					scrollTop: this.mescroll.getScrollTop(),
59
+					bodyHeight: this.mescroll.getBodyHeight(),
60
+					isDownScrolling: this.mescroll.isDownScrolling,
61
+					isUpScrolling: this.mescroll.isUpScrolling,
62
+					isUpBoth: this.mescroll.optUp.isBoth,
63
+					isScrollBody:this.mescroll.isScrollBody,
64
+					t: Date.now()
65
+				}
66
+			}else if(msg.type === 'setLoadType'){
67
+				// 设置inOffset,outOffset的状态
68
+				this.downLoadType = msg.downLoadType
69
+				// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
70
+				this.$set(this.mescroll, 'downLoadType', this.downLoadType)
71
+				// 重置是否加载成功的状态
72
+				this.$set(this.mescroll, 'isDownEndSuccess', null)
73
+			}else if(msg.type === 'triggerDownScroll'){
74
+				// 主动触发下拉刷新
75
+				this.mescroll.triggerDownScroll();
76
+			}else if(msg.type === 'endDownScroll'){
77
+				// 结束下拉刷新
78
+				this.mescroll.endDownScroll();
79
+			}else if(msg.type === 'triggerUpScroll'){
80
+				// 主动触发上拉加载
81
+				this.mescroll.triggerUpScroll(true);
82
+			}
83
+		}
84
+	},
85
+	mounted() {
86
+		// #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5
87
+		// 配置主动触发wxs显示加载进度的回调
88
+		this.mescroll.optDown.afterLoading = ()=>{
89
+			this.callProp = {callType: "showLoading", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
90
+		}
91
+		// 配置主动触发wxs隐藏加载进度的回调
92
+		this.mescroll.optDown.afterEndDownScroll = ()=>{
93
+			this.callProp = {callType: "endDownScroll", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
94
+			let delay = 300 + (this.mescroll.optDown.beforeEndDelay || 0)
95
+			setTimeout(()=>{
96
+				if(this.downLoadType === 4 || this.downLoadType === 0){
97
+					this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
98
+				}
99
+				// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
100
+				this.$set(this.mescroll, 'downLoadType', this.downLoadType)
101
+			}, delay)
102
+		}
103
+		// 初始化wxs的数据
104
+		this.wxsCall({type: 'setWxsProp'})
105
+		// #endif
106
+	}
107
+}
108
+
109
+export default WxsMixin;

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

@@ -0,0 +1,92 @@
1
+// 使用renderjs直接操作window对象,实现动态控制app和h5的bounce
2
+// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果 (下拉刷新时禁止)
3
+// https://uniapp.dcloud.io/frame?id=renderjs
4
+
5
+// 与wxs的me实例一致
6
+var me = {}
7
+
8
+// 初始化window对象的touch事件 (仅初始化一次)
9
+if(window && !window.$mescrollRenderInit){
10
+	window.$mescrollRenderInit = true
11
+	
12
+	
13
+	window.addEventListener('touchstart', function(e){
14
+		if (me.disabled()) return;
15
+		me.startPoint = me.getPoint(e); // 记录起点
16
+	}, {passive: true})
17
+	
18
+	
19
+	window.addEventListener('touchmove', function(e){
20
+		if (me.disabled()) return;
21
+		if (me.getScrollTop() > 0) return; // 需在顶部下拉,才禁止bounce
22
+		
23
+		var curPoint = me.getPoint(e); // 当前点
24
+		var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
25
+		// 向下拉
26
+		if (moveY > 0) {
27
+			// 可下拉的条件
28
+			if (!me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && me.isUpBoth))) {
29
+				
30
+				// 只有touch在mescroll的view上面,才禁止bounce
31
+				var el = e.target;
32
+				var isMescrollTouch = false;
33
+				while (el && el.tagName && el.tagName !== 'UNI-PAGE-BODY' && el.tagName != "BODY") {
34
+					var cls = el.classList;
35
+					if (cls && cls.contains('mescroll-render-touch')) {
36
+						isMescrollTouch = true
37
+						break;
38
+					}
39
+					el = el.parentNode; // 继续检查其父元素
40
+				}
41
+				// 禁止bounce (不会对swiper和iOS侧滑返回造成影响)
42
+				if (isMescrollTouch && e.cancelable && !e.defaultPrevented) e.preventDefault();
43
+			}
44
+		}
45
+	}, {passive: false})
46
+}
47
+
48
+/* 获取滚动条的位置 */
49
+me.getScrollTop = function() {
50
+	return me.scrollTop || document.documentElement.scrollTop || document.body.scrollTop || 0
51
+}
52
+
53
+/* 是否禁用下拉刷新 */
54
+me.disabled = function(){
55
+	return !me.optDown || !me.optDown.use || me.optDown.native
56
+}
57
+
58
+/* 根据点击滑动事件获取第一个手指的坐标 */
59
+me.getPoint = function(e) {
60
+	if (!e) {
61
+		return {x: 0,y: 0}
62
+	}
63
+	if (e.touches && e.touches[0]) {
64
+		return {x: e.touches[0].pageX,y: e.touches[0].pageY}
65
+	} else if (e.changedTouches && e.changedTouches[0]) {
66
+		return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
67
+	} else {
68
+		return {x: e.clientX,y: e.clientY}
69
+	}
70
+}
71
+
72
+/**
73
+ * 监听逻辑层数据的变化 (实时更新数据)
74
+ */
75
+function propObserver(wxsProp) {
76
+	me.optDown = wxsProp.optDown
77
+	me.scrollTop = wxsProp.scrollTop
78
+	me.isDownScrolling = wxsProp.isDownScrolling
79
+	me.isUpScrolling = wxsProp.isUpScrolling
80
+	me.isUpBoth = wxsProp.isUpBoth
81
+}
82
+
83
+/* 导出模块 */
84
+const renderBiz = {
85
+	data() {
86
+		return {
87
+			propObserver: propObserver,
88
+		}
89
+	}
90
+}
91
+
92
+export default renderBiz;

+ 269 - 0
src/uni_modules/mescroll-uni/components/mescroll-uni/wxs/wxs.wxs

@@ -0,0 +1,269 @@
1
+// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响
2
+// https://uniapp.dcloud.io/frame?id=wxs
3
+// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html 
4
+
5
+// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致
6
+var me = {}
7
+
8
+// ------ 自定义下拉刷新动画 start ------
9
+
10
+/* 下拉过程中的回调,滑动过程一直在执行 (rate<1为inOffset; rate>1为outOffset) */
11
+me.onMoving = function (ins, rate, downHight){
12
+	ins.requestAnimationFrame(function () {
13
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
14
+			'will-change': 'transform', // 可解决下拉过程中, image和swiper脱离文档流的问题
15
+			'transform': 'translateY(' + downHight + 'px)',
16
+			'transition': ''
17
+		})
18
+		// 环形进度条
19
+		var progress = ins.selectComponent('.mescroll-wxs-progress')
20
+		progress && progress.setStyle({transform: 'rotate(' + 360 * rate + 'deg)'})
21
+	})
22
+}
23
+
24
+/* 显示下拉刷新进度 */
25
+me.showLoading = function (ins){
26
+	me.downHight = me.optDown.offset
27
+	ins.requestAnimationFrame(function () {
28
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
29
+			'will-change': 'auto',
30
+			'transform': 'translateY(' + me.downHight + 'px)',
31
+			'transition': 'transform 300ms'
32
+		})
33
+	})
34
+}
35
+
36
+/* 结束下拉 */
37
+me.endDownScroll = function (ins){
38
+	me.downHight = 0;
39
+	me.isDownScrolling = false;
40
+	ins.requestAnimationFrame(function () {
41
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
42
+			'will-change': 'auto',
43
+			'transform': 'translateY(0)', // 不可以写空串,否则scroll-view渲染不完整 (延时350ms会调clearTransform置空)
44
+			'transition': 'transform 300ms'
45
+		})
46
+	})
47
+}
48
+
49
+/* 结束下拉动画执行完毕后, 清除transform和transition, 避免对列表内容样式造成影响, 如: h5的list-msg示例下拉进度条漏出来等 */
50
+me.clearTransform = function (ins){
51
+	ins.requestAnimationFrame(function () {
52
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
53
+			'will-change': '',
54
+			'transform': '',
55
+			'transition': ''
56
+		})
57
+	})
58
+}
59
+
60
+// ------ 自定义下拉刷新动画 end ------
61
+
62
+/**
63
+ * 监听逻辑层数据的变化 (实时更新数据)
64
+ */
65
+function propObserver(wxsProp) {
66
+	if(!wxsProp) return
67
+	me.optDown = wxsProp.optDown
68
+	me.scrollTop = wxsProp.scrollTop
69
+	me.bodyHeight = wxsProp.bodyHeight
70
+	me.isDownScrolling = wxsProp.isDownScrolling
71
+	me.isUpScrolling = wxsProp.isUpScrolling
72
+	me.isUpBoth = wxsProp.isUpBoth
73
+	me.isScrollBody = wxsProp.isScrollBody
74
+	me.startTop = wxsProp.scrollTop // 及时更新touchstart触发的startTop, 避免scroll-view快速惯性滚动到顶部取值不准确
75
+}
76
+
77
+/**
78
+ * 监听逻辑层数据的变化 (调用wxs的方法)
79
+ */
80
+function callObserver(callProp, oldValue, ins) {
81
+	if (me.disabled()) return;
82
+	if(callProp.callType){
83
+		// 逻辑层(App Service)的style已失效,需在视图层(Webview)设置style
84
+		if(callProp.callType === 'showLoading'){
85
+			me.showLoading(ins)
86
+		}else if(callProp.callType === 'endDownScroll'){
87
+			me.endDownScroll(ins)
88
+		}else if(callProp.callType === 'clearTransform'){
89
+			me.clearTransform(ins)
90
+		}
91
+	}
92
+}
93
+
94
+/**
95
+ * touch事件
96
+ */
97
+function touchstartEvent(e, ins) {
98
+	me.downHight = 0; // 下拉的距离
99
+	me.startPoint = me.getPoint(e); // 记录起点
100
+	me.startTop = me.getScrollTop(); // 记录此时的滚动条位置
101
+	me.startAngle = 0; // 初始角度
102
+	me.lastPoint = me.startPoint; // 重置上次move的点
103
+	me.maxTouchmoveY = me.getBodyHeight() - me.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
104
+	me.inTouchend = false; // 标记不是touchend
105
+	
106
+	me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
107
+}
108
+
109
+function touchmoveEvent(e, ins) {
110
+	var isPrevent = true // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
111
+	
112
+	if (me.disabled()) return isPrevent;
113
+	
114
+	var scrollTop = me.getScrollTop(); // 当前滚动条的距离
115
+	var curPoint = me.getPoint(e); // 当前点
116
+	
117
+	var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
118
+	
119
+	// 向下拉 && 在顶部
120
+	// mescroll-body,直接判定在顶部即可
121
+	// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
122
+	// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
123
+	if (moveY > 0 && (
124
+			(me.isScrollBody && scrollTop <= 0)
125
+			||
126
+			(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
127
+		)) {
128
+		// 可下拉的条件
129
+		if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
130
+				me.isUpBoth))) {
131
+	
132
+			// 下拉的角度是否在配置的范围内
133
+			if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
134
+			if (me.startAngle < me.optDown.minAngle) return isPrevent; // 如果小于配置的角度,则不往下执行下拉刷新
135
+	
136
+			// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
137
+			if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
138
+				me.inTouchend = true; // 标记执行touchend
139
+				touchendEvent(e, ins); // 提前触发touchend
140
+				return isPrevent;
141
+			}
142
+			
143
+			isPrevent = false // 小程序是return false
144
+	
145
+			var diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
146
+	
147
+			// 下拉距离  < 指定距离
148
+			if (me.downHight < me.optDown.offset) {
149
+				if (me.movetype !== 1) {
150
+					me.movetype = 1; // 加入标记,保证只执行一次
151
+					// me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
152
+					me.callMethod(ins, {type: 'setLoadType', downLoadType: 1})
153
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
154
+				}
155
+				me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
156
+	
157
+				// 指定距离  <= 下拉距离
158
+			} else {
159
+				if (me.movetype !== 2) {
160
+					me.movetype = 2; // 加入标记,保证只执行一次
161
+					// me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
162
+					me.callMethod(ins, {type: 'setLoadType', downLoadType: 2})
163
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
164
+				}
165
+				if (diff > 0) { // 向下拉
166
+					me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
167
+				} else { // 向上收
168
+					me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
169
+				}
170
+			}
171
+			
172
+			me.downHight = Math.round(me.downHight) // 取整
173
+			var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
174
+			// me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
175
+			me.onMoving(ins, rate, me.downHight)
176
+		}
177
+	}
178
+	
179
+	me.lastPoint = curPoint; // 记录本次移动的点
180
+	
181
+	return isPrevent // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
182
+}
183
+
184
+function touchendEvent(e, ins) {
185
+	// 如果下拉区域高度已改变,则需重置回来
186
+	if (me.isMoveDown) {
187
+		if (me.downHight >= me.optDown.offset) {
188
+			// 符合触发刷新的条件
189
+			me.downHight = me.optDown.offset; // 更新下拉区域高度
190
+			// me.triggerDownScroll();
191
+			me.callMethod(ins, {type: 'triggerDownScroll'})
192
+		} else {
193
+			// 不符合的话 则重置
194
+			me.downHight = 0;
195
+			// me.optDown.endDownScroll && me.optDown.endDownScroll(me);
196
+			me.callMethod(ins, {type: 'endDownScroll'})
197
+		}
198
+		me.movetype = 0;
199
+		me.isMoveDown = false;
200
+	} else if (!me.isScrollBody && me.getScrollTop() === me.startTop) { // scroll-view到顶/左/右/底的滑动事件
201
+		var isScrollUp = me.getPoint(e).y - me.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
202
+		// 上滑
203
+		if (isScrollUp) {
204
+			// 需检查滑动的角度
205
+			var angle = me.getAngle(me.getPoint(e), me.startPoint); // 两点之间的角度,区间 [0,90]
206
+			if (angle > 80) {
207
+				// 检查并触发上拉
208
+				// me.triggerUpScroll(true);
209
+				me.callMethod(ins, {type: 'triggerUpScroll'})
210
+			}
211
+		}
212
+	}
213
+	me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
214
+}
215
+
216
+/* 是否禁用下拉刷新 */
217
+me.disabled = function(){
218
+	return !me.optDown || !me.optDown.use || me.optDown.native
219
+}
220
+
221
+/* 根据点击滑动事件获取第一个手指的坐标 */
222
+me.getPoint = function(e) {
223
+	if (!e) {
224
+		return {x: 0,y: 0}
225
+	}
226
+	if (e.touches && e.touches[0]) {
227
+		return {x: e.touches[0].pageX,y: e.touches[0].pageY}
228
+	} else if (e.changedTouches && e.changedTouches[0]) {
229
+		return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
230
+	} else {
231
+		return {x: e.clientX,y: e.clientY}
232
+	}
233
+}
234
+
235
+/* 计算两点之间的角度: 区间 [0,90]*/
236
+me.getAngle = function (p1, p2) {
237
+	var x = Math.abs(p1.x - p2.x);
238
+	var y = Math.abs(p1.y - p2.y);
239
+	var z = Math.sqrt(x * x + y * y);
240
+	var angle = 0;
241
+	if (z !== 0) {
242
+		angle = Math.asin(y / z) / Math.PI * 180;
243
+	}
244
+	return angle
245
+}
246
+
247
+/* 获取滚动条的位置 */
248
+me.getScrollTop = function() {
249
+	return me.scrollTop || 0
250
+}
251
+
252
+/* 获取body的高度 */
253
+me.getBodyHeight = function() {
254
+	return me.bodyHeight || 0;
255
+}
256
+
257
+/* 调用逻辑层的方法 */
258
+me.callMethod = function(ins, param) {
259
+	if(ins) ins.callMethod('wxsCall', param)
260
+}
261
+
262
+/* 导出模块 */
263
+module.exports = {
264
+	propObserver: propObserver,
265
+	callObserver: callObserver,
266
+	touchstartEvent: touchstartEvent,
267
+	touchmoveEvent: touchmoveEvent,
268
+	touchendEvent: touchendEvent
269
+}

+ 66 - 0
src/uni_modules/mescroll-uni/hooks/useMescroll.js

@@ -0,0 +1,66 @@
1
+// 小程序无法在hook中使用页面级别生命周期,需单独传入: https://ask.dcloud.net.cn/question/161173
2
+// import { onPageScroll, onReachBottom, onPullDownRefresh} from '@dcloudio/uni-app';
3
+
4
+/** 
5
+ * 初始化mescroll, 相当于vue2的mescroll-mixins.js文件 (mescroll-body 和 mescroll-uni 通用) 
6
+ * mescroll-body需传入onPageScroll, onReachBottom
7
+ * mescroll-uni无需传onPageScroll, onReachBottom
8
+ * 当down.native为true时,需传入onPullDownRefresh
9
+ */ 
10
+function useMescroll(onPageScroll, onReachBottom, onPullDownRefresh){
11
+	// mescroll实例对象
12
+	let mescroll = null;
13
+	
14
+	// mescroll组件初始化的回调,可获取到mescroll对象
15
+	const mescrollInit = (e)=> {
16
+		mescroll = e;
17
+	}
18
+	
19
+	// 获取mescroll对象, mescrollInit执行之后会有值, 生命周期created中会有值
20
+	const getMescroll = ()=>{
21
+		return mescroll
22
+	}
23
+	
24
+	// 下拉刷新的回调 (mixin默认resetUpScroll)
25
+	const downCallback = ()=> {
26
+		if(mescroll.optUp.use){
27
+			mescroll.resetUpScroll()
28
+		}else{
29
+			setTimeout(()=>{
30
+				mescroll.endSuccess();
31
+			}, 500)
32
+		}
33
+	}
34
+	
35
+	// 上拉加载的回调
36
+	const upCallback = ()=> {
37
+		// mixin默认延时500自动结束加载
38
+		setTimeout(()=>{
39
+			mescroll.endErr();
40
+		}, 500)
41
+	}
42
+	
43
+	// 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
44
+	onPullDownRefresh && onPullDownRefresh(() => {
45
+	  mescroll && mescroll.onPullDownRefresh();
46
+	})
47
+	
48
+	// 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
49
+	onPageScroll && onPageScroll(e=>{
50
+		mescroll && mescroll.onPageScroll(e);
51
+	})
52
+	
53
+	// 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
54
+	onReachBottom && onReachBottom(()=>{
55
+		mescroll && mescroll.onReachBottom();
56
+	}) 
57
+	
58
+	return {
59
+		getMescroll,
60
+		mescrollInit,
61
+		downCallback,
62
+		upCallback
63
+	}
64
+}
65
+
66
+export default useMescroll

+ 56 - 0
src/uni_modules/mescroll-uni/hooks/useMescrollComp.js

@@ -0,0 +1,56 @@
1
+import { ref } from 'vue';
2
+
3
+// 小程序无法在hook中使用页面级别生命周期,需单独传入: https://ask.dcloud.net.cn/question/161173
4
+// import { onPageScroll, onReachBottom, onPullDownRefresh} from '@dcloudio/uni-app';
5
+
6
+/** 
7
+ * mescroll-body写在子组件时,需通过useMescrollComp补充子组件缺少的生命周期, 相当于vue2的mescroll-comp.js文件
8
+ * 必须传入onPageScroll, onReachBottom
9
+ * 当down.native为true时,需传入onPullDownRefresh
10
+ */ 
11
+function useMescrollComp(onPageScroll, onReachBottom, onPullDownRefresh){
12
+	// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件
13
+	onPageScroll(e=>{
14
+		handlePageScroll(e)
15
+	})
16
+	
17
+	onReachBottom(()=>{
18
+		handleReachBottom()
19
+	})
20
+	
21
+	// 当down的native: true时, 还需传递此方法进到子组件
22
+	onPullDownRefresh && onPullDownRefresh(()=>{
23
+		handlePullDownRefresh()
24
+	})
25
+	
26
+	const mescrollItem = ref(null)
27
+	
28
+	const handlePageScroll = (e)=>{
29
+		const mescroll = getMescroll()
30
+		mescroll && mescroll.onPageScroll(e);
31
+	}
32
+	
33
+	const handleReachBottom = ()=>{
34
+		const mescroll = getMescroll()
35
+		mescroll && mescroll.onReachBottom();
36
+	}
37
+	
38
+	const handlePullDownRefresh = ()=>{
39
+		const mescroll = getMescroll()
40
+		mescroll && mescroll.onPullDownRefresh();
41
+	}
42
+	
43
+	const getMescroll = ()=>{
44
+		if(mescrollItem.value && mescrollItem.value.getMescroll){
45
+			return mescrollItem.value.getMescroll()
46
+		}
47
+		return null
48
+	}
49
+	
50
+	return {
51
+		mescrollItem,
52
+		getMescroll
53
+	}
54
+}
55
+
56
+export default useMescrollComp

+ 0 - 0
src/uni_modules/mescroll-uni/hooks/useMescrollMore.js


部分文件因文件數量過多而無法顯示