lex 2 سال پیش
والد
کامیت
10c3318630
34فایلهای تغییر یافته به همراه1717 افزوده شده و 229 حذف شده
  1. 63 0
      package-lock.json
  2. 1 0
      package.json
  3. BIN
      src/common/images/icon-check-active.png
  4. BIN
      src/common/images/icon-check.png
  5. BIN
      src/common/images/icon-upload-close.png
  6. BIN
      src/common/images/icon-upload.png
  7. BIN
      src/common/images/logo.png
  8. 183 183
      src/component-ui/index.less
  9. 45 0
      src/components/m-img-code/index.module.less
  10. 140 0
      src/components/m-img-code/index.tsx
  11. 112 0
      src/components/m-popup/index.tsx
  12. 42 0
      src/components/m-protocol/index.module.less
  13. 136 0
      src/components/m-protocol/index.tsx
  14. 59 0
      src/components/m-uploader/index.module.less
  15. 341 10
      src/components/m-uploader/index.tsx
  16. 6 0
      src/helpers/utils.ts
  17. 8 0
      src/router/router-root.ts
  18. 21 23
      src/router/routes-common.ts
  19. 8 1
      src/state.ts
  20. 8 0
      src/styles/index.less
  21. 32 0
      src/views/layout/auth.module.less
  22. 112 0
      src/views/layout/auth.tsx
  23. BIN
      src/views/layout/images/bottom_manage_bg.png
  24. BIN
      src/views/layout/images/bottom_student_bg.png
  25. BIN
      src/views/layout/images/bottom_teacher_bg.png
  26. BIN
      src/views/layout/images/top_bg.png
  27. 84 0
      src/views/layout/login.module.less
  28. 192 0
      src/views/layout/login.tsx
  29. BIN
      src/views/school-register/images/banner.png
  30. BIN
      src/views/school-register/images/icon-school.png
  31. BIN
      src/views/school-register/images/icon-tips.png
  32. 41 0
      src/views/school-register/index.module.less
  33. 70 0
      src/views/school-register/index.tsx
  34. 13 12
      src/views/test/index.tsx

+ 63 - 0
package-lock.json

@@ -13,6 +13,7 @@
         "clean-deep": "^3.4.0",
         "dayjs": "^1.11.7",
         "numeral": "^2.0.6",
+        "query-string": "^8.1.0",
         "umi-request": "^1.4.0",
         "vant": "^4.1.2",
         "vue": "^3.2.47",
@@ -3510,6 +3511,14 @@
         }
       }
     },
+    "node_modules/decode-uri-component": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/decode-uri-component/-/decode-uri-component-0.4.1.tgz",
+      "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==",
+      "engines": {
+        "node": ">=14.16"
+      }
+    },
     "node_modules/deep-is": {
       "version": "0.1.4",
       "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz",
@@ -4212,6 +4221,14 @@
         "node": ">=8"
       }
     },
+    "node_modules/filter-obj": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmmirror.com/filter-obj/-/filter-obj-5.1.0.tgz",
+      "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==",
+      "engines": {
+        "node": ">=14.16"
+      }
+    },
     "node_modules/find-up": {
       "version": "5.0.0",
       "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz",
@@ -6523,6 +6540,19 @@
         "node": ">=0.6"
       }
     },
+    "node_modules/query-string": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmmirror.com/query-string/-/query-string-8.1.0.tgz",
+      "integrity": "sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==",
+      "dependencies": {
+        "decode-uri-component": "^0.4.1",
+        "filter-obj": "^5.1.0",
+        "split-on-first": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=14.16"
+      }
+    },
     "node_modules/queue-microtask": {
       "version": "1.2.3",
       "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -6917,6 +6947,14 @@
       "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
       "deprecated": "Please use @jridgewell/sourcemap-codec instead"
     },
+    "node_modules/split-on-first": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/split-on-first/-/split-on-first-3.0.0.tgz",
+      "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/stdin-discarder": {
       "version": "0.1.0",
       "resolved": "https://registry.npmmirror.com/stdin-discarder/-/stdin-discarder-0.1.0.tgz",
@@ -10432,6 +10470,11 @@
         "ms": "2.1.2"
       }
     },
+    "decode-uri-component": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/decode-uri-component/-/decode-uri-component-0.4.1.tgz",
+      "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ=="
+    },
     "deep-is": {
       "version": "0.1.4",
       "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz",
@@ -10992,6 +11035,11 @@
         "to-regex-range": "^5.0.1"
       }
     },
+    "filter-obj": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmmirror.com/filter-obj/-/filter-obj-5.1.0.tgz",
+      "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng=="
+    },
     "find-up": {
       "version": "5.0.0",
       "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz",
@@ -12800,6 +12848,16 @@
         "side-channel": "^1.0.4"
       }
     },
+    "query-string": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmmirror.com/query-string/-/query-string-8.1.0.tgz",
+      "integrity": "sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==",
+      "requires": {
+        "decode-uri-component": "^0.4.1",
+        "filter-obj": "^5.1.0",
+        "split-on-first": "^3.0.0"
+      }
+    },
     "queue-microtask": {
       "version": "1.2.3",
       "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -13119,6 +13177,11 @@
       "resolved": "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
       "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
     },
+    "split-on-first": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/split-on-first/-/split-on-first-3.0.0.tgz",
+      "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA=="
+    },
     "stdin-discarder": {
       "version": "0.1.0",
       "resolved": "https://registry.npmmirror.com/stdin-discarder/-/stdin-discarder-0.1.0.tgz",

+ 1 - 0
package.json

@@ -26,6 +26,7 @@
     "clean-deep": "^3.4.0",
     "dayjs": "^1.11.7",
     "numeral": "^2.0.6",
+    "query-string": "^8.1.0",
     "umi-request": "^1.4.0",
     "vant": "^4.1.2",
     "vue": "^3.2.47",

BIN
src/common/images/icon-check-active.png


BIN
src/common/images/icon-check.png


BIN
src/common/images/icon-upload-close.png


BIN
src/common/images/icon-upload.png


BIN
src/common/images/logo.png


+ 183 - 183
src/component-ui/index.less

@@ -1,121 +1,141 @@
 // 公用变量
 @import './global.less';
-:global {
-  // 单元格
-  .van-cell-group--inset {
-    margin: 0 var(--k-padding-page);
-    border-radius: var(--k-radius-xl);
-  }
-  .van-cell {
-    font-size: 15px;
-    padding: 12px 18px;
-    color: var(--k-gray-1);
-  }
+// :global {
+// 单元格
+.van-cell-group--inset {
+  margin: 0 var(--k-padding-page);
+  border-radius: var(--k-radius-xl);
+}
+.van-cell {
+  font-size: 15px;
+  padding: 12px 18px;
+  color: var(--k-gray-1);
+}
 
-  // 导航 - ✅
-  .van-nav-bar__left,
-  .van-nav-bar__right {
-    padding: 0 var(--k-padding-md);
-  }
+// 导航 - ✅
+.van-nav-bar__left,
+.van-nav-bar__right {
+  padding: 0 var(--k-padding-md);
+}
 
-  // 选项卡 侧边导航栏(分类选择) - ✅
-  .van-tabs--card {
-    padding: 0;
-    .van-tabs__nav {
-      background-color: transparent;
-    }
-    .van-tabs__nav--card {
-      border-radius: var(--k-radius-max);
-      border: 0;
-    }
-    .van-tab--card {
-      margin-right: var(--k-padding-md);
-      border-right: 0;
-      color: var(--k-gray-1);
-      background-color: #fff;
-      border-radius: var(--k-radius-max);
-      &.van-tab--active {
-        color: #fff;
-        background-color: var(--k-primary);
-      }
-      &:last-child {
-        margin-right: 0;
-      }
+// 选项卡 侧边导航栏(分类选择) - ✅
+.van-tabs--card {
+  padding: 0;
+  .van-tabs__nav {
+    background-color: transparent;
+  }
+  .van-tabs__nav--card {
+    border-radius: var(--k-radius-max);
+    border: 0;
+  }
+  .van-tab--card {
+    margin-right: var(--k-padding-md);
+    border-right: 0;
+    color: var(--k-gray-1);
+    background-color: #fff;
+    border-radius: var(--k-radius-max);
+    &.van-tab--active {
+      color: #fff;
+      background-color: var(--k-primary);
     }
-    .van-tab--shrink {
-      padding: 0 var(--k-padding-lg);
+    &:last-child {
+      margin-right: 0;
     }
   }
+  .van-tab--shrink {
+    padding: 0 var(--k-padding-lg);
+  }
+}
 
-  // 宫格 - ✅
-  // 步骤条 - ✅
-  // 按钮 - ✅
-  .van-button {
-    font-weight: 500;
-    &:active:before {
-      opacity: 0.2;
-    }
+// 宫格 - ✅
+// 步骤条 - ✅
+// 按钮 - ✅
+.van-button {
+  font-weight: 500;
+  &:active:before {
+    opacity: 0.2;
   }
-  .van-button--disabled {
-    position: relative;
-    opacity: 1;
-    &:active:before {
-      opacity: 0.6;
-    }
-    &:before {
-      content: ' ';
-      display: block;
-      position: absolute;
-      top: 50%;
-      left: 50%;
-      width: 100%;
-      height: 100%;
-      background: #fff;
-      border: inherit;
-      border-color: #fff;
-      border-radius: inherit;
-      transform: translate(-50%, -50%);
-      opacity: 0.6;
-    }
+}
+.van-button--disabled {
+  position: relative;
+  opacity: 1;
+  &:active:before {
+    opacity: 0.6;
+  }
+  &:before {
+    content: ' ';
+    display: block;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    width: 100%;
+    height: 100%;
+    background: #fff;
+    border: inherit;
+    border-color: #fff;
+    border-radius: inherit;
+    transform: translate(-50%, -50%);
+    opacity: 0.6;
   }
+}
 
-  // 搜索框 - 【不处理】
-  .van-search__field {
-    padding: 0 var(--van-padding-xs) 0 0;
-  }
+// 搜索框 - 【不处理】
+.van-search__field {
+  padding: 0 var(--van-padding-xs) 0 0;
+}
 
-  // 气泡弹出框 - 【不处理】
-  // 对话框 - ✅
-  // 轻提示 - 【不处理】
-  // 通知栏 - ✅
-  // 遮罩/基础样式 - ✅
-  // 定义了基础变量,如果需要对应的引入; --k-overlay-background-dark, --k-overlay-background-shallow
+// 气泡弹出框 - 【不处理】
+// 对话框 - ✅
+// 轻提示 - 【不处理】
+// 通知栏 - ✅
+// 遮罩/基础样式 - ✅
+// 定义了基础变量,如果需要对应的引入; --k-overlay-background-dark, --k-overlay-background-shallow
+
+// 单元格滑动 - 【不处理】
+// 协议 - 【不处理】
+// 缺省图 - 【不处理】
+// 输入框 - ✅
+.van-field__label {
+  color: var(--k-gray-2);
+  font-size: 15px;
+}
+.van-field__control {
+  font-size: 15px;
+}
+.van-field__control::placeholder {
+  color: var(--k-gray-5);
+}
+.van-cell__right-icon {
+  font-size: 13px;
+  font-weight: bold;
+  color: #d8d8d8;
+}
+.van-field__label--top.border {
+  position: relative;
+  padding-bottom: 10px;
+  margin-bottom: 10px;
+  &::after {
+    display: block;
+    position: absolute;
+    box-sizing: border-box;
+    content: ' ';
+    pointer-events: none;
+    right: var(--van-padding-md);
+    bottom: 0;
+    left: var(--van-padding-md);
+    border-bottom: 0.02667rem solid var(--van-cell-border-color);
+    transform: scaleY(0.5);
+  }
+}
 
-  // 单元格滑动 - 【不处理】
-  // 协议 - 【不处理】
-  // 缺省图 - 【不处理】
-  // 输入框 - ✅
-  .van-field__label {
-    color: var(--k-gray-2);
-    font-size: 15px;
-  }
-  .van-field__control {
-    font-size: 15px;
-  }
-  .van-field__control::placeholder {
-    color: var(--k-gray-5);
-  }
-  .van-cell__right-icon {
-    font-size: 13px;
-    font-weight: bold;
-    color: #d8d8d8;
-  }
-  .van-field__label--top {
+// 选择框
+// 上拉选择 - ✅
+// 选择器 - ✅
+.van-picker {
+  --van-picker-toolbar-height: 44px !important;
+  .van-picker__toolbar {
     position: relative;
-    padding-bottom: 10px;
-    margin-bottom: 10px;
     &::after {
-      display: block;
       position: absolute;
       box-sizing: border-box;
       content: ' ';
@@ -123,96 +143,76 @@
       right: var(--van-padding-md);
       bottom: 0;
       left: var(--van-padding-md);
-      border-bottom: 0.02667rem solid var(--van-cell-border-color);
+      border-bottom: 1px solid var(--van-cell-border-color);
       transform: scaleY(0.5);
     }
   }
-
-  // 选择框
-  // 上拉选择 - ✅
-  // 选择器 - ✅
-  .van-picker {
-    --van-picker-toolbar-height: 44px !important;
-    .van-picker__toolbar {
-      position: relative;
-      &::after {
-        position: absolute;
-        box-sizing: border-box;
-        content: ' ';
-        pointer-events: none;
-        right: var(--van-padding-md);
-        bottom: 0;
-        left: var(--van-padding-md);
-        border-bottom: 1px solid var(--van-cell-border-color);
-        transform: scaleY(0.5);
-      }
-    }
-    .van-picker__columns {
-      padding: 0 24px;
-    }
-    .van-picker-column {
-      position: relative;
-      z-index: 1;
-    }
-    .van-picker__frame {
-      z-index: 0;
-      &::after {
-        background: var(--k-bg-4);
-        border-radius: 8px;
-      }
-    }
-    .van-picker__cancel,
-    .van-picker__confirm {
-      font-size: 14px;
-    }
-    .van-picker__cancel {
-      color: var(--k-gray-3);
-    }
-    .van-picker__confirm {
-      color: var(--k-font-primary);
-    }
-    .van-picker-column__item {
-      color: var(--k-gray-1);
-      font-size: 16px;
-    }
-    .van-picker-column__item--selected {
-      font-weight: 600;
+  .van-picker__columns {
+    padding: 0 24px;
+  }
+  .van-picker-column {
+    position: relative;
+    z-index: 1;
+  }
+  .van-picker__frame {
+    z-index: 0;
+    &::after {
+      background: var(--k-bg-4);
+      border-radius: 8px;
     }
   }
+  .van-picker__cancel,
+  .van-picker__confirm {
+    font-size: 14px;
+  }
+  .van-picker__cancel {
+    color: var(--k-gray-3);
+  }
+  .van-picker__confirm {
+    color: var(--k-font-primary);
+  }
+  .van-picker-column__item {
+    color: var(--k-gray-1);
+    font-size: 16px;
+  }
+  .van-picker-column__item--selected {
+    font-weight: 600;
+  }
+}
 
-  // 日历选择器 - ✅
+// 日历选择器 - ✅
 
-  // 上拉菜单-分享面板 - 【不处理】
-  // 下拉菜单/搜索样式框 - 【不处理】
+// 上拉菜单-分享面板 - 【不处理】
+// 下拉菜单/搜索样式框 - 【不处理】
 
-  // 文件上传 - 【不处理】
-  // 图片展示 - 【不处理】
-  // 图片全屏展示 - ✅
-  .van-image-preview {
-    .van-image-preview__close-icon,
-    .van-image-preview__index {
-      top: calc(var(--van-padding-md) + env(safe-area-inset-bottom));
-    }
+// 文件上传 - 【不处理】
+// 图片展示 - 【不处理】
+// 图片全屏展示 - ✅
+.van-image-preview {
+  .van-image-preview__close-icon,
+  .van-image-preview__index {
+    top: calc(var(--van-padding-md) + env(safe-area-inset-bottom));
   }
-  // 弹出层 - 【不处理】
-  // 开关 / 基础样式 / 禁用样式
+}
+// 弹出层 - 【不处理】
+// 开关 / 基础样式 / 禁用样式
 
-  // 折叠面板 - ✅
-  // 标签样式
+// 折叠面板 - ✅
+// 标签样式
 
-  .van-tag {
-    padding-top: 1px;
-    .van-tag__close {
-      margin-top: -2px;
-    }
-  }
-  .van-tag--large {
-    padding-top: calc(var(--van-padding-base) + 1px);
+.van-tag {
+  padding-top: 1px;
+  .van-tag__close {
+    margin-top: -2px;
   }
-  .van-tag--medium {
-    padding-top: 3px;
-  }
-  // 轮播 - 【不处理】
-  // 骨架屏 - 【不处理】
-  // 进度条 - 【不处理】
 }
+.van-tag--large {
+  padding-top: calc(var(--van-padding-base) + 1px);
+}
+.van-tag--medium {
+  padding-top: 3px;
+}
+// 轮播 - 【不处理】
+// 骨架屏 - 【不处理】
+// 进度条 - 【不处理】
+// }

+ 45 - 0
src/components/m-img-code/index.module.less

@@ -0,0 +1,45 @@
+.imgCode {
+  padding: 16px;
+
+  .codeTitle {
+    text-align: center;
+    font-size: 16px;
+    color: #4f4f4f;
+    margin: 0;
+    padding-bottom: 16px;
+  }
+
+  .img {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .imgChange {
+    display: block;
+    color: #aaaaaa;
+    font-size: 12px;
+    text-align: center;
+    padding-top: 5px;
+  }
+
+  .field {
+    background: #f4f4f4;
+    padding: 10px 12px !important;
+  }
+}
+
+.imgCodePopup {
+  width: 90%;
+  border-radius: 5px;
+  overflow: initial;
+
+  :global {
+    .van-popup__close-icon {
+      top: -37px !important;
+      right: 0 !important;
+      font-size: 25px;
+      color: #fff;
+    }
+  }
+}

+ 140 - 0
src/components/m-img-code/index.tsx

@@ -0,0 +1,140 @@
+import { defineComponent } from 'vue';
+import {
+  Col,
+  Popup,
+  Row,
+  Image as VanImage,
+  Loading,
+  Field,
+  showToast
+} from 'vant';
+import styles from './index.module.less';
+import request from '@/helpers/request';
+
+export default defineComponent({
+  name: 'o-img-code',
+  props: {
+    value: Boolean,
+    phone: [String, Number],
+    type: {
+      type: String,
+      default: 'LOGIN'
+    }
+  },
+  emits: ['close', 'sendCode'],
+  data() {
+    return {
+      isSuffix: '/api-school',
+      showStatus: false,
+      identifyingCode: null as any,
+      code: null
+    };
+  },
+  mounted() {
+    this.showStatus = this.value;
+    this.sendImgCode();
+  },
+  watch: {
+    value(val: any) {
+      this.showStatus = val;
+    },
+    code(val: any) {
+      if (val.length >= 4) {
+        this.checkVerifyLoginImage();
+      }
+    }
+  },
+  methods: {
+    async sendImgCode() {
+      const { data } = await request.get(this.isSuffix + '/open/sendImgCode', {
+        requestType: 'form',
+        hideLoading: true,
+        params: {
+          phone: this.phone
+        }
+      });
+      this.identifyingCode = data;
+    },
+    async updateIdentifyingCode() {
+      this.sendImgCode();
+      // 刷新token
+      // const origin = window.location.origin
+      // this.identifyingCode = `${origin}${this.isSuffix}/code/getImageCode?phone=${
+      //   this.phone
+      // }&token=${Math.random()}`
+    },
+    async checkVerifyLoginImage() {
+      try {
+        if ((this as any).code.length < 4) {
+          return;
+        }
+        await request.post(`${this.isSuffix}/open/verifyImgCode`, {
+          requestType: 'form',
+          hideLoading: true,
+          data: {
+            phone: this.phone,
+            code: this.code
+          }
+        });
+        await request.post(`${this.isSuffix}/open/sendSms`, {
+          requestType: 'form',
+          hideLoading: true,
+          data: {
+            mobile: this.phone,
+            type: this.type,
+            clientId: 'SYSTEM'
+          }
+        });
+        setTimeout(() => {
+          showToast('验证码已发送');
+        }, 100);
+        this.$emit('close');
+        this.$emit('sendCode');
+      } catch {
+        this.updateIdentifyingCode();
+      }
+    }
+  },
+  render() {
+    return (
+      <Popup
+        show={this.showStatus}
+        class={styles.imgCodePopup}
+        closeOnClickOverlay={false}
+        onClose={() => {
+          this.$emit('close');
+        }}
+        closeable
+        closeIcon="close">
+        <div class={styles.imgCode}>
+          <p class={styles.codeTitle}>输入图形验证码</p>
+          <Row>
+            <Col span="14">
+              <Field
+                placeholder="请输入验证码"
+                v-model={this.code}
+                class={styles.field}
+              />
+            </Col>
+            <Col span="10" class={styles.img}>
+              <VanImage
+                src={this.identifyingCode}
+                onClick={() => this.updateIdentifyingCode()}>
+                {{ loading: () => <Loading type="spinner" size="20" /> }}
+              </VanImage>
+            </Col>
+          </Row>
+          <Row style={{ display: 'flex', justifyContent: 'end' }}>
+            <Col span="10">
+              <span
+                class={styles.imgChange}
+                onClick={() => this.updateIdentifyingCode()}>
+                看不清?换一换
+              </span>
+            </Col>
+          </Row>
+        </div>
+      </Popup>
+    );
+  }
+});

+ 112 - 0
src/components/m-popup/index.tsx

@@ -0,0 +1,112 @@
+import { Popup, PopupPosition } from 'vant';
+import { defineComponent, PropType } from 'vue';
+import qs from 'query-string';
+
+export default defineComponent({
+  name: 'col-popup',
+  props: {
+    height: {
+      type: String,
+      default: '100%'
+    },
+    width: {
+      type: String,
+      default: '100%'
+    },
+    teleport: {
+      type: String,
+      default: ''
+    },
+    destroy: {
+      type: Boolean,
+      default: false
+    },
+    modelValue: {
+      type: Boolean,
+      default: false
+    },
+    position: {
+      type: String as PropType<PopupPosition>,
+      default: 'bottom'
+    },
+    zIndex: {
+      type: Number,
+      default: 2018
+    }
+  },
+  emits: ['close', 'update:modelValue'],
+  data() {
+    return {
+      popupShow: false,
+      isDestroy: false
+    };
+  },
+  watch: {
+    modelValue() {
+      this.hashState();
+    }
+  },
+  mounted() {
+    this.destroy && (this.isDestroy = false);
+
+    window.addEventListener('hashchange', this.onHash, false);
+  },
+  unmounted() {
+    window.removeEventListener('hashchange', this.onHash, false);
+  },
+  methods: {
+    onHash() {
+      this.$emit('update:modelValue', false);
+      this.isDestroy = false;
+      this.$emit('close');
+    },
+    onPopupClose(val: boolean) {
+      this.$emit('update:modelValue', val);
+      this.hashState();
+    },
+    hashState() {
+      // 打开弹窗
+      if (this.modelValue) {
+        this.isDestroy = false;
+        const splitUrl = window.location.hash.slice(1).split('?');
+        const query = qs.parse(splitUrl[1]);
+        let times = 0;
+        // eslint-disable-next-line @typescript-eslint/no-unused-vars
+        for (const key in query) {
+          times++;
+        }
+        const origin = window.location.href;
+        const url = times > 0 ? '&sPop=' + +new Date() : '?sPop=' + +new Date();
+        history.pushState('', '', `${origin}${url}`);
+      } else {
+        const splitUrl = window.location.hash.slice(1).split('?');
+        const query = qs.parse(splitUrl[1]);
+        if (query.sPop) {
+          window.history.go(-1);
+        }
+      }
+      if (this.$refs.protocolPopup) {
+        (this.$refs.protocolPopup as any).scrollTop = 0;
+      }
+    }
+  },
+  render() {
+    return (
+      <Popup
+        ref="protocolPopup"
+        show={this.modelValue}
+        transitionAppear={true}
+        position={this.position}
+        teleport={this.teleport}
+        style={{ height: this.height, width: this.width }}
+        zIndex={this.zIndex}
+        onClosed={() => {
+          if (this.destroy) {
+            this.isDestroy = true;
+          }
+        }}>
+        {this.$slots.default && !this.isDestroy && this.$slots.default()}
+      </Popup>
+    );
+  }
+});

+ 42 - 0
src/components/m-protocol/index.module.less

@@ -0,0 +1,42 @@
+.mProtocol {
+  // display: flex;
+  // align-items: center;
+  font-size: 12px;
+  padding: 15px 14px;
+  color: var(--k-gray-4);
+  .protocolText {
+    color: var(--van-primary);
+    line-height: 15px;
+  }
+
+  .boxStyle {
+    background: transparent !important;
+    width: 15px;
+    height: 15px;
+    border: transparent !important;
+  }
+  :global {
+    .van-checkbox {
+      display: inline-block;
+      align-items: inherit;
+      overflow: inherit;
+    }
+    .van-checkbox__icon {
+      height: 15px;
+      line-height: 15px;
+      display: inline-block;
+      vertical-align: sub;
+    }
+    .van-checkbox__label {
+      line-height: 15px;
+      color: var(--k-gray-4);
+    }
+  }
+
+  .protocolContent {
+    font-size: 14px;
+    padding: 12px;
+    color: #333;
+    line-height: 1.4;
+  }
+}

+ 136 - 0
src/components/m-protocol/index.tsx

@@ -0,0 +1,136 @@
+import { Checkbox, Icon, Popup } from 'vant';
+import { defineComponent, PropType } from 'vue';
+import styles from './index.module.less';
+import activeButtonIcon from '@/common/images/icon-check-active.png';
+import inactiveButtonIcon from '@/common/images/icon-check.png';
+import MHeader from '../m-header';
+import request from '@/helpers/request';
+const protocolText: any = {
+  BUY_ORDER: '《管乐团平台服务协议》',
+  REGISTER: '《管乐团平台注册协议》'
+};
+export default defineComponent({
+  name: 'o-protocol',
+  props: {
+    showHeader: {
+      type: Boolean,
+      default: false
+    },
+    modelValue: {
+      type: Boolean,
+      default: false
+    },
+    prototcolType: {
+      type: String as PropType<'BUY_ORDER' | 'REGISTER' | 'WITHDRAW'>,
+      default: 'BUY_ORDER'
+    }
+  },
+  data() {
+    return {
+      exists: true,
+      checked: this.modelValue,
+      popupStatus: false,
+      protocolHTML: '',
+      protocolPopup: null as any
+    };
+  },
+  async mounted() {
+    try {
+      this.checked = this.checked || this.exists;
+      this.$emit('update:modelValue', this.checked || this.exists);
+    } catch {
+      //
+    }
+    this.checked = this.modelValue;
+    window.addEventListener('hashchange', this.onHash, false);
+  },
+  unmounted() {
+    window.removeEventListener('hashchange', this.onHash, false);
+  },
+  watch: {
+    checked(val) {
+      this.$emit('update:modelValue', val);
+    }
+  },
+  methods: {
+    async getContractDetail() {
+      try {
+        // 判断是否有协议内容
+        if (!this.protocolHTML) {
+          const { data } = await request.get(
+            '/api-student/schoolContractTemplate/queryLatestContractTemplate',
+            {
+              params: {
+                contractType: this.prototcolType
+              }
+            }
+          );
+          this.protocolHTML = data.contractTemplateContent;
+        }
+        this.onPopupClose();
+      } catch {
+        //
+      }
+    },
+    onHash() {
+      this.popupStatus = false;
+    },
+    onPopupClose() {
+      this.popupStatus = !this.popupStatus;
+
+      // 打开弹窗
+      if (this.popupStatus) {
+        const route = this.$route;
+        let times = 0;
+        // eslint-disable-next-line @typescript-eslint/no-unused-vars
+        for (const i in route.query) {
+          times += 1;
+        }
+        const origin = window.location.href;
+        const url = times > 0 ? '&pto=' + +new Date() : '?pto=' + +new Date();
+        history.pushState('', '', `${origin}${url}`);
+      } else {
+        window.history.go(-1);
+      }
+      if (this.protocolPopup) {
+        (this.protocolPopup as any).scrollTop = 0;
+      }
+    }
+  },
+  render() {
+    return (
+      <div class={styles.mProtocol}>
+        <Checkbox
+          v-model={this.checked}
+          v-slots={{
+            icon: (props: any) => (
+              <Icon
+                class={styles.boxStyle}
+                name={props.checked ? activeButtonIcon : inactiveButtonIcon}
+              />
+            )
+          }}>
+          我已阅读并同意
+        </Checkbox>
+        <span onClick={this.getContractDetail} class={styles.protocolText}>
+          {protocolText[this.prototcolType]}
+        </span>
+
+        <Popup
+          ref={this.protocolPopup}
+          show={this.popupStatus}
+          position="bottom"
+          style={{ height: '100%' }}>
+          {this.showHeader && <MHeader title="管乐团平台服务协议" />}
+          {this.popupStatus && (
+            <div id="mProtocol">
+              <div
+                class={styles.protocolContent}
+                v-html={this.protocolHTML}></div>
+            </div>
+          )}
+        </Popup>
+      </div>
+    );
+  }
+});

+ 59 - 0
src/components/m-uploader/index.module.less

@@ -0,0 +1,59 @@
+.uploader-section {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  box-sizing: border-box;
+  position: relative;
+  .img-close {
+    position: absolute;
+    top: 5px;
+    right: 12px;
+    z-index: 99;
+    font-size: 10px;
+    background-color: rgba(0, 0, 0, 0.4);
+    color: #fff;
+    font-weight: bold;
+    width: 16px;
+    height: 16px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    border-radius: 50%;
+  }
+  .singleImgClose {
+    right: 5px;
+  }
+
+  .uploader {
+    position: relative;
+    &.default {
+      :global {
+        .van-uploader__upload {
+          width: 76px;
+          height: 76px;
+          background-color: #fff;
+        }
+      }
+      .previewImg {
+        width: 76px;
+        height: 76px;
+        border-radius: 4px;
+        overflow: hidden;
+      }
+
+      .uploadImg {
+        width: 76px;
+        height: 76px;
+        border-radius: 4px;
+        overflow: hidden;
+      }
+    }
+    :global {
+      .van-uploader__upload-icon,
+      .van-icon__image {
+        width: 100%;
+        height: 100%;
+      }
+    }
+  }
+}

+ 341 - 10
src/components/m-uploader/index.tsx

@@ -1,24 +1,355 @@
-import { defineComponent } from 'vue';
+import {
+  closeToast,
+  Icon,
+  Image,
+  showLoadingToast,
+  showToast,
+  Uploader
+} from 'vant';
+import { defineComponent, PropType } from 'vue';
 import styles from './index.module.less';
-import { Uploader } from 'vant';
+import { useCustomFieldValue } from '@vant/use';
+import { postMessage } from '@/helpers/native-message';
+import umiRequest from 'umi-request';
+import iconUploader from '@common/images/icon-upload.png';
+// import iconUploadClose from '@common/images/icon-upload-close.png';
+import request from '@/helpers/request';
+import { getOssUploadUrl } from '@/state';
 
 export default defineComponent({
-  name: 'm-uploader',
+  name: 'col-upload',
   props: {
+    modelValue: {
+      type: Array,
+      default: () => []
+    },
+    deletable: {
+      type: Boolean,
+      default: true
+    },
     maxCount: {
       type: Number,
       default: 1
     },
-    maxSize: {
+    native: {
+      // 是否原生上传
+      type: Boolean,
+      default: false
+    },
+    uploadSize: {
+      // 上传图片大小
       type: Number,
-      default: 2048
+      default: 5
+    },
+    uploadType: {
+      type: String as PropType<'IMAGE' | 'VIDEO'>,
+      default: 'IMAGE'
+    },
+    accept: {
+      type: String,
+      default: 'image/*'
+    },
+    bucket: {
+      type: String,
+      default: 'gyt'
+    },
+    path: {
+      type: String,
+      default: ''
+    },
+    uploadIcon: {
+      type: String,
+      default: iconUploader
+    },
+    size: {
+      type: String,
+      default: 'default'
+    },
+    disabled: {
+      type: Boolean,
+      default: false
     }
   },
-  setup(props) {
-    return () => (
-      <>
-        <Uploader maxCount={props.maxCount} />
-      </>
+  emits: ['uploadChange', 'update:modelValue'],
+  methods: {
+    nativeUpload() {
+      if (this.disabled) {
+        return;
+      }
+      const type = this.uploadType === 'VIDEO' ? 'video' : 'img';
+      postMessage(
+        {
+          api: 'chooseFile',
+          content: { type: type, max: 1, bucket: this.bucket, path: this.path }
+        },
+        (res: any) => {
+          console.log(res, 'fileUrl');
+          // 判断是否是多选
+          if (this.maxCount > 1) {
+            this.$emit('update:modelValue', [...this.modelValue, res.fileUrl]);
+            this.$emit('uploadChange', [...this.modelValue, res.fileUrl]);
+          } else {
+            this.$emit('update:modelValue', [res.fileUrl]);
+            this.$emit('uploadChange', [res.fileUrl]);
+          }
+        }
+      );
+    },
+    beforeRead(file: any) {
+      console.log(file, 'beforeRead');
+      const isLt2M = file.size / 1024 / 1024 < this.uploadSize;
+      if (!isLt2M) {
+        showToast(`上传文件大小不能超过 ${this.uploadSize}MB`);
+        return false;
+      }
+      return true;
+    },
+    beforeDelete() {
+      // this.dataModel.splice(detail.index, 1)
+      return true;
+    },
+    async afterRead(file: any) {
+      try {
+        file.status = 'uploading';
+        file.message = '上传中...';
+        await this.uploadFile(file.file);
+      } catch (error) {
+        closeToast();
+      }
+    },
+    onClose(e: any, item: any) {
+      const models = this.modelValue;
+      const index = models.findIndex(model => model == item);
+      if (index > -1) {
+        models.splice(index, 1);
+        this.$emit('update:modelValue', models);
+        this.$emit('uploadChange');
+      }
+
+      e.stopPropagation();
+    },
+    async getFile(file: any) {
+      try {
+        await this.uploadFile(file);
+      } catch {
+        //
+      }
+    },
+    async uploadFile(file: any) {
+      // 上传文件
+      try {
+        // 获取签名
+        const signUrl = '/api-school/open/getUploadSign';
+        const tempName = file.name || '';
+        const fileName =
+          this.path + '/' + (tempName && tempName.replace(/ /gi, '_'));
+        const key = new Date().getTime() + fileName;
+        console.log(file);
+
+        const res = await request.post(signUrl, {
+          data: {
+            filename: fileName,
+            bucketName: this.bucket,
+            postData: {
+              filename: fileName,
+              acl: 'public-read',
+              key: key,
+              unknowValueField: []
+            }
+          }
+        });
+        showLoadingToast({
+          message: '加载中...',
+          forbidClick: true,
+          loadingType: 'spinner',
+          duration: 0
+        });
+        const obj = {
+          policy: res.data.policy,
+          signature: res.data.signature,
+          key: key,
+          KSSAccessKeyId: res.data.kssAccessKeyId,
+          acl: 'public-read',
+          name: fileName
+        };
+        const formData = new FormData();
+        for (const key in obj) {
+          formData.append(key, obj[key]);
+        }
+        formData.append('file', file, fileName);
+        await umiRequest(getOssUploadUrl(this.bucket), {
+          method: 'POST',
+          data: formData
+        });
+        console.log(getOssUploadUrl(this.bucket) + key);
+        const uploadUrl = getOssUploadUrl(this.bucket) + key;
+        closeToast();
+        // 判断是否是多选
+        if (this.maxCount > 1) {
+          this.$emit('update:modelValue', [...this.modelValue, uploadUrl]);
+          this.$emit('uploadChange', [...this.modelValue, uploadUrl]);
+        } else {
+          this.$emit('update:modelValue', [uploadUrl]);
+          this.$emit('uploadChange', [uploadUrl]);
+        }
+      } catch (error) {
+        console.log(error, 'uploadFile');
+      }
+    }
+  },
+  render() {
+    useCustomFieldValue(() => this.modelValue);
+
+    return (
+      <div class={styles['uploader-section']}>
+        {this.modelValue.length > 0 &&
+          this.maxCount > 1 &&
+          this.modelValue.map((item: any) => (
+            <div class={[styles.uploader, styles[this.size]]}>
+              {/* 删除按钮 */}
+              {this.deletable && !this.disabled && (
+                <Icon
+                  name="cross"
+                  onClick={(e: any) => this.onClose(e, item)}
+                  class={styles['img-close']}
+                />
+              )}
+              <div class={['van-uploader__upload']}>
+                {this.uploadType === 'IMAGE' ? (
+                  <Image src={item} class={styles.previewImg} fit="cover" />
+                ) : (
+                  <video
+                    ref="videoUpload"
+                    style={{ backgroundColor: '#F8F8F8' }}
+                    class={styles.previewImg}
+                    src={item + '#t=1,4'}
+                  />
+                )}
+              </div>
+            </div>
+          ))}
+
+        {this.native ? (
+          this.maxCount > 1 ? (
+            // 小于长度才显示
+            this.modelValue.length < this.maxCount && (
+              <div
+                class={[styles.uploader, styles[this.size]]}
+                onClick={this.nativeUpload}>
+                <Icon
+                  name={this.uploadIcon}
+                  class={['van-uploader__upload']}
+                  size="32"
+                />
+              </div>
+            )
+          ) : (
+            <div
+              class={[styles.uploader, styles[this.size]]}
+              onClick={this.nativeUpload}>
+              {this.modelValue.length > 0 ? (
+                <div class={['van-uploader__upload']}>
+                  {this.modelValue.map((item: any) => (
+                    <>
+                      {/* 删除按钮 */}
+                      {this.deletable && !this.disabled && (
+                        <Icon
+                          name="cross"
+                          onClick={(e: any) => this.onClose(e, item)}
+                          class={[styles['img-close'], styles.singleImgClose]}
+                        />
+                      )}
+                      {this.uploadType === 'IMAGE' ? (
+                        <Image
+                          fit="cover"
+                          position="center"
+                          class={styles.uploadImg}
+                          src={item}
+                        />
+                      ) : (
+                        <video
+                          ref="videoUpload"
+                          class={styles.uploadImg}
+                          style={{ backgroundColor: '#F8F8F8' }}
+                          src={item + '#t=1,4'}
+                        />
+                      )}
+                    </>
+                  ))}
+                </div>
+              ) : (
+                <Icon
+                  name={this.uploadIcon}
+                  class={['van-uploader__upload']}
+                  size="32"
+                />
+              )}
+            </div>
+          )
+        ) : this.maxCount > 1 ? (
+          // 小于长度才显示
+          this.modelValue.length < this.maxCount && (
+            <Uploader
+              class={[styles.uploader, styles[this.size]]}
+              afterRead={this.afterRead}
+              beforeRead={this.beforeRead}
+              beforeDelete={this.beforeDelete}
+              uploadIcon={this.uploadIcon}
+              maxCount={this.maxCount}
+              disabled={this.disabled}
+              accept={this.accept}
+            />
+          )
+        ) : (
+          <Uploader
+            class={[styles.uploader, styles[this.size]]}
+            afterRead={this.afterRead}
+            beforeRead={this.beforeRead}
+            beforeDelete={this.beforeDelete}
+            uploadIcon={this.uploadIcon}
+            accept={this.accept}
+            disabled={this.disabled}>
+            {this.modelValue.length > 0 ? (
+              <div class={['van-uploader__upload']}>
+                {this.modelValue.map((item: any) => (
+                  <>
+                    {/* 删除按钮 */}
+                    {this.deletable && !this.disabled && (
+                      <Icon
+                        name="cross"
+                        onClick={(e: any) => this.onClose(e, item)}
+                        class={[styles['img-close'], styles.singleImgClose]}
+                      />
+                    )}
+
+                    {this.uploadType === 'IMAGE' ? (
+                      <Image
+                        fit="cover"
+                        position="center"
+                        class={styles.uploadImg}
+                        src={item}
+                      />
+                    ) : (
+                      <video
+                        ref="videoUpload"
+                        class={styles.uploadImg}
+                        style={{ backgroundColor: '#F8F8F8' }}
+                        src={item + '#t=1,4'}
+                      />
+                    )}
+                  </>
+                ))}
+              </div>
+            ) : (
+              <Icon
+                name={this.uploadIcon}
+                class={['van-uploader__upload']}
+                size="32"
+              />
+            )}
+          </Uploader>
+        )}
+      </div>
     );
   }
 });

+ 6 - 0
src/helpers/utils.ts

@@ -50,3 +50,9 @@ export const setAuth = (token: any) => {
 export const getAuth = () => {
   sessionStorage.getItem('Authorization');
 };
+
+export function checkPhone(phone: string) {
+  const phoneRule =
+    /^((13[0-9])|(14(0|[5-7]|9))|(15([0-3]|[5-9]))|(16(2|[5-7]))|(17[0-8])|(18[0-9])|(19([0-3]|[5-9])))\d{8}$/;
+  return phoneRule.test(phone);
+}

+ 8 - 0
src/router/router-root.ts

@@ -1,6 +1,14 @@
 // 不需要登录的路由
 export default [
   {
+    path: '/school-register',
+    name: 'school-register',
+    component: () => import('@/views/school-register/index'),
+    meta: {
+      title: '乐团注册'
+    }
+  },
+  {
     path: '/test',
     name: 'test',
     component: () => import('@/views/test/index'),

+ 21 - 23
src/router/routes-common.ts

@@ -1,4 +1,4 @@
-// import Auth from '@/views/layout/auth';
+import Auth from '@/views/layout/auth';
 import rootRouter from './router-root';
 
 type metaType = {
@@ -6,29 +6,27 @@ type metaType = {
 };
 
 export default [
-  // {
-  //   path: '/',
-  //   component: Auth,
-  //   children: [
-  //     ...router,
-  //     {
-  //       path: '/login',
-  //       name: 'login',
-  //       component: () => import('@/views/layout/login'),
-  //       meta: {
-  //         isRegister: false
-  //       } as metaType
-  //     }
-  //   ]
-  // },
   {
-    path: '/home',
-    name: 'home',
-    component: () => import('@/views/home/index'),
-    meta: {
-      title: '首页'
-    }
+    path: '/',
+    component: Auth,
+    children: [
+      {
+        path: '/login',
+        name: 'login',
+        component: () => import('@/views/layout/login'),
+        meta: {
+          isRegister: false
+        } as metaType
+      },
+      {
+        path: '/home',
+        name: 'home',
+        component: () => import('@/views/home/index'),
+        meta: {
+          title: '首页'
+        }
+      }
+    ]
   },
-
   ...rootRouter
 ];

+ 8 - 1
src/state.ts

@@ -9,9 +9,16 @@ export const state = reactive({
     status: 'init' as status,
     data: {} as any
   },
-  navBarHeight: 0 // 状态栏高度
+  navBarHeight: 0, // 状态栏高度
+  ossUploadUrl: 'https://ks3-cn-beijing.ksyuncs.com/'
 });
 
+// 预览上传到oss的地址
+export const getOssUploadUrl = (bucket: string) => {
+  const tmpBucket = bucket || 'gym';
+  return `https://${tmpBucket}.ks3-cn-beijing.ksyuncs.com/`;
+};
+
 export const setLoginInit = () => {
   state.user.status = 'init';
   state.user.data = null;

+ 8 - 0
src/styles/index.less

@@ -31,3 +31,11 @@ body {
   background-color: #f8f9fc;
   user-select: none;
 }
+
+// 固定底部按钮样式
+
+.btnGroupFixed {
+  padding: 0 25px;
+  padding-bottom: calc(20px + constant(safe-area-inset-bottom));
+  padding-bottom: calc(20px + env(safe-area-inset-bottom));
+}

+ 32 - 0
src/views/layout/auth.module.less

@@ -0,0 +1,32 @@
+.error {
+  background-color: #fff;
+  display: flex;
+  // padding-top: 20px;
+  flex-direction: column;
+  min-height: calc(100vh);
+  align-items: center;
+  justify-content: center;
+  .info {
+    display: flex;
+    align-items: center;
+    margin-bottom: 30px;
+
+    span {
+      display: inline-block;
+      margin-left: 10px;
+      color: #58727e;
+      font-size: 18px;
+    }
+  }
+
+  :global {
+    .o-result-container,
+    .van-empty {
+      padding-top: 0;
+    }
+
+    .van-button {
+      width: 50%;
+    }
+  }
+}

+ 112 - 0
src/views/layout/auth.tsx

@@ -0,0 +1,112 @@
+import { defineComponent } from 'vue';
+import styles from './auth.module.less';
+import { state, setLogin, setLogout, setLoginError } from '@/state';
+import { browser, setAuth } from '@/helpers/utils';
+import { postMessage } from '@/helpers/native-message';
+import { RouterView } from 'vue-router';
+import request from '@/helpers/request';
+import MHeader from '@/components/m-header';
+import MEmpty from '@/components/m-empty';
+
+export default defineComponent({
+  name: 'Auth-loayout',
+  data() {
+    return {
+      loading: false as boolean
+    };
+  },
+  computed: {
+    isExternal() {
+      // 该路由在外部连接打开是否需要登录
+      // 只判断是否在学员端打开
+      return this.$route.meta.isExternal || false;
+    },
+    isNeedView() {
+      return (
+        state.user.status === 'login' ||
+        this.$route.path === '/login' ||
+        (this as any).isExternal
+      );
+    }
+  },
+  mounted() {
+    !this.isExternal && this.setAuth();
+  },
+  methods: {
+    async setAuth() {
+      const { query } = this.$route;
+      const token = query.userInfo || query.Authorization;
+
+      if (token) {
+        setAuth(token);
+      }
+      if (this.loading) {
+        return;
+      }
+      if (state.user.status === 'init' || state.user.status === 'error') {
+        this.loading = true;
+        try {
+          const res = await request.get('/api-school/user/getUserInfo', {
+            initRequest: true, // 初始化接口
+            requestType: 'form',
+            hideLoading: true
+          });
+          setLogin(res.data);
+        } catch (e: any) {
+          // console.log(e, 'e')
+          const message = e.message;
+          if (
+            message.indexOf('5000') === -1 &&
+            message.indexOf('authentication') === -1
+          ) {
+            setLoginError();
+          } else {
+            setLogout();
+          }
+        }
+        this.loading = false;
+      }
+      if (state.user.status === 'logout') {
+        if (browser().isApp) {
+          postMessage({ api: 'login' });
+        } else {
+          try {
+            const route = this.$route;
+            const query = {
+              returnUrl: this.$route.path,
+              ...this.$route.query
+            } as any;
+            if (route.meta.isRegister) {
+              query.isRegister = route.meta.isRegister;
+            }
+            this.$router.replace({
+              path: '/login',
+              query: query
+            });
+          } catch (error) {
+            //
+          }
+        }
+      }
+    }
+  },
+  render() {
+    return (
+      <>
+        {state.user.status === 'error' ? (
+          <div class={styles.error}>
+            <MHeader />
+            <MEmpty
+              image="network"
+              description="加载失败,请稍后重试"
+              buttonText="重新加载"
+              onClick={this.setAuth}
+            />
+          </div>
+        ) : this.isNeedView ? (
+          <RouterView></RouterView>
+        ) : null}
+      </>
+    );
+  }
+});

BIN
src/views/layout/images/bottom_manage_bg.png


BIN
src/views/layout/images/bottom_student_bg.png


BIN
src/views/layout/images/bottom_teacher_bg.png


BIN
src/views/layout/images/top_bg.png


+ 84 - 0
src/views/layout/login.module.less

@@ -0,0 +1,84 @@
+.login {
+  min-height: 100vh;
+  padding: 0 48px;
+  background: linear-gradient(to bottom, #01c1b5, #1bcbbf);
+
+  .codeText {
+    color: #fff;
+  }
+
+  .logo {
+    display: table;
+    padding-top: 100px;
+    padding-bottom: 90px;
+    width: 160px;
+    height: 45px;
+    margin: 0 auto;
+    img {
+      width: inherit;
+      height: inherit;
+    }
+  }
+  .container {
+    background-color: transparent;
+  }
+
+  .input-group {
+    position: relative;
+    border-radius: 50px;
+    border: 2px solid #fff;
+    margin-bottom: 20px;
+    padding-left: 30px;
+    padding-right: 30px;
+    display: flex;
+    align-items: center;
+    background: transparent;
+    input {
+      flex: 1;
+      font-size: 14px;
+      color: #fff;
+
+      background: transparent;
+      border: none;
+      &::placeholder {
+        color: #fff;
+      }
+    }
+    .code-text {
+      position: absolute;
+      right: 0;
+      flex: 1;
+      display: block;
+      width: 94px;
+      text-align: center;
+
+      font-size: 14px;
+      color: #fff;
+      line-height: 30px;
+      height: 30px;
+    }
+
+    :global {
+      .van-field__button {
+        border-left: 2px solid #fff;
+        margin-left: 12px;
+        margin-right: -18px;
+      }
+    }
+  }
+
+  .login-change {
+    padding-top: 8px;
+    font-size: 14px;
+    color: #fff;
+    float: right;
+    cursor: pointer;
+  }
+
+  :global {
+    .van-button--disabled {
+      opacity: 1;
+      color: rgba(0, 0, 0, 0.25);
+    }
+  }
+}

+ 192 - 0
src/views/layout/login.tsx

@@ -0,0 +1,192 @@
+import { defineComponent } from 'vue';
+import { CellGroup, Field, Button, CountDown, showToast } from 'vant';
+import ImgCode from '@/components/m-img-code';
+import request from '@/helpers/request';
+import { setLogin, state } from '@/state';
+import { checkPhone, removeAuth, setAuth } from '@/helpers/utils';
+import styles from './login.module.less';
+import logo from '@/common/images/logo.png';
+
+type loginType = 'PWD' | 'SMS';
+export default defineComponent({
+  name: 'layout-login',
+  data() {
+    return {
+      loginType: 'SMS' as loginType,
+      username: '',
+      password: '',
+      smsCode: '',
+      countDownStatus: true, // 是否发送验证码
+      countDownTime: 1000 * 120, // 倒计时时间
+      // countDownRef: null as any, // 倒计时实例
+      imgCodeStatus: false
+    };
+  },
+  computed: {
+    codeDisable() {
+      let status = true;
+      if (this.loginType === 'PWD') {
+        this.username && this.password && (status = false);
+      } else {
+        this.username && this.smsCode && (status = false);
+      }
+      return status;
+    }
+  },
+  mounted() {
+    removeAuth();
+    this.directNext();
+  },
+  methods: {
+    directNext() {
+      if (state.user.status === 'login' || state.user.status === 'error') {
+        // eslint-disable-next-line @typescript-eslint/no-unused-vars
+        const { returnUrl, isRegister, ...rest } = this.$route.query;
+        this.$router.replace({
+          path: returnUrl as any,
+          query: {
+            ...rest
+          }
+        });
+      }
+    },
+    async onLogin() {
+      try {
+        // let res: any
+        const forms: any = {
+          username: this.username,
+          client_id: 'SYSTEM',
+          client_secret: 'SYSTEM'
+        };
+
+        if (this.loginType === 'PWD') {
+          forms.password = this.password;
+          forms.loginType = 'PASSWORD';
+          forms.grant_type = 'password';
+        } else {
+          forms.password = this.smsCode;
+          forms.loginType = 'SMS';
+          forms.grant_type = 'password';
+        }
+        const { data } = await request.post('/api-auth/usernameLogin', {
+          requestType: 'form',
+          data: {
+            ...forms
+          }
+        });
+
+        setAuth(data.token_type + ' ' + data.access_token);
+
+        const userCash = await request.get('/api-school/user/getUserInfo', {
+          initRequest: true // 初始化接口
+        });
+        setLogin(userCash.data);
+
+        this.directNext();
+      } catch {
+        //
+      }
+    },
+    async onSendCode() {
+      // 发送验证码
+      if (!checkPhone(this.username)) {
+        return showToast('请输入正确的手机号码');
+      }
+      this.imgCodeStatus = true;
+    },
+    onCodeSend() {
+      this.countDownStatus = false;
+      this.$nextTick(() => {
+        (this.$refs.countDownRef as any).start();
+      });
+    },
+    onFinished() {
+      this.countDownStatus = true;
+      (this.$refs.countDownRef as any).reset();
+    },
+    onChange() {
+      if (this.loginType === 'PWD') {
+        this.loginType = 'SMS';
+      } else if (this.loginType === 'SMS') {
+        this.loginType = 'PWD';
+      }
+    }
+  },
+  render() {
+    return (
+      <div class={[styles.login]}>
+        <div class={styles.logo}>
+          <img src={logo} />
+        </div>
+        <CellGroup class={styles.container} border={false}>
+          <Field
+            v-model={this.username}
+            name="手机号"
+            placeholder="请输入您的手机号"
+            type="tel"
+            class={styles['input-group']}
+            maxlength={11}
+          />
+
+          {this.loginType === 'PWD' ? (
+            <Field
+              v-model={this.password}
+              type="password"
+              name="密码"
+              class={styles['input-group']}
+              placeholder="请输入密码"
+            />
+          ) : (
+            <Field
+              v-model={this.smsCode}
+              name="验证码"
+              placeholder="请输入验证码"
+              type="tel"
+              class={styles['input-group']}
+              maxlength={6}
+              v-slots={{
+                button: () =>
+                  this.countDownStatus ? (
+                    <span class={styles.codeText} onClick={this.onSendCode}>
+                      获取验证码
+                    </span>
+                  ) : (
+                    <CountDown
+                      ref="countDownRef"
+                      auto-start={false}
+                      time={this.countDownTime}
+                      onFinish={this.onFinished}
+                      format="ss秒"
+                    />
+                  )
+              }}
+            />
+          )}
+        </CellGroup>
+        <div class={styles.margin34}>
+          <Button
+            round
+            block
+            disabled={this.codeDisable}
+            onClick={this.onLogin}>
+            提交
+          </Button>
+          <span class={styles['login-change']} onClick={this.onChange}>
+            {this.loginType === 'PWD' ? '验证码登录' : '密码登录'}
+          </span>
+        </div>
+
+        {this.imgCodeStatus ? (
+          <ImgCode
+            v-model:value={this.imgCodeStatus}
+            phone={this.username}
+            onClose={() => {
+              this.imgCodeStatus = false;
+            }}
+            onSendCode={this.onCodeSend}
+          />
+        ) : null}
+      </div>
+    );
+  }
+});

BIN
src/views/school-register/images/banner.png


BIN
src/views/school-register/images/icon-school.png


BIN
src/views/school-register/images/icon-tips.png


+ 41 - 0
src/views/school-register/index.module.less

@@ -0,0 +1,41 @@
+.school-register {
+  --van-cell-font-size: 16px;
+
+  .banner {
+    width: 100%;
+
+    img {
+      width: inherit;
+    }
+  }
+  .required {
+    color: #ff5a56;
+  }
+  :global {
+    .van-field__label {
+      color: var(--k-gray-1);
+      font-size: 16px;
+    }
+    .van-cell-group {
+      margin-top: 12px;
+    }
+  }
+
+  .tips {
+    height: 28px;
+    display: flex;
+    align-items: center;
+    background: #edf9f7;
+    border-radius: 6px;
+    padding: 0 8px;
+    font-size: 13px;
+    color: var(--k-font-primary);
+    line-height: 18px;
+
+    .iconTips {
+      width: 16px;
+      height: 16px;
+      margin-right: 4px;
+    }
+  }
+}

+ 70 - 0
src/views/school-register/index.tsx

@@ -0,0 +1,70 @@
+import { defineComponent, reactive } from 'vue';
+import styles from './index.module.less';
+import { Button, Cell, CellGroup, Field, Image } from 'vant';
+import banner from './images/banner.png';
+// import iconSchool from './images/icon-school.png';
+import iconTips from './images/icon-tips.png';
+import MSticky from '@/components/m-sticky';
+import MProtocol from '@/components/m-protocol';
+
+export default defineComponent({
+  name: 'school-register',
+  setup() {
+    const forms = reactive({
+      username: '',
+      phone: '',
+      isAgree: false
+    });
+    return () => (
+      <div class={styles['school-register']}>
+        <div class={styles.banner}>
+          <img src={banner} />
+        </div>
+
+        <CellGroup inset>
+          <Field
+            labelAlign="top"
+            class="border"
+            v-model={forms.username}
+            placeholder="请填写谷尚居的真实姓名">
+            {{
+              label: () => (
+                <>
+                  真实姓名<i class={styles.required}>*</i>
+                </>
+              )
+            }}
+          </Field>
+          <Field
+            labelAlign="top"
+            v-model={forms.phone}
+            placeholder="请填写您的手机号码">
+            {{
+              label: () => (
+                <>
+                  手机号码<i class={styles.required}>*</i>
+                </>
+              )
+            }}
+          </Field>
+
+          <Cell>
+            <div class={styles.tips}>
+              <Image src={iconTips} class={styles.iconTips} />
+              提示:手机号码将成为您管乐迷学校端登录账户
+            </div>
+          </Cell>
+        </CellGroup>
+
+        <MSticky position="bottom">
+          <MProtocol style={{ textAlign: 'center' }} />
+          <div class={['btnGroupFixed']}>
+            <Button round block type="primary">
+              提交
+            </Button>
+          </div>
+        </MSticky>
+      </div>
+    );
+  }
+});

+ 13 - 12
src/views/test/index.tsx

@@ -3,18 +3,14 @@ import MFullRefresh from '@/components/m-full-refresh';
 import MHeader from '@/components/m-header';
 import MSearch from '@/components/m-search';
 import MSticky from '@/components/m-sticky';
-import MUploader from '@/components/m-uploader';
+
 import {
-  Button,
   Cell,
   CellGroup,
   showLoadingToast,
-  Image,
   Skeleton,
   SkeletonAvatar,
-  SkeletonParagraph,
-  Search,
-  Field
+  SkeletonParagraph
 } from 'vant';
 import { defineComponent, reactive, ref } from 'vue';
 
@@ -24,7 +20,8 @@ export default defineComponent({
     const refreshing = ref(false);
     const loading = ref(true);
     const forms = reactive({
-      search: ''
+      search: '',
+      files: [] as any
     });
     const getData = () => {
       refreshing.value = true;
@@ -50,10 +47,14 @@ export default defineComponent({
           <MSearch v-model:modelValue={forms.search} />
         </MSticky>
 
-        <Field style={{ marginTop: '12px' }} label="上传图片">
-          {{ input: () => <MUploader /> }}
-        </Field>
-        {/* <MFullRefresh
+        {/* <Field style={{ marginTop: '12px' }} label="上传图片">
+          {{
+            input: () => (
+              <MUploader maxCount={2} v-model:modelValue={forms.files} />
+            )
+          }}
+        </Field> */}
+        <MFullRefresh
           v-model:modelValue={refreshing.value}
           onRefresh={getData}
           style={{
@@ -144,7 +145,7 @@ export default defineComponent({
               )
             }}
           </Skeleton>
-        </MFullRefresh> */}
+        </MFullRefresh>
 
         {/* <Button type="primary" onClick={onClick}>
           提交