Browse Source

拉取vue版本代码

zhangyiming 4 years ago
parent
commit
04271f3ea6
90 changed files with 5344 additions and 2 deletions
  1. 1 1
      README.md
  2. 5 0
      frontend/vue-ts/.env
  3. 11 0
      frontend/vue-ts/.env.development
  4. 2 0
      frontend/vue-ts/.env.production
  5. 4 0
      frontend/vue-ts/.gitignore
  6. 55 1
      frontend/vue-ts/README.md
  7. 6 0
      frontend/vue-ts/babel.config.js
  8. 19 0
      frontend/vue-ts/build/proxy.ts
  9. 38 0
      frontend/vue-ts/build/utils.ts
  10. 258 0
      frontend/vue-ts/index.html
  11. 1142 0
      frontend/vue-ts/package-lock.json
  12. 41 0
      frontend/vue-ts/package.json
  13. 3 0
      frontend/vue-ts/postcss.config.js
  14. 10 0
      frontend/vue-ts/public/animate.css
  15. BIN
      frontend/vue-ts/public/favicon.ico
  16. 18 0
      frontend/vue-ts/public/iconfont.css
  17. 3 0
      frontend/vue-ts/src/App.vue
  18. 16 0
      frontend/vue-ts/src/api/user.ts
  19. BIN
      frontend/vue-ts/src/assets/401.gif
  20. BIN
      frontend/vue-ts/src/assets/404.png
  21. BIN
      frontend/vue-ts/src/assets/404_cloud.png
  22. BIN
      frontend/vue-ts/src/assets/bg.png
  23. BIN
      frontend/vue-ts/src/assets/ch.png
  24. BIN
      frontend/vue-ts/src/assets/en.png
  25. 25 0
      frontend/vue-ts/src/assets/iconfont/iconfont.css
  26. BIN
      frontend/vue-ts/src/assets/iconfont/iconfont.eot
  27. 0 0
      frontend/vue-ts/src/assets/iconfont/iconfont.js
  28. 23 0
      frontend/vue-ts/src/assets/iconfont/iconfont.json
  29. 32 0
      frontend/vue-ts/src/assets/iconfont/iconfont.svg
  30. BIN
      frontend/vue-ts/src/assets/iconfont/iconfont.ttf
  31. BIN
      frontend/vue-ts/src/assets/iconfont/iconfont.woff
  32. BIN
      frontend/vue-ts/src/assets/iconfont/iconfont.woff2
  33. BIN
      frontend/vue-ts/src/assets/login.png
  34. BIN
      frontend/vue-ts/src/assets/welcome - 副本.png
  35. BIN
      frontend/vue-ts/src/assets/welcome.png
  36. 95 0
      frontend/vue-ts/src/components/breadCrumb/index.vue
  37. 50 0
      frontend/vue-ts/src/components/hamBurger/index.vue
  38. 207 0
      frontend/vue-ts/src/components/info/index.vue
  39. 216 0
      frontend/vue-ts/src/components/splitPane/index.vue
  40. 59 0
      frontend/vue-ts/src/components/splitPane/resizer.vue
  41. 46 0
      frontend/vue-ts/src/layout/components/AppMain.vue
  42. 178 0
      frontend/vue-ts/src/layout/components/Navbar.vue
  43. 4 0
      frontend/vue-ts/src/layout/components/index.ts
  44. 133 0
      frontend/vue-ts/src/layout/components/panel/index.vue
  45. 77 0
      frontend/vue-ts/src/layout/components/screenfull/index.vue
  46. 74 0
      frontend/vue-ts/src/layout/components/setting/index.vue
  47. 30 0
      frontend/vue-ts/src/layout/components/sidebar/Link.vue
  48. 101 0
      frontend/vue-ts/src/layout/components/sidebar/SidebarItem.vue
  49. 55 0
      frontend/vue-ts/src/layout/components/sidebar/index.vue
  50. 51 0
      frontend/vue-ts/src/layout/components/tag/index.vue
  51. 176 0
      frontend/vue-ts/src/layout/index.vue
  52. 14 0
      frontend/vue-ts/src/layout/store.ts
  53. 11 0
      frontend/vue-ts/src/locales/ch.json
  54. 11 0
      frontend/vue-ts/src/locales/en.json
  55. 35 0
      frontend/vue-ts/src/main.ts
  56. 171 0
      frontend/vue-ts/src/router/index.ts
  57. 9 0
      frontend/vue-ts/src/settings.ts
  58. 10 0
      frontend/vue-ts/src/shims-vue.d.ts
  59. 6 0
      frontend/vue-ts/src/store/getters.ts
  60. 12 0
      frontend/vue-ts/src/store/index.ts
  61. 58 0
      frontend/vue-ts/src/store/modules/app.ts
  62. 29 0
      frontend/vue-ts/src/store/modules/settings.ts
  63. 48 0
      frontend/vue-ts/src/style/element-ui.scss
  64. 102 0
      frontend/vue-ts/src/style/index.scss
  65. 28 0
      frontend/vue-ts/src/style/mixin.scss
  66. 201 0
      frontend/vue-ts/src/style/sidebar.scss
  67. 44 0
      frontend/vue-ts/src/style/transition.scss
  68. 22 0
      frontend/vue-ts/src/style/variables.scss
  69. 24 0
      frontend/vue-ts/src/utils/algorithm/index.ts
  70. 11 0
      frontend/vue-ts/src/utils/debounce/index.ts
  71. 31 0
      frontend/vue-ts/src/utils/http/config.ts
  72. 244 0
      frontend/vue-ts/src/utils/http/core.ts
  73. 3 0
      frontend/vue-ts/src/utils/http/index.ts
  74. 48 0
      frontend/vue-ts/src/utils/http/types.d.ts
  75. 29 0
      frontend/vue-ts/src/utils/http/utils.ts
  76. 54 0
      frontend/vue-ts/src/utils/loaders/index.ts
  77. 43 0
      frontend/vue-ts/src/utils/message/index.ts
  78. 14 0
      frontend/vue-ts/src/utils/operate/index.ts
  79. 12 0
      frontend/vue-ts/src/utils/progress/index.ts
  80. 32 0
      frontend/vue-ts/src/utils/resize/index.ts
  81. 42 0
      frontend/vue-ts/src/utils/storage/index.ts
  82. 85 0
      frontend/vue-ts/src/views/components/split-pane/index.vue
  83. 73 0
      frontend/vue-ts/src/views/error/401.vue
  84. 236 0
      frontend/vue-ts/src/views/error/404.vue
  85. 84 0
      frontend/vue-ts/src/views/login.vue
  86. 73 0
      frontend/vue-ts/src/views/register.vue
  87. 15 0
      frontend/vue-ts/src/views/user.vue
  88. 26 0
      frontend/vue-ts/src/views/welcome.vue
  89. 45 0
      frontend/vue-ts/tsconfig.json
  90. 45 0
      frontend/vue-ts/vite.config.ts

+ 1 - 1
README.md

@@ -1,4 +1,4 @@
-# CURD-TS 正在开发中……
+# Vue3.0版本已开发完成!
 一套基于TS的增删改查系统,前端语言Vue3.0、React、Angular,后端语言node+express,采用了三种数据库MySQL、MongoDB、SQLite编写。  
 A TS based add, delete, modify and query system, the front-end language vue3.0, react, angular, back-end language node + Express, using three kinds of database mysql, mongodb, SQLite  
 

+ 5 - 0
frontend/vue-ts/.env

@@ -0,0 +1,5 @@
+# public path
+VITE_PUBLIC_PATH = /
+
+# Cross-domain proxy, you can configure multiple
+VITE_PROXY = [ ["/api", "http://127.0.0.1:3000" ] ]

+ 11 - 0
frontend/vue-ts/.env.development

@@ -0,0 +1,11 @@
+# port
+VITE_PORT = 3001
+
+# open
+VITE_OPEN = false
+
+# public path
+VITE_PUBLIC_PATH = /
+
+# Cross-domain proxy, you can configure multiple
+VITE_PROXY = [ ["/api", "http://127.0.0.1:3000" ] ]

+ 2 - 0
frontend/vue-ts/.env.production

@@ -0,0 +1,2 @@
+# public path
+VITE_PUBLIC_PATH = /manages/

+ 4 - 0
frontend/vue-ts/.gitignore

@@ -0,0 +1,4 @@
+/node_modules
+/dist
+.DS_Store
+src/.DS_Store

+ 55 - 1
frontend/vue-ts/README.md

@@ -1 +1,55 @@
-# vue-ts
+# Vue3.0后台管理系统
+
+## 知识库地址
+
+帮助你获取最新的 API  
+[vue3.0 中文文档地址]: https://vue3js.cn/docs/zh/  
+[element-plus 中文文档地址]: https://element-plus.org/#/zh-CN  
+[composition-Api 中文文档地址]: https://composition-api.vuejs.org/zh/  
+[vue-router-next 文档地址]: https://next.router.vuejs.org/  
+[next.vuex 文档地址]: https://next.vuex.vuejs.org/  
+[vite 源码]: https://github.com/vitejs/vite  
+[vite 文档地址]: https://vitejs.dev/  
+[vite 中文文档地址(非官方版本)]: https://vite-design.surge.sh/guide/chinese-doc.html  
+[vue-i18n-next]: https://vue-i18n-next.intlify.dev/  
+[composition-api-vue-i18n-next]: https://vue-i18n-next.intlify.dev/advanced/composition.html#local-scope  
+
+## 安装依赖
+
+```
+npm install
+```
+
+## 项目运行
+
+```
+npm run serve
+```
+
+## 项目打包
+
+```
+npm run build
+```
+
+## 注意点
+
+请先全局安装 typescript、ts-node、vite 如安装请忽略
+
+```
+npm install -g typescript
+npm install -g ts-node
+npm install -g create-vite-app
+```
+
+坑位  
+1.  
+path模块线上部署会遇到process is undefined问题  
+解决办法:在源码中开头加入window.process = {}  
+issues:https://github.com/jinder/path/issues/7  
+2.  
+运行项目时控制台报NODE_ENV not found  
+解决办法:删除node_modules和package-lock.json文件,重新npm install  
+3.  
+运行项目会感觉菜单切换比较卡,这个原因是使用route造成的,watch(route)是隐式的{ deep: true },最好使用watchEffect  
+issues:https://github.com/vuejs/vue-next/issues/2027  

+ 6 - 0
frontend/vue-ts/babel.config.js

@@ -0,0 +1,6 @@
+
+const productPlugins = []
+process.env.NODE_ENV === "production" && productPlugins.push("transform-remove-console")
+module.exports = {
+  plugins: [...productPlugins],
+}

+ 19 - 0
frontend/vue-ts/build/proxy.ts

@@ -0,0 +1,19 @@
+type ProxyItem = [string, string];
+
+type ProxyList = ProxyItem[];
+
+const regExps = (value: string,reg: string): string => {
+  return value.replace(new RegExp(reg, 'g'), '');
+}
+
+export function createProxy(list: ProxyList = []) {
+  const ret: any = {};
+  for (const [prefix, target] of list) {
+    ret[prefix] = {
+      target: target,
+      changeOrigin: true,
+      rewrite: (path:string) => regExps(path, prefix)
+    };
+  }
+  return ret;
+}

+ 38 - 0
frontend/vue-ts/build/utils.ts

@@ -0,0 +1,38 @@
+import dotenv from 'dotenv';
+
+export interface ViteEnv {
+  VITE_PORT: number;
+  VITE_OPEN: boolean;
+  VITE_USE_MOCK: boolean;
+  VITE_PUBLIC_PATH: string;
+  VITE_PROXY: [string, string][];
+}
+
+export function loadEnv(): ViteEnv {
+  const env = process.env.NODE_ENV;
+  const ret: any = {};
+  const envList = [`.env.${env}.local`, `.env.${env}`, '.env.local', '.env', ,]
+  envList.forEach((e) => {
+    dotenv.config({
+      path: e,
+    });
+  });
+  for (const envName of Object.keys(process.env)) {
+    let realName = (process.env as any)[envName].replace(/\\n/g, '\n');
+    realName = realName === 'true' ? true : realName === 'false' ? false : realName;
+    if (envName === 'VITE_PORT') {
+      realName = Number(realName);
+    }
+    if (envName === 'VITE_OPEN') {
+      realName = Boolean(realName);
+    }
+    if (envName === 'VITE_PROXY') {
+      try {
+        realName = JSON.parse(realName);
+      } catch (error) { }
+    }
+    ret[envName] = realName;
+    process.env[envName] = realName;
+  }
+  return ret;
+}

+ 258 - 0
frontend/vue-ts/index.html

@@ -0,0 +1,258 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8">
+  <link rel="icon" href="/favicon.ico" />
+  <link rel="stylesheet" href="/iconfont.css" />
+  <link rel="stylesheet" href="/animate.css">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>后台管理系统</title>
+</head>
+<body>
+  <div id="app">
+     <style>
+        html,
+        body,
+        #app {
+          height: 100%;
+          margin: 0px;
+          padding: 0px;
+        }
+
+        .chromeframe {
+          margin: 0.2em 0;
+          background: #ccc;
+          color: #000;
+          padding: 0.2em 0;
+        }
+
+        #loader-wrapper {
+          position: fixed;
+          top: 0;
+          left: 0;
+          width: 100%;
+          height: 100%;
+          z-index: 999999;
+        }
+
+        #loader {
+          display: block;
+          position: relative;
+          left: 50%;
+          top: 50%;
+          width: 150px;
+          height: 150px;
+          margin: -75px 0 0 -75px;
+          border-radius: 50%;
+          border: 3px solid transparent;
+          /* COLOR 1 */
+          border-top-color: #FFF;
+          -webkit-animation: spin 2s linear infinite;
+          /* Chrome, Opera 15+, Safari 5+ */
+          -ms-animation: spin 2s linear infinite;
+          /* Chrome, Opera 15+, Safari 5+ */
+          -moz-animation: spin 2s linear infinite;
+          /* Chrome, Opera 15+, Safari 5+ */
+          -o-animation: spin 2s linear infinite;
+          /* Chrome, Opera 15+, Safari 5+ */
+          animation: spin 2s linear infinite;
+          /* Chrome, Firefox 16+, IE 10+, Opera */
+          z-index: 1001;
+        }
+
+        #loader:before {
+          content: "";
+          position: absolute;
+          top: 5px;
+          left: 5px;
+          right: 5px;
+          bottom: 5px;
+          border-radius: 50%;
+          border: 3px solid transparent;
+          /* COLOR 2 */
+          border-top-color: #FFF;
+          -webkit-animation: spin 3s linear infinite;
+          /* Chrome, Opera 15+, Safari 5+ */
+          -moz-animation: spin 3s linear infinite;
+          /* Chrome, Opera 15+, Safari 5+ */
+          -o-animation: spin 3s linear infinite;
+          /* Chrome, Opera 15+, Safari 5+ */
+          -ms-animation: spin 3s linear infinite;
+          /* Chrome, Opera 15+, Safari 5+ */
+          animation: spin 3s linear infinite;
+          /* Chrome, Firefox 16+, IE 10+, Opera */
+        }
+
+        #loader:after {
+          content: "";
+          position: absolute;
+          top: 15px;
+          left: 15px;
+          right: 15px;
+          bottom: 15px;
+          border-radius: 50%;
+          border: 3px solid transparent;
+          border-top-color: #FFF;
+          /* COLOR 3 */
+          -moz-animation: spin 1.5s linear infinite;
+          /* Chrome, Opera 15+, Safari 5+ */
+          -o-animation: spin 1.5s linear infinite;
+          /* Chrome, Opera 15+, Safari 5+ */
+          -ms-animation: spin 1.5s linear infinite;
+          /* Chrome, Opera 15+, Safari 5+ */
+          -webkit-animation: spin 1.5s linear infinite;
+          /* Chrome, Opera 15+, Safari 5+ */
+          animation: spin 1.5s linear infinite;
+          /* Chrome, Firefox 16+, IE 10+, Opera */
+        }
+
+        @-webkit-keyframes spin {
+          0% {
+            -webkit-transform: rotate(0deg);
+            /* Chrome, Opera 15+, Safari 3.1+ */
+            -ms-transform: rotate(0deg);
+            /* IE 9 */
+            transform: rotate(0deg);
+            /* Firefox 16+, IE 10+, Opera */
+          }
+
+          100% {
+            -webkit-transform: rotate(360deg);
+            /* Chrome, Opera 15+, Safari 3.1+ */
+            -ms-transform: rotate(360deg);
+            /* IE 9 */
+            transform: rotate(360deg);
+            /* Firefox 16+, IE 10+, Opera */
+          }
+        }
+
+        @keyframes spin {
+          0% {
+            -webkit-transform: rotate(0deg);
+            /* Chrome, Opera 15+, Safari 3.1+ */
+            -ms-transform: rotate(0deg);
+            /* IE 9 */
+            transform: rotate(0deg);
+            /* Firefox 16+, IE 10+, Opera */
+          }
+
+          100% {
+            -webkit-transform: rotate(360deg);
+            /* Chrome, Opera 15+, Safari 3.1+ */
+            -ms-transform: rotate(360deg);
+            /* IE 9 */
+            transform: rotate(360deg);
+            /* Firefox 16+, IE 10+, Opera */
+          }
+        }
+
+        #loader-wrapper .loader-section {
+          position: fixed;
+          top: 0;
+          width: 51%;
+          height: 100%;
+          background: #5d94f8;
+          /* Old browsers */
+          z-index: 1000;
+          -webkit-transform: translateX(0);
+          /* Chrome, Opera 15+, Safari 3.1+ */
+          -ms-transform: translateX(0);
+          /* IE 9 */
+          transform: translateX(0);
+          /* Firefox 16+, IE 10+, Opera */
+        }
+
+        #loader-wrapper .loader-section.section-left {
+          left: 0;
+        }
+
+        #loader-wrapper .loader-section.section-right {
+          right: 0;
+        }
+
+        /* Loaded */
+        .loaded #loader-wrapper .loader-section.section-left {
+          -webkit-transform: translateX(-100%);
+          /* Chrome, Opera 15+, Safari 3.1+ */
+          -ms-transform: translateX(-100%);
+          /* IE 9 */
+          transform: translateX(-100%);
+          /* Firefox 16+, IE 10+, Opera */
+          -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+          transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+        }
+
+        .loaded #loader-wrapper .loader-section.section-right {
+          -webkit-transform: translateX(100%);
+          /* Chrome, Opera 15+, Safari 3.1+ */
+          -ms-transform: translateX(100%);
+          /* IE 9 */
+          transform: translateX(100%);
+          /* Firefox 16+, IE 10+, Opera */
+          -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+          transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+        }
+
+        .loaded #loader {
+          opacity: 0;
+          -webkit-transition: all 0.3s ease-out;
+          transition: all 0.3s ease-out;
+        }
+
+        .loaded #loader-wrapper {
+          visibility: hidden;
+          -webkit-transform: translateY(-100%);
+          /* Chrome, Opera 15+, Safari 3.1+ */
+          -ms-transform: translateY(-100%);
+          /* IE 9 */
+          transform: translateY(-100%);
+          /* Firefox 16+, IE 10+, Opera */
+          -webkit-transition: all 0.3s 1s ease-out;
+          transition: all 0.3s 1s ease-out;
+        }
+
+        /* JavaScript Turned Off */
+        .no-js #loader-wrapper {
+          display: none;
+        }
+
+        .no-js h1 {
+          color: #222222;
+        }
+
+        #loader-wrapper .load_title {
+          font-family: 'Open Sans';
+          color: #FFF;
+          font-size: 19px;
+          width: 100%;
+          text-align: center;
+          z-index: 9999999999999;
+          position: absolute;
+          top: 60%;
+          opacity: 1;
+          line-height: 30px;
+        }
+
+        #loader-wrapper .load_title span {
+          font-weight: normal;
+          font-style: italic;
+          font-size: 13px;
+          color: #FFF;
+          opacity: 0.5;
+        }
+    </style>
+    <div id="loader-wrapper">
+      <div id="loader"></div>
+      <div class="loader-section section-left"></div>
+      <div class="loader-section section-right"></div>
+      <div class="load_title">加载中,请耐心等待...
+        <br>
+        <span id="version">V0.0.1</span>
+      </div>
+    </div>
+  </div>
+  <script type="module" src="/src/main.ts"></script>
+</body>
+
+</html>

+ 1142 - 0
frontend/vue-ts/package-lock.json

@@ -0,0 +1,1142 @@
+{
+  "name": "vue-ts",
+  "version": "0.1.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@babel/helper-validator-identifier": {
+      "version": "7.12.11",
+      "resolved": "http://192.168.250.101:4873/@babel%2fhelper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
+      "integrity": "sha1-yaHwIZF9y1zPDU5FPjmQIpgfye0="
+    },
+    "@babel/parser": {
+      "version": "7.12.11",
+      "resolved": "http://192.168.250.101:4873/@babel%2fparser/-/parser-7.12.11.tgz",
+      "integrity": "sha1-nONZW810vFxGaQXobFNbiyUBHnk="
+    },
+    "@babel/types": {
+      "version": "7.12.11",
+      "resolved": "http://192.168.250.101:4873/@babel%2ftypes/-/types-7.12.11.tgz",
+      "integrity": "sha1-qG5NceMKm27hAlkERsmGYliSg84=",
+      "requires": {
+        "@babel/helper-validator-identifier": "^7.12.11",
+        "lodash": "^4.17.19",
+        "to-fast-properties": "^2.0.0"
+      }
+    },
+    "@intlify/core-base": {
+      "version": "9.0.0-rc.4",
+      "resolved": "http://192.168.250.101:4873/@intlify%2fcore-base/-/core-base-9.0.0-rc.4.tgz",
+      "integrity": "sha1-oPTKtPptRJIovLOQ3cTvB2ySElo=",
+      "requires": {
+        "@intlify/message-compiler": "9.0.0-rc.4",
+        "@intlify/message-resolver": "9.0.0-rc.4",
+        "@intlify/runtime": "9.0.0-rc.4",
+        "@intlify/shared": "9.0.0-rc.4"
+      }
+    },
+    "@intlify/message-compiler": {
+      "version": "9.0.0-rc.4",
+      "resolved": "http://192.168.250.101:4873/@intlify%2fmessage-compiler/-/message-compiler-9.0.0-rc.4.tgz",
+      "integrity": "sha1-DyHIzKjuf40OLj9kYMM3MdQbke4=",
+      "requires": {
+        "@intlify/message-resolver": "9.0.0-rc.4",
+        "@intlify/shared": "9.0.0-rc.4",
+        "source-map": "0.6.1"
+      }
+    },
+    "@intlify/message-resolver": {
+      "version": "9.0.0-rc.4",
+      "resolved": "http://192.168.250.101:4873/@intlify%2fmessage-resolver/-/message-resolver-9.0.0-rc.4.tgz",
+      "integrity": "sha1-AUHqu8iMlO1MTIocOgd36m4F4CY="
+    },
+    "@intlify/runtime": {
+      "version": "9.0.0-rc.4",
+      "resolved": "http://192.168.250.101:4873/@intlify%2fruntime/-/runtime-9.0.0-rc.4.tgz",
+      "integrity": "sha1-u1TAfDiLrGO1f1lelRmF4mmB5qc=",
+      "requires": {
+        "@intlify/message-compiler": "9.0.0-rc.4",
+        "@intlify/message-resolver": "9.0.0-rc.4",
+        "@intlify/shared": "9.0.0-rc.4"
+      }
+    },
+    "@intlify/shared": {
+      "version": "9.0.0-rc.4",
+      "resolved": "http://192.168.250.101:4873/@intlify%2fshared/-/shared-9.0.0-rc.4.tgz",
+      "integrity": "sha1-IrJdNkzDauijzlYr5Dgpg9O7tME="
+    },
+    "@popperjs/core": {
+      "version": "2.6.0",
+      "resolved": "http://192.168.250.101:4873/@popperjs%2fcore/-/core-2.6.0.tgz",
+      "integrity": "sha1-8CIZWv38lC4IjuIQEoWh0xx9cn8="
+    },
+    "@types/json-schema": {
+      "version": "7.0.6",
+      "resolved": "http://192.168.250.101:4873/@types%2fjson-schema/-/json-schema-7.0.6.tgz",
+      "integrity": "sha1-9MfsQ+gbMZqYFRFQMXCfJph4kfA=",
+      "dev": true
+    },
+    "@types/node": {
+      "version": "14.14.14",
+      "resolved": "http://192.168.250.101:4873/@types%2fnode/-/node-14.14.14.tgz",
+      "integrity": "sha1-9/1fPMhSEwERn2ORDw+5ZcfXYa4=",
+      "dev": true
+    },
+    "@types/nprogress": {
+      "version": "0.2.0",
+      "resolved": "http://192.168.250.101:4873/@types%2fnprogress/-/nprogress-0.2.0.tgz",
+      "integrity": "sha1-hsWTaC1BmSEqBQnMPE1WK7vW5F8="
+    },
+    "@vitejs/plugin-vue": {
+      "version": "1.1.2",
+      "resolved": "http://192.168.250.101:4873/@vitejs%2fplugin-vue/-/plugin-vue-1.1.2.tgz",
+      "integrity": "sha1-ZNHw4HOWdfVxcBX/tNhhxTr4/mA=",
+      "dev": true
+    },
+    "@vue/compiler-core": {
+      "version": "3.0.4",
+      "resolved": "http://192.168.250.101:4873/@vue%2fcompiler-core/-/compiler-core-3.0.4.tgz",
+      "integrity": "sha1-ASKspuraTLKLOe2TCvkXREdV4zA=",
+      "requires": {
+        "@babel/parser": "^7.12.0",
+        "@babel/types": "^7.12.0",
+        "@vue/shared": "3.0.4",
+        "estree-walker": "^2.0.1",
+        "source-map": "^0.6.1"
+      }
+    },
+    "@vue/compiler-dom": {
+      "version": "3.0.4",
+      "resolved": "http://192.168.250.101:4873/@vue%2fcompiler-dom/-/compiler-dom-3.0.4.tgz",
+      "integrity": "sha1-g0/UsVxWmM+fRQXCv7zMoFioQ+s=",
+      "requires": {
+        "@vue/compiler-core": "3.0.4",
+        "@vue/shared": "3.0.4"
+      }
+    },
+    "@vue/compiler-sfc": {
+      "version": "3.0.4",
+      "resolved": "http://192.168.250.101:4873/@vue%2fcompiler-sfc/-/compiler-sfc-3.0.4.tgz",
+      "integrity": "sha1-IRn+HmjSwmiq+iBGHILBOamt+OA=",
+      "dev": true,
+      "requires": {
+        "@babel/parser": "^7.12.0",
+        "@babel/types": "^7.12.0",
+        "@vue/compiler-core": "3.0.4",
+        "@vue/compiler-dom": "3.0.4",
+        "@vue/compiler-ssr": "3.0.4",
+        "@vue/shared": "3.0.4",
+        "consolidate": "^0.16.0",
+        "estree-walker": "^2.0.1",
+        "hash-sum": "^2.0.0",
+        "lru-cache": "^5.1.1",
+        "magic-string": "^0.25.7",
+        "merge-source-map": "^1.1.0",
+        "postcss": "^7.0.32",
+        "postcss-modules": "^3.2.2",
+        "postcss-selector-parser": "^6.0.4",
+        "source-map": "^0.6.1"
+      }
+    },
+    "@vue/compiler-ssr": {
+      "version": "3.0.4",
+      "resolved": "http://192.168.250.101:4873/@vue%2fcompiler-ssr/-/compiler-ssr-3.0.4.tgz",
+      "integrity": "sha1-zL0fVXNNUdFAL62CWsECACp6B8c=",
+      "dev": true,
+      "requires": {
+        "@vue/compiler-dom": "3.0.4",
+        "@vue/shared": "3.0.4"
+      }
+    },
+    "@vue/devtools-api": {
+      "version": "6.0.0-beta.3",
+      "resolved": "http://192.168.250.101:4873/@vue%2fdevtools-api/-/devtools-api-6.0.0-beta.3.tgz",
+      "integrity": "sha1-WmbMi+7WiP4YwnLueovY7X41pUw="
+    },
+    "@vue/reactivity": {
+      "version": "3.0.4",
+      "resolved": "http://192.168.250.101:4873/@vue%2freactivity/-/reactivity-3.0.4.tgz",
+      "integrity": "sha1-tlmd2CcadFlgoD8FdEzPeZG6XY0=",
+      "requires": {
+        "@vue/shared": "3.0.4"
+      }
+    },
+    "@vue/runtime-core": {
+      "version": "3.0.4",
+      "resolved": "http://192.168.250.101:4873/@vue%2fruntime-core/-/runtime-core-3.0.4.tgz",
+      "integrity": "sha1-pbmgAVYLH9jAGkP2i3ZMVV3ng2w=",
+      "requires": {
+        "@vue/reactivity": "3.0.4",
+        "@vue/shared": "3.0.4"
+      }
+    },
+    "@vue/runtime-dom": {
+      "version": "3.0.4",
+      "resolved": "http://192.168.250.101:4873/@vue%2fruntime-dom/-/runtime-dom-3.0.4.tgz",
+      "integrity": "sha1-b4GuxUXyRRHSwooxWqM5FCC2nGg=",
+      "requires": {
+        "@vue/runtime-core": "3.0.4",
+        "@vue/shared": "3.0.4",
+        "csstype": "^2.6.8"
+      }
+    },
+    "@vue/shared": {
+      "version": "3.0.4",
+      "resolved": "http://192.168.250.101:4873/@vue%2fshared/-/shared-3.0.4.tgz",
+      "integrity": "sha1-bcUPWTvf3qphg9HbwV4tRefGuLM="
+    },
+    "ajv": {
+      "version": "6.12.6",
+      "resolved": "http://192.168.250.101:4873/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha1-uvWmLoArB9l3A0WG+MO69a3ybfQ=",
+      "dev": true,
+      "requires": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "ajv-keywords": {
+      "version": "3.5.2",
+      "resolved": "http://192.168.250.101:4873/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+      "integrity": "sha1-MfKdpatuANHC0yms97WSlhTVAU0=",
+      "dev": true
+    },
+    "ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "http://192.168.250.101:4873/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=",
+      "dev": true,
+      "requires": {
+        "color-convert": "^1.9.0"
+      }
+    },
+    "anymatch": {
+      "version": "3.1.1",
+      "resolved": "http://192.168.250.101:4873/anymatch/-/anymatch-3.1.1.tgz",
+      "integrity": "sha1-xV7PAhheJGklk5kxDBc84xIzsUI=",
+      "dev": true,
+      "requires": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      }
+    },
+    "async-validator": {
+      "version": "3.5.1",
+      "resolved": "http://192.168.250.101:4873/async-validator/-/async-validator-3.5.1.tgz",
+      "integrity": "sha1-zWK5aIskZfSEIOJ620d2CrG1VZ8="
+    },
+    "autoprefixer": {
+      "version": "9.8.6",
+      "resolved": "http://192.168.250.101:4873/autoprefixer/-/autoprefixer-9.8.6.tgz",
+      "integrity": "sha1-O3NZTKG/kmYyDFrPFYjXTep0IQ8=",
+      "dev": true,
+      "requires": {
+        "browserslist": "^4.12.0",
+        "caniuse-lite": "^1.0.30001109",
+        "colorette": "^1.2.1",
+        "normalize-range": "^0.1.2",
+        "num2fraction": "^1.2.2",
+        "postcss": "^7.0.32",
+        "postcss-value-parser": "^4.1.0"
+      }
+    },
+    "await-to-js": {
+      "version": "2.1.1",
+      "resolved": "http://192.168.250.101:4873/await-to-js/-/await-to-js-2.1.1.tgz",
+      "integrity": "sha1-wgk81aOG8ruUXXmykoF7vD9Bsxs="
+    },
+    "axios": {
+      "version": "0.21.0",
+      "resolved": "http://192.168.250.101:4873/axios/-/axios-0.21.0.tgz",
+      "integrity": "sha1-Jt8IiAOiNQ3/LCf5b++Z/klEKso=",
+      "requires": {
+        "follow-redirects": "^1.10.0"
+      }
+    },
+    "babel-plugin-transform-remove-console": {
+      "version": "6.9.4",
+      "resolved": "http://192.168.250.101:4873/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz",
+      "integrity": "sha1-uYA2DAZzhOJLNXpYjYB9PINSd4A=",
+      "dev": true
+    },
+    "big.js": {
+      "version": "5.2.2",
+      "resolved": "http://192.168.250.101:4873/big.js/-/big.js-5.2.2.tgz",
+      "integrity": "sha1-ZfCvOC9Xi83HQr2cKB6cstd2gyg=",
+      "dev": true
+    },
+    "binary-extensions": {
+      "version": "2.1.0",
+      "resolved": "http://192.168.250.101:4873/binary-extensions/-/binary-extensions-2.1.0.tgz",
+      "integrity": "sha1-MPpAyef+B9vIlWeM0ocCTeokHdk=",
+      "dev": true
+    },
+    "bluebird": {
+      "version": "3.7.2",
+      "resolved": "http://192.168.250.101:4873/bluebird/-/bluebird-3.7.2.tgz",
+      "integrity": "sha1-nyKcFb4nJFT/qXOs4NvueaGww28=",
+      "dev": true
+    },
+    "braces": {
+      "version": "3.0.2",
+      "resolved": "http://192.168.250.101:4873/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha1-NFThpGLujVmeI23zNs2epPiv4Qc=",
+      "dev": true,
+      "requires": {
+        "fill-range": "^7.0.1"
+      }
+    },
+    "browserslist": {
+      "version": "4.16.0",
+      "resolved": "http://192.168.250.101:4873/browserslist/-/browserslist-4.16.0.tgz",
+      "integrity": "sha1-QQJ3YnUAvjyyihv+A3WG++35SIs=",
+      "dev": true,
+      "requires": {
+        "caniuse-lite": "^1.0.30001165",
+        "colorette": "^1.2.1",
+        "electron-to-chromium": "^1.3.621",
+        "escalade": "^3.1.1",
+        "node-releases": "^1.1.67"
+      }
+    },
+    "caniuse-lite": {
+      "version": "1.0.30001168",
+      "resolved": "http://192.168.250.101:4873/caniuse-lite/-/caniuse-lite-1.0.30001168.tgz",
+      "integrity": "sha1-b80JjBOdADub1ITLucomy4mQf5o=",
+      "dev": true
+    },
+    "chalk": {
+      "version": "2.4.2",
+      "resolved": "http://192.168.250.101:4873/chalk/-/chalk-2.4.2.tgz",
+      "integrity": "sha1-zUJUFnelQzPPVBpJEIwUMrRMlCQ=",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      },
+      "dependencies": {
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "http://192.168.250.101:4873/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "chokidar": {
+      "version": "3.4.3",
+      "resolved": "http://192.168.250.101:4873/chokidar/-/chokidar-3.4.3.tgz",
+      "integrity": "sha1-wd84IxRI5FykrFiObHlXO6alfVs=",
+      "dev": true,
+      "requires": {
+        "anymatch": "~3.1.1",
+        "braces": "~3.0.2",
+        "fsevents": "~2.1.2",
+        "glob-parent": "~5.1.0",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.5.0"
+      }
+    },
+    "clone-deep": {
+      "version": "4.0.1",
+      "resolved": "http://192.168.250.101:4873/clone-deep/-/clone-deep-4.0.1.tgz",
+      "integrity": "sha1-wZ/Zvbv4WUK0/ZechNz31fB8I4c=",
+      "dev": true,
+      "requires": {
+        "is-plain-object": "^2.0.4",
+        "kind-of": "^6.0.2",
+        "shallow-clone": "^3.0.0"
+      }
+    },
+    "color-convert": {
+      "version": "1.9.3",
+      "resolved": "http://192.168.250.101:4873/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha1-u3GFBpDh8TZWfeYp0tVHHe2kweg=",
+      "dev": true,
+      "requires": {
+        "color-name": "1.1.3"
+      }
+    },
+    "color-name": {
+      "version": "1.1.3",
+      "resolved": "http://192.168.250.101:4873/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+      "dev": true
+    },
+    "colorette": {
+      "version": "1.2.1",
+      "resolved": "http://192.168.250.101:4873/colorette/-/colorette-1.2.1.tgz",
+      "integrity": "sha1-TQuSEyXBT6+SYzCGpTbbbolWSxs=",
+      "dev": true
+    },
+    "consolidate": {
+      "version": "0.16.0",
+      "resolved": "http://192.168.250.101:4873/consolidate/-/consolidate-0.16.0.tgz",
+      "integrity": "sha1-oRhkdokw8vGUMWYKZZBmaPX73BY=",
+      "dev": true,
+      "requires": {
+        "bluebird": "^3.7.2"
+      }
+    },
+    "cssesc": {
+      "version": "3.0.0",
+      "resolved": "http://192.168.250.101:4873/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha1-N3QZGZA7hoVl4cCep0dEXNGJg+4=",
+      "dev": true
+    },
+    "csstype": {
+      "version": "2.6.14",
+      "resolved": "http://192.168.250.101:4873/csstype/-/csstype-2.6.14.tgz",
+      "integrity": "sha1-AEgipAUDRbVa1NzAC+HZzy9Clt4="
+    },
+    "dayjs": {
+      "version": "1.10.4",
+      "resolved": "http://192.168.250.101:4873/dayjs/-/dayjs-1.10.4.tgz",
+      "integrity": "sha1-jlRKm4aD9heD9XCYCoqA6vVKseI="
+    },
+    "dotenv": {
+      "version": "8.2.0",
+      "resolved": "http://192.168.250.101:4873/dotenv/-/dotenv-8.2.0.tgz",
+      "integrity": "sha1-l+YZJZradQ7qPk6j4mvO6lQksWo="
+    },
+    "electron-to-chromium": {
+      "version": "1.3.629",
+      "resolved": "http://192.168.250.101:4873/electron-to-chromium/-/electron-to-chromium-1.3.629.tgz",
+      "integrity": "sha1-oI0Ttk2Q48d+xbm/+j77xbSgCWk=",
+      "dev": true
+    },
+    "element-plus": {
+      "version": "1.0.2-beta.30",
+      "resolved": "http://192.168.250.101:4873/element-plus/-/element-plus-1.0.2-beta.30.tgz",
+      "integrity": "sha1-mxaIK5cWnfC4MsizXEbPRLJW280=",
+      "requires": {
+        "@popperjs/core": "^2.4.4",
+        "async-validator": "^3.4.0",
+        "dayjs": "1.x",
+        "lodash": "^4.17.20",
+        "mitt": "^2.1.0",
+        "normalize-wheel": "^1.0.1",
+        "resize-observer-polyfill": "^1.5.1"
+      }
+    },
+    "emojis-list": {
+      "version": "3.0.0",
+      "resolved": "http://192.168.250.101:4873/emojis-list/-/emojis-list-3.0.0.tgz",
+      "integrity": "sha1-VXBmIEatKeLpFucariYKvf9Pang=",
+      "dev": true
+    },
+    "esbuild": {
+      "version": "0.8.36",
+      "resolved": "http://192.168.250.101:4873/esbuild/-/esbuild-0.8.36.tgz",
+      "integrity": "sha1-9bfGFHNyHH1T/+fIhwjiXzthgvM=",
+      "dev": true
+    },
+    "escalade": {
+      "version": "3.1.1",
+      "resolved": "http://192.168.250.101:4873/escalade/-/escalade-3.1.1.tgz",
+      "integrity": "sha1-2M/ccACWXFoBdLSoLqpcBVJ0LkA=",
+      "dev": true
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "http://192.168.250.101:4873/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+      "dev": true
+    },
+    "estree-walker": {
+      "version": "2.0.2",
+      "resolved": "http://192.168.250.101:4873/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha1-UvAQF4wqTBF6d1fP6UKtt9LaTKw="
+    },
+    "fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "http://192.168.250.101:4873/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha1-On1WtVnWy8PrUSMlJE5hmmXGxSU=",
+      "dev": true
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "http://192.168.250.101:4873/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM=",
+      "dev": true
+    },
+    "fill-range": {
+      "version": "7.0.1",
+      "resolved": "http://192.168.250.101:4873/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha1-GRmmp8df44ssfHflGYU12prN2kA=",
+      "dev": true,
+      "requires": {
+        "to-regex-range": "^5.0.1"
+      }
+    },
+    "follow-redirects": {
+      "version": "1.13.1",
+      "resolved": "http://192.168.250.101:4873/follow-redirects/-/follow-redirects-1.13.1.tgz",
+      "integrity": "sha1-X2m4Ezds7k/QR0o6uoNd8Eq3Y7c="
+    },
+    "fsevents": {
+      "version": "2.1.3",
+      "resolved": "http://192.168.250.101:4873/fsevents/-/fsevents-2.1.3.tgz",
+      "integrity": "sha1-+3OHA66NL5/pAMM4Nt3r7ouX8j4=",
+      "dev": true,
+      "optional": true
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "http://192.168.250.101:4873/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=",
+      "dev": true
+    },
+    "generic-names": {
+      "version": "2.0.1",
+      "resolved": "http://192.168.250.101:4873/generic-names/-/generic-names-2.0.1.tgz",
+      "integrity": "sha1-+KN46tLMqno08DF7BVVIMq5BuHI=",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0"
+      }
+    },
+    "glob-parent": {
+      "version": "5.1.1",
+      "resolved": "http://192.168.250.101:4873/glob-parent/-/glob-parent-5.1.1.tgz",
+      "integrity": "sha1-tsHvQXxOVmPqSY8cRa+saRa7wik=",
+      "dev": true,
+      "requires": {
+        "is-glob": "^4.0.1"
+      }
+    },
+    "has": {
+      "version": "1.0.3",
+      "resolved": "http://192.168.250.101:4873/has/-/has-1.0.3.tgz",
+      "integrity": "sha1-ci18v8H2qoJB8W3YFOAR4fQeh5Y=",
+      "dev": true,
+      "requires": {
+        "function-bind": "^1.1.1"
+      }
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "http://192.168.250.101:4873/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+      "dev": true
+    },
+    "hash-sum": {
+      "version": "2.0.0",
+      "resolved": "http://192.168.250.101:4873/hash-sum/-/hash-sum-2.0.0.tgz",
+      "integrity": "sha1-gdAbtd6OpKIUrV1urRtSNGCwtFo=",
+      "dev": true
+    },
+    "icss-replace-symbols": {
+      "version": "1.1.0",
+      "resolved": "http://192.168.250.101:4873/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz",
+      "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=",
+      "dev": true
+    },
+    "icss-utils": {
+      "version": "4.1.1",
+      "resolved": "http://192.168.250.101:4873/icss-utils/-/icss-utils-4.1.1.tgz",
+      "integrity": "sha1-IRcLU3ie4nRHwvR91oMIFAP5pGc=",
+      "dev": true,
+      "requires": {
+        "postcss": "^7.0.14"
+      }
+    },
+    "indexes-of": {
+      "version": "1.0.1",
+      "resolved": "http://192.168.250.101:4873/indexes-of/-/indexes-of-1.0.1.tgz",
+      "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
+      "dev": true
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "http://192.168.250.101:4873/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+    },
+    "is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "http://192.168.250.101:4873/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha1-6h9/O4DwZCNug0cPhsCcJU+0Wwk=",
+      "dev": true,
+      "requires": {
+        "binary-extensions": "^2.0.0"
+      }
+    },
+    "is-core-module": {
+      "version": "2.2.0",
+      "resolved": "http://192.168.250.101:4873/is-core-module/-/is-core-module-2.2.0.tgz",
+      "integrity": "sha1-lwN+89UiJNhRY/VZeytj2a/tmBo=",
+      "dev": true,
+      "requires": {
+        "has": "^1.0.3"
+      }
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "http://192.168.250.101:4873/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "dev": true
+    },
+    "is-glob": {
+      "version": "4.0.1",
+      "resolved": "http://192.168.250.101:4873/is-glob/-/is-glob-4.0.1.tgz",
+      "integrity": "sha1-dWfb6fL14kZ7x3q4PEopSCQHpdw=",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-number": {
+      "version": "7.0.0",
+      "resolved": "http://192.168.250.101:4873/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha1-dTU0W4lnNNX4DE0GxQlVUnoU8Ss=",
+      "dev": true
+    },
+    "is-plain-object": {
+      "version": "2.0.4",
+      "resolved": "http://192.168.250.101:4873/is-plain-object/-/is-plain-object-2.0.4.tgz",
+      "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=",
+      "dev": true,
+      "requires": {
+        "isobject": "^3.0.1"
+      }
+    },
+    "isobject": {
+      "version": "3.0.1",
+      "resolved": "http://192.168.250.101:4873/isobject/-/isobject-3.0.1.tgz",
+      "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+      "dev": true
+    },
+    "json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "http://192.168.250.101:4873/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=",
+      "dev": true
+    },
+    "json5": {
+      "version": "1.0.1",
+      "resolved": "http://192.168.250.101:4873/json5/-/json5-1.0.1.tgz",
+      "integrity": "sha1-d5+wAYYE+oVOrL9iUhgNg1Q+Pb4=",
+      "dev": true,
+      "requires": {
+        "minimist": "^1.2.0"
+      }
+    },
+    "kind-of": {
+      "version": "6.0.3",
+      "resolved": "http://192.168.250.101:4873/kind-of/-/kind-of-6.0.3.tgz",
+      "integrity": "sha1-B8BQNKbDSfoG4k+jWqdttFgM5N0=",
+      "dev": true
+    },
+    "loader-utils": {
+      "version": "1.4.0",
+      "resolved": "http://192.168.250.101:4873/loader-utils/-/loader-utils-1.4.0.tgz",
+      "integrity": "sha1-xXm140yzSxp07cbB+za/o3HVphM=",
+      "dev": true,
+      "requires": {
+        "big.js": "^5.2.2",
+        "emojis-list": "^3.0.0",
+        "json5": "^1.0.1"
+      }
+    },
+    "lodash": {
+      "version": "4.17.20",
+      "resolved": "http://192.168.250.101:4873/lodash/-/lodash-4.17.20.tgz",
+      "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI="
+    },
+    "lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "http://192.168.250.101:4873/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=",
+      "dev": true
+    },
+    "lru-cache": {
+      "version": "5.1.1",
+      "resolved": "http://192.168.250.101:4873/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha1-HaJ+ZxAnGUdpXa9oSOhH8B2EuSA=",
+      "dev": true,
+      "requires": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "magic-string": {
+      "version": "0.25.7",
+      "resolved": "http://192.168.250.101:4873/magic-string/-/magic-string-0.25.7.tgz",
+      "integrity": "sha1-P0l9b9NMZpxnmNy4IfLvMfVEUFE=",
+      "dev": true,
+      "requires": {
+        "sourcemap-codec": "^1.4.4"
+      }
+    },
+    "merge-source-map": {
+      "version": "1.1.0",
+      "resolved": "http://192.168.250.101:4873/merge-source-map/-/merge-source-map-1.1.0.tgz",
+      "integrity": "sha1-L93n5gIJOfcJBqaPLXrmheTIxkY=",
+      "dev": true,
+      "requires": {
+        "source-map": "^0.6.1"
+      }
+    },
+    "minimist": {
+      "version": "1.2.5",
+      "resolved": "http://192.168.250.101:4873/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha1-Z9ZgFLZqaoqqDAg8X9WN9OTpdgI=",
+      "dev": true
+    },
+    "mitt": {
+      "version": "2.1.0",
+      "resolved": "http://192.168.250.101:4873/mitt/-/mitt-2.1.0.tgz",
+      "integrity": "sha1-90BXfCMXbGIFsSGylzUU6t4bIjA="
+    },
+    "nanoid": {
+      "version": "3.1.20",
+      "resolved": "http://192.168.250.101:4873/nanoid/-/nanoid-3.1.20.tgz",
+      "integrity": "sha1-utwmPGsdzxS3HvqoX2q0wdbPx4g=",
+      "dev": true
+    },
+    "neo-async": {
+      "version": "2.6.2",
+      "resolved": "http://192.168.250.101:4873/neo-async/-/neo-async-2.6.2.tgz",
+      "integrity": "sha1-tKr7k+OustgXTKU88WOrfXMIMF8=",
+      "dev": true
+    },
+    "node-releases": {
+      "version": "1.1.67",
+      "resolved": "http://192.168.250.101:4873/node-releases/-/node-releases-1.1.67.tgz",
+      "integrity": "sha1-KOv8zNC6pqrY6NTY/ky8Sa4jnBI=",
+      "dev": true
+    },
+    "normalize-path": {
+      "version": "3.0.0",
+      "resolved": "http://192.168.250.101:4873/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha1-Dc1p/yOhybEf0JeDFmRKA4ghamU=",
+      "dev": true
+    },
+    "normalize-range": {
+      "version": "0.1.2",
+      "resolved": "http://192.168.250.101:4873/normalize-range/-/normalize-range-0.1.2.tgz",
+      "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
+      "dev": true
+    },
+    "normalize-wheel": {
+      "version": "1.0.1",
+      "resolved": "http://192.168.250.101:4873/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
+      "integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU="
+    },
+    "nprogress": {
+      "version": "0.2.0",
+      "resolved": "http://192.168.250.101:4873/nprogress/-/nprogress-0.2.0.tgz",
+      "integrity": "sha1-y480xTIT2JVyP8urkH6UIq28r7E="
+    },
+    "num2fraction": {
+      "version": "1.2.2",
+      "resolved": "http://192.168.250.101:4873/num2fraction/-/num2fraction-1.2.2.tgz",
+      "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=",
+      "dev": true
+    },
+    "path": {
+      "version": "0.12.7",
+      "resolved": "http://192.168.250.101:4873/path/-/path-0.12.7.tgz",
+      "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=",
+      "requires": {
+        "process": "^0.11.1",
+        "util": "^0.10.3"
+      }
+    },
+    "path-parse": {
+      "version": "1.0.6",
+      "resolved": "http://192.168.250.101:4873/path-parse/-/path-parse-1.0.6.tgz",
+      "integrity": "sha1-1i27VnlAXXLEc37FhgDp3c8G0kw=",
+      "dev": true
+    },
+    "path-to-regexp": {
+      "version": "6.2.0",
+      "resolved": "http://192.168.250.101:4873/path-to-regexp/-/path-to-regexp-6.2.0.tgz",
+      "integrity": "sha1-97OAMzYQTDRoia3s5hRmkjBkXzg="
+    },
+    "picomatch": {
+      "version": "2.2.2",
+      "resolved": "http://192.168.250.101:4873/picomatch/-/picomatch-2.2.2.tgz",
+      "integrity": "sha1-IfMz6ba46v8CRo9RRupAbTRfTa0=",
+      "dev": true
+    },
+    "pify": {
+      "version": "2.3.0",
+      "resolved": "http://192.168.250.101:4873/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+      "dev": true
+    },
+    "postcss": {
+      "version": "7.0.35",
+      "resolved": "http://192.168.250.101:4873/postcss/-/postcss-7.0.35.tgz",
+      "integrity": "sha1-0r4AuZj38hHYonaXQHny6SuXDiQ=",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.2",
+        "source-map": "^0.6.1",
+        "supports-color": "^6.1.0"
+      }
+    },
+    "postcss-import": {
+      "version": "12.0.1",
+      "resolved": "http://192.168.250.101:4873/postcss-import/-/postcss-import-12.0.1.tgz",
+      "integrity": "sha1-z4x6sLXMq1ZJAkU25WX4QZKLcVM=",
+      "dev": true,
+      "requires": {
+        "postcss": "^7.0.1",
+        "postcss-value-parser": "^3.2.3",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      },
+      "dependencies": {
+        "postcss-value-parser": {
+          "version": "3.3.1",
+          "resolved": "http://192.168.250.101:4873/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+          "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
+          "dev": true
+        }
+      }
+    },
+    "postcss-modules": {
+      "version": "3.2.2",
+      "resolved": "http://192.168.250.101:4873/postcss-modules/-/postcss-modules-3.2.2.tgz",
+      "integrity": "sha1-7jkN4PnxjnYeF3jfub4maFwCxR8=",
+      "dev": true,
+      "requires": {
+        "generic-names": "^2.0.1",
+        "icss-replace-symbols": "^1.1.0",
+        "lodash.camelcase": "^4.3.0",
+        "postcss": "^7.0.32",
+        "postcss-modules-extract-imports": "^2.0.0",
+        "postcss-modules-local-by-default": "^3.0.2",
+        "postcss-modules-scope": "^2.2.0",
+        "postcss-modules-values": "^3.0.0",
+        "string-hash": "^1.1.1"
+      }
+    },
+    "postcss-modules-extract-imports": {
+      "version": "2.0.0",
+      "resolved": "http://192.168.250.101:4873/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz",
+      "integrity": "sha1-gYcZoa4doyX5gyRGsBE27rSTzX4=",
+      "dev": true,
+      "requires": {
+        "postcss": "^7.0.5"
+      }
+    },
+    "postcss-modules-local-by-default": {
+      "version": "3.0.3",
+      "resolved": "http://192.168.250.101:4873/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz",
+      "integrity": "sha1-uxTgzHgnnVBNvcv9fgyiiZP/u7A=",
+      "dev": true,
+      "requires": {
+        "icss-utils": "^4.1.1",
+        "postcss": "^7.0.32",
+        "postcss-selector-parser": "^6.0.2",
+        "postcss-value-parser": "^4.1.0"
+      }
+    },
+    "postcss-modules-scope": {
+      "version": "2.2.0",
+      "resolved": "http://192.168.250.101:4873/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz",
+      "integrity": "sha1-OFyuATzHdD9afXYC0Qc6iequYu4=",
+      "dev": true,
+      "requires": {
+        "postcss": "^7.0.6",
+        "postcss-selector-parser": "^6.0.0"
+      }
+    },
+    "postcss-modules-values": {
+      "version": "3.0.0",
+      "resolved": "http://192.168.250.101:4873/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz",
+      "integrity": "sha1-W1AA1uuuKbQlUwG0o6VFdEI+fxA=",
+      "dev": true,
+      "requires": {
+        "icss-utils": "^4.0.0",
+        "postcss": "^7.0.6"
+      }
+    },
+    "postcss-selector-parser": {
+      "version": "6.0.4",
+      "resolved": "http://192.168.250.101:4873/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz",
+      "integrity": "sha1-VgdaE4CgRgTDiwY+p3Z6Epr1wrM=",
+      "dev": true,
+      "requires": {
+        "cssesc": "^3.0.0",
+        "indexes-of": "^1.0.1",
+        "uniq": "^1.0.1",
+        "util-deprecate": "^1.0.2"
+      }
+    },
+    "postcss-value-parser": {
+      "version": "4.1.0",
+      "resolved": "http://192.168.250.101:4873/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
+      "integrity": "sha1-RD9qIM7WSBor2k+oUypuVdeJoss=",
+      "dev": true
+    },
+    "process": {
+      "version": "0.11.10",
+      "resolved": "http://192.168.250.101:4873/process/-/process-0.11.10.tgz",
+      "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
+    },
+    "punycode": {
+      "version": "2.1.1",
+      "resolved": "http://192.168.250.101:4873/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew=",
+      "dev": true
+    },
+    "read-cache": {
+      "version": "1.0.0",
+      "resolved": "http://192.168.250.101:4873/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=",
+      "dev": true,
+      "requires": {
+        "pify": "^2.3.0"
+      }
+    },
+    "readdirp": {
+      "version": "3.5.0",
+      "resolved": "http://192.168.250.101:4873/readdirp/-/readdirp-3.5.0.tgz",
+      "integrity": "sha1-m6dMAZsV02UnjS6Ru4xI17TULJ4=",
+      "dev": true,
+      "requires": {
+        "picomatch": "^2.2.1"
+      }
+    },
+    "resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": "http://192.168.250.101:4873/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+      "integrity": "sha1-DpAg3T0hAkRY1OvSfiPkAmmBBGQ="
+    },
+    "resolve": {
+      "version": "1.19.0",
+      "resolved": "http://192.168.250.101:4873/resolve/-/resolve-1.19.0.tgz",
+      "integrity": "sha1-GvW/YwQJc0oGfK4pMYqsf6KaJnw=",
+      "dev": true,
+      "requires": {
+        "is-core-module": "^2.1.0",
+        "path-parse": "^1.0.6"
+      }
+    },
+    "rollup": {
+      "version": "2.38.1",
+      "resolved": "http://192.168.250.101:4873/rollup/-/rollup-2.38.1.tgz",
+      "integrity": "sha1-7OoPfObvLB8CP9t5Uk63rrZw6nk=",
+      "dev": true,
+      "requires": {
+        "fsevents": "~2.1.2"
+      }
+    },
+    "sass": {
+      "version": "1.30.0",
+      "resolved": "http://192.168.250.101:4873/sass/-/sass-1.30.0.tgz",
+      "integrity": "sha1-YLu7r3a6EBF+YcbCTwAWHD1gYQ4=",
+      "dev": true,
+      "requires": {
+        "chokidar": ">=2.0.0 <4.0.0"
+      }
+    },
+    "sass-loader": {
+      "version": "8.0.2",
+      "resolved": "http://192.168.250.101:4873/sass-loader/-/sass-loader-8.0.2.tgz",
+      "integrity": "sha1-3r7NjDziQ8dkVPLoKQSCFQOACQ0=",
+      "dev": true,
+      "requires": {
+        "clone-deep": "^4.0.1",
+        "loader-utils": "^1.2.3",
+        "neo-async": "^2.6.1",
+        "schema-utils": "^2.6.1",
+        "semver": "^6.3.0"
+      }
+    },
+    "schema-utils": {
+      "version": "2.7.1",
+      "resolved": "http://192.168.250.101:4873/schema-utils/-/schema-utils-2.7.1.tgz",
+      "integrity": "sha1-HKTzLRskxZDCA7jnpQvw6kzTlNc=",
+      "dev": true,
+      "requires": {
+        "@types/json-schema": "^7.0.5",
+        "ajv": "^6.12.4",
+        "ajv-keywords": "^3.5.2"
+      }
+    },
+    "screenfull": {
+      "version": "5.0.2",
+      "resolved": "http://192.168.250.101:4873/screenfull/-/screenfull-5.0.2.tgz",
+      "integrity": "sha1-uazc8exnapSGdN9c0P9muQKwvtc="
+    },
+    "semver": {
+      "version": "6.3.0",
+      "resolved": "http://192.168.250.101:4873/semver/-/semver-6.3.0.tgz",
+      "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=",
+      "dev": true
+    },
+    "shallow-clone": {
+      "version": "3.0.1",
+      "resolved": "http://192.168.250.101:4873/shallow-clone/-/shallow-clone-3.0.1.tgz",
+      "integrity": "sha1-jymBrZJTH1UDWwH7IwdppA4C76M=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^6.0.2"
+      }
+    },
+    "source-map": {
+      "version": "0.6.1",
+      "resolved": "http://192.168.250.101:4873/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM="
+    },
+    "sourcemap-codec": {
+      "version": "1.4.8",
+      "resolved": "http://192.168.250.101:4873/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+      "integrity": "sha1-6oBL2UhXQC5pktBaOO8a41qatMQ=",
+      "dev": true
+    },
+    "string-hash": {
+      "version": "1.1.3",
+      "resolved": "http://192.168.250.101:4873/string-hash/-/string-hash-1.1.3.tgz",
+      "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=",
+      "dev": true
+    },
+    "supports-color": {
+      "version": "6.1.0",
+      "resolved": "http://192.168.250.101:4873/supports-color/-/supports-color-6.1.0.tgz",
+      "integrity": "sha1-B2Srxpxj1ayELdSGfo0CXogN+PM=",
+      "dev": true,
+      "requires": {
+        "has-flag": "^3.0.0"
+      }
+    },
+    "to-fast-properties": {
+      "version": "2.0.0",
+      "resolved": "http://192.168.250.101:4873/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+      "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
+    },
+    "to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "http://192.168.250.101:4873/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha1-FkjESq58jZiKMmAY7XL1tN0DkuQ=",
+      "dev": true,
+      "requires": {
+        "is-number": "^7.0.0"
+      }
+    },
+    "typescript": {
+      "version": "4.1.3",
+      "resolved": "http://192.168.250.101:4873/typescript/-/typescript-4.1.3.tgz",
+      "integrity": "sha1-UZ1YK9lMugz4k0x9joRn5HP1O7c=",
+      "dev": true
+    },
+    "uniq": {
+      "version": "1.0.1",
+      "resolved": "http://192.168.250.101:4873/uniq/-/uniq-1.0.1.tgz",
+      "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
+      "dev": true
+    },
+    "uri-js": {
+      "version": "4.4.0",
+      "resolved": "http://192.168.250.101:4873/uri-js/-/uri-js-4.4.0.tgz",
+      "integrity": "sha1-qnFCYd55PoqCNHp7zJznTobyhgI=",
+      "dev": true,
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "util": {
+      "version": "0.10.4",
+      "resolved": "http://192.168.250.101:4873/util/-/util-0.10.4.tgz",
+      "integrity": "sha1-OqASW/5mikZy3liFfTrOJ+y3aQE=",
+      "requires": {
+        "inherits": "2.0.3"
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "http://192.168.250.101:4873/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+      "dev": true
+    },
+    "v-contextmenu": {
+      "version": "3.0.0-alpha.4",
+      "resolved": "http://192.168.250.101:4873/v-contextmenu/-/v-contextmenu-3.0.0-alpha.4.tgz",
+      "integrity": "sha1-9GWYrJivrF1duMZUDfwSMEztEUA="
+    },
+    "vite": {
+      "version": "2.0.0-beta.56",
+      "resolved": "http://192.168.250.101:4873/vite/-/vite-2.0.0-beta.56.tgz",
+      "integrity": "sha1-slTsRkeuE4PXxbbz/bAfiXG1vN0=",
+      "dev": true,
+      "requires": {
+        "esbuild": "^0.8.34",
+        "fsevents": "~2.1.2",
+        "postcss": "^8.2.1",
+        "resolve": "^1.19.0",
+        "rollup": "^2.35.1"
+      },
+      "dependencies": {
+        "postcss": {
+          "version": "8.2.4",
+          "resolved": "http://192.168.250.101:4873/postcss/-/postcss-8.2.4.tgz",
+          "integrity": "sha1-IKmKOc8wPRUSnChlqew37aADHQQ=",
+          "dev": true,
+          "requires": {
+            "colorette": "^1.2.1",
+            "nanoid": "^3.1.20",
+            "source-map": "^0.6.1"
+          }
+        }
+      }
+    },
+    "vue": {
+      "version": "3.0.4",
+      "resolved": "http://192.168.250.101:4873/vue/-/vue-3.0.4.tgz",
+      "integrity": "sha1-hyxlwUP1cXvVOHxhYT2fVfTMD0M=",
+      "requires": {
+        "@vue/compiler-dom": "3.0.4",
+        "@vue/runtime-dom": "3.0.4",
+        "@vue/shared": "3.0.4"
+      }
+    },
+    "vue-i18n": {
+      "version": "9.0.0-rc.4",
+      "resolved": "http://192.168.250.101:4873/vue-i18n/-/vue-i18n-9.0.0-rc.4.tgz",
+      "integrity": "sha1-cX2WBOYbyPWvdKAjxAiBoZT4ilg=",
+      "requires": {
+        "@intlify/core-base": "9.0.0-rc.4",
+        "@intlify/shared": "9.0.0-rc.4",
+        "@vue/devtools-api": "^6.0.0-beta.3"
+      }
+    },
+    "vue-router": {
+      "version": "4.0.3",
+      "resolved": "http://192.168.250.101:4873/vue-router/-/vue-router-4.0.3.tgz",
+      "integrity": "sha1-iyYFDIiy3sfieoiDX3EEazZYI+w="
+    },
+    "vuex": {
+      "version": "4.0.0-rc.2",
+      "resolved": "http://192.168.250.101:4873/vuex/-/vuex-4.0.0-rc.2.tgz",
+      "integrity": "sha1-NoHITrb1FxsDntqhfMeBBeIHJPM="
+    },
+    "vxe-table": {
+      "version": "4.0.0-beta.3",
+      "resolved": "http://192.168.250.101:4873/vxe-table/-/vxe-table-4.0.0-beta.3.tgz",
+      "integrity": "sha1-rz3SkD6vR2XuXemZCSg2k3v+cBY="
+    },
+    "xe-utils": {
+      "version": "3.0.4",
+      "resolved": "http://192.168.250.101:4873/xe-utils/-/xe-utils-3.0.4.tgz",
+      "integrity": "sha1-27RRgLB/6rZVHM+Od0/xVQAj53M="
+    },
+    "yallist": {
+      "version": "3.1.1",
+      "resolved": "http://192.168.250.101:4873/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha1-27fa+b/YusmrRev2ArjLrQ1dCP0=",
+      "dev": true
+    }
+  }
+}

+ 41 - 0
frontend/vue-ts/package.json

@@ -0,0 +1,41 @@
+{
+  "name": "vue-ts",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vite",
+    "build": "vite build"
+  },
+  "dependencies": {
+    "@types/nprogress": "^0.2.0",
+    "await-to-js": "^2.1.1",
+    "axios": "^0.21.0",
+    "dotenv": "^8.2.0",
+    "element-plus": "^1.0.2-beta.30",
+    "mitt": "^2.1.0",
+    "nprogress": "^0.2.0",
+    "path": "^0.12.7",
+    "path-to-regexp": "^6.2.0",
+    "resize-observer-polyfill": "^1.5.1",
+    "screenfull": "^5.0.2",
+    "v-contextmenu": "^3.0.0-alpha.4",
+    "vue": "^3.0.4",
+    "vue-i18n": "^9.0.0-rc.4",
+    "vue-router": "^4.0.3",
+    "vuex": "^4.0.0-rc.2",
+    "vxe-table": "^4.0.0-beta.3",
+    "xe-utils": "^3.0.4"
+  },
+  "devDependencies": {
+    "@types/node": "^14.14.14",
+    "@vitejs/plugin-vue": "^1.1.2",
+    "@vue/compiler-sfc": "^3.0.4",
+    "autoprefixer": "^9.8.6",
+    "babel-plugin-transform-remove-console": "^6.9.4",
+    "postcss-import": "^12.0.1",
+    "sass": "^1.26.5",
+    "sass-loader": "^8.0.2",
+    "typescript": "^4.1.3",
+    "vite": "^2.0.0-beta.56"
+  }
+}

+ 3 - 0
frontend/vue-ts/postcss.config.js

@@ -0,0 +1,3 @@
+module.exports = {
+  plugins: [require('autoprefixer'), require('postcss-import')],
+};

File diff suppressed because it is too large
+ 10 - 0
frontend/vue-ts/public/animate.css


BIN
frontend/vue-ts/public/favicon.ico


+ 18 - 0
frontend/vue-ts/public/iconfont.css

@@ -0,0 +1,18 @@
+@font-face {
+  font-family: "iconfont"; /* project id 1098500 */
+  src: url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.eot");
+  src: url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.eot?#iefix")
+      format("embedded-opentype"),
+    url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.woff2") format("woff2"),
+    url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.woff") format("woff"),
+    url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.ttf") format("truetype"),
+    url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.svg#iconfont") format("svg");
+}
+
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}

+ 3 - 0
frontend/vue-ts/src/App.vue

@@ -0,0 +1,3 @@
+<template>
+  <router-view />
+</template>

+ 16 - 0
frontend/vue-ts/src/api/user.ts

@@ -0,0 +1,16 @@
+import { http } from "../utils/http"
+
+// 获取验证码
+export const getVerify = (): any => {
+  return http.request("get", "/captcha")
+}
+
+// 登录
+export const getLogin = (data: object): any => {
+  return http.request("post", "/login", data)
+}
+
+// 注册
+export const getRegist = (data: object): any => {
+  return http.request("post", "/register", data)
+}

BIN
frontend/vue-ts/src/assets/401.gif


BIN
frontend/vue-ts/src/assets/404.png


BIN
frontend/vue-ts/src/assets/404_cloud.png


BIN
frontend/vue-ts/src/assets/bg.png


BIN
frontend/vue-ts/src/assets/ch.png


BIN
frontend/vue-ts/src/assets/en.png


+ 25 - 0
frontend/vue-ts/src/assets/iconfont/iconfont.css

@@ -0,0 +1,25 @@
+@font-face {font-family: "iconfont";
+  src: url('iconfont.eot?t=1607695324289'); /* IE9 */
+  src: url('iconfont.eot?t=1607695324289#iefix') format('embedded-opentype'), /* IE6-IE8 */
+  url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAOYAAsAAAAACDQAAANLAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCCfgqEFIMzATYCJAMMCwgABCAFhG0HRhsBB8gekiQBgDxM4A0kAIgioN8v3Ud3F2KVqgCpNrIc0GFVJBVVWVtFrBIhOx5JmMaWNar/v/v99nGtOz+KOqLjkL7t7ovvDWum0bJoImRoGiKExrYjC6g2lM6fV4PQ9vCs/c/l9Fp+RWXzA+Uyx9aoF2A0gQIaa2iUaIEk6CkMr13sMM8T0GwbFdns/NQS6pLRaQHhwtl8BnVldHIDMWgoaxVrs3BV0YiX4wdcCb4fftkeGkQloXMXToUp8n2d+DqZLlXbDngI/nSXQUHCFCQT1iptC1qj2JRWzVqszRC0rWGBr5NVxdcJj7fZPzyCSKihha1gDNJ4RXxH5tL4iIu49k3U0IwNvGUIvZ6i9FKZzM04f1vGCnHQizTFjAGXOnGWLmi8LgG9xC1WmK8lB4pOtBX0Kg4xYfE2ry5ey8Q6JbR6iyf7Mjpdw0qVKO8Uzh3pmbbum7XpTGXrUPUu9Y7qR9nckW8NQE7IjYWNjrm3/r2srSk+619wmEI3RohhGW4tZLwr/U/zycbfUpLCGCbLAW6wwYFl9c4IGxVz7/1lNW2pbcz0E4xYib5sbUw75r4GyvsNvC+bMxvPTkWYPDcepPTFWegO8LOcLO8GFpzGjRcnxHJeHkVhSpcJ8bkDkc+BJ8vxGUGffT10F1p7Vsv7gfWxseePRJdRGkL1OL1IM1D1JEWKPb/zN6bCpwOHB3f+brRk8Pn/w63AWz3Ouhs0btge+Hn8jjVZF1J9yirP4ZnJZFH76+GAIzpVqKvb33BFX+dCgoa+BKK6QUgahjCZMAVFi2l/TcPRCmk2KYrNLXpwrUJuwoQDAkGnV4jaPYak0w8mE36h6PcPNZ0hQbO10LNni5FwtnlmHTICKM5u4WKuXmO2UfSExQuQX6tkeFoYUAlwqRjEeo0un4xCDfgUC6T1vEEIhhmvV3GEXAaVSh03eL0EOaEpCNFwaLWs6kWaXL2KeszgQIYAKCxrC1aUU1fDvMZCT+HzC0DemooM3lBVYyUAJyn2julp6DpgorJap6pbGSxZl2cgCIydxHB1VVgEFlCxYnVYo3pUCcgRNAoDIg0OWqYe6yrTLK+ovt8uaEZ/l0IMKWQdJy8WhZqtVSpSjgPUemItQgA=') format('woff2'),
+  url('iconfont.woff?t=1607695324289') format('woff'),
+  url('iconfont.ttf?t=1607695324289') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
+  url('iconfont.svg?t=1607695324289#iconfont') format('svg'); /* iOS 4.1- */
+}
+
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.team-iconexit-fullscreen:before {
+  content: "\e62a";
+}
+
+.team-iconfullscreen:before {
+  content: "\e62b";
+}
+

BIN
frontend/vue-ts/src/assets/iconfont/iconfont.eot


File diff suppressed because it is too large
+ 0 - 0
frontend/vue-ts/src/assets/iconfont/iconfont.js


+ 23 - 0
frontend/vue-ts/src/assets/iconfont/iconfont.json

@@ -0,0 +1,23 @@
+{
+  "id": "2208059",
+  "name": "CURD-TS",
+  "font_family": "iconfont",
+  "css_prefix_text": "team-icon",
+  "description": "增删查改xi't",
+  "glyphs": [
+    {
+      "icon_id": "5698509",
+      "name": "全屏缩小",
+      "font_class": "exit-fullscreen",
+      "unicode": "e62a",
+      "unicode_decimal": 58922
+    },
+    {
+      "icon_id": "5698510",
+      "name": "全屏显示",
+      "font_class": "fullscreen",
+      "unicode": "e62b",
+      "unicode_decimal": 58923
+    }
+  ]
+}

+ 32 - 0
frontend/vue-ts/src/assets/iconfont/iconfont.svg

@@ -0,0 +1,32 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<!--
+2013-9-30: Created.
+-->
+<svg>
+<metadata>
+Created by iconfont
+</metadata>
+<defs>
+
+<font id="iconfont" horiz-adv-x="1024" >
+  <font-face
+    font-family="iconfont"
+    font-weight="500"
+    font-stretch="normal"
+    units-per-em="1024"
+    ascent="896"
+    descent="-128"
+  />
+    <missing-glyph />
+    
+    <glyph glyph-name="exit-fullscreen" unicode="&#58922;" d="M366.2 714.2c-1 8-10.8 11.4-16.5 5.7l-53.1-53.1L134.2 829c-3.8 3.8-10 3.8-13.7 0L69 777.7c-3.8-3.8-3.8-10 0-13.7l162.4-162.4-53.3-53.3c-5.7-5.7-2.3-15.5 5.7-16.5l194.6-23c6.2-0.7 11.5 4.5 10.8 10.8l-23 194.6z m12.3-453.3l-194.7-23c-8-1-11.4-10.8-5.7-16.5l53.3-53.3L69 5.899999999999977c-3.8-3.8-3.8-10 0-13.7l51.5-51.4c3.8-3.8 10-3.8 13.7 0l162.4 162.3 53.1-53.1c5.7-5.7 15.5-2.3 16.5 5.7l23 194.4c0.7 6.3-4.5 11.5-10.7 10.8z m269.4 248l194.7 23c8 1 11.4 10.8 5.7 16.5L795 601.6l162.4 162.3c3.8 3.8 3.8 10 0 13.7L905.9 829c-3.8 3.8-10 3.8-13.7 0L729.7 666.8l-53.1 53.1c-5.7 5.7-15.6 2.3-16.5-5.7l-23-194.5c-0.6-6.3 4.6-11.5 10.8-10.8zM795 168.20000000000005l53.3 53.3c5.7 5.7 2.3 15.5-5.7 16.5L648 261c-6.2 0.7-11.5-4.5-10.8-10.8l23-194.6c1-8 10.8-11.4 16.5-5.7l53.1 53.1 162.4-162.3c3.8-3.8 10-3.8 13.7 0l51.5 51.4c3.8 3.8 3.8 10 0 13.7L795 168.20000000000005z m0 0"  horiz-adv-x="1024" />
+
+    
+    <glyph glyph-name="fullscreen" unicode="&#58923;" d="M229.8 733l55.7 55.7c6 6 2.4 16.2-6 17.2l-203.2 24c-6.5 0.8-12-4.7-11.3-11.3l24-203.2c1-8.4 11.3-11.9 17.2-6l55.4 55.4 169.6-169.4c3.9-3.9 10.4-3.9 14.3 0l53.8 53.6c3.9 3.9 3.9 10.4 0 14.3L229.8 733z m447.3-237.6c3.9-3.9 10.4-3.9 14.3 0L861 664.9l55.4-55.4c6-6 16.2-2.4 17.2 6l24 203c0.8 6.5-4.7 12-11.3 11.3l-203.2-24c-8.4-1-11.9-11.3-6-17.2l55.7-55.7-169.5-169.4c-3.9-3.9-3.9-10.4 0-14.3l53.8-53.8z m256.6-343.9c-1 8.4-11.3 11.9-17.2 6L861 102 691.4 271.5c-3.9 3.9-10.4 3.9-14.3 0l-53.8-53.6c-3.9-3.9-3.9-10.4 0-14.3L792.9 34l-55.7-55.7c-6-6-2.4-16.2 6-17.2l203.2-24c6.5-0.8 12 4.7 11.3 11.3l-24 203.1z m-588.1 120c-3.9 3.9-10.4 3.9-14.3 0L161.7 102l-55.4 55.4c-6 6-16.2 2.4-17.2-6l-24-203c-0.8-6.5 4.7-12.1 11.3-11.3l203.2 24c8.4 1 11.9 11.3 6 17.2l-55.7 55.5 169.6 169.4c3.9 3.9 3.9 10.4 0 14.3l-53.9 54z m0 0"  horiz-adv-x="1024" />
+
+    
+
+
+  </font>
+</defs></svg>

BIN
frontend/vue-ts/src/assets/iconfont/iconfont.ttf


BIN
frontend/vue-ts/src/assets/iconfont/iconfont.woff


BIN
frontend/vue-ts/src/assets/iconfont/iconfont.woff2


BIN
frontend/vue-ts/src/assets/login.png


BIN
frontend/vue-ts/src/assets/welcome - 副本.png


BIN
frontend/vue-ts/src/assets/welcome.png


+ 95 - 0
frontend/vue-ts/src/components/breadCrumb/index.vue

@@ -0,0 +1,95 @@
+<template>
+  <el-breadcrumb class="app-breadcrumb" separator="/">
+    <transition-group appear name="breadcrumb">
+      <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
+        <span
+          v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
+          class="no-redirect"
+          >{{ $t(item.meta.title) }}</span
+        >
+        <a v-else @click.prevent="handleLink(item)">{{
+          $t(item.meta.title)
+        }}</a>
+      </el-breadcrumb-item>
+    </transition-group>
+  </el-breadcrumb>
+</template>
+
+<script lang="ts">
+import * as pathToRegexp from "path-to-regexp";
+import { ref, defineComponent, watch, Ref } from "vue";
+import { useRoute, useRouter, RouteLocationMatched } from "vue-router";
+
+export default defineComponent({
+  name: "breadCrumb",
+  setup() {
+    const levelList: Ref<RouteLocationMatched[]> = ref([]);
+    const route = useRoute();
+    const router = useRouter();
+
+    const isDashboard = (route: RouteLocationMatched): Boolean | string => {
+      const name = route && (route.name as string);
+      if (!name) {
+        return false;
+      }
+      return name.trim().toLocaleLowerCase() === "welcome".toLocaleLowerCase();
+    };
+
+    const getBreadcrumb = (): void => {
+      let matched = route.matched.filter(
+        (item) => item.meta && item.meta.title
+      );
+      const first = matched[0];
+      if (!isDashboard(first)) {
+        matched = [
+          ({
+            path: "/welcome",
+            meta: { title: "home" },
+          } as unknown) as RouteLocationMatched,
+        ].concat(matched);
+      }
+      levelList.value = matched.filter(
+        (item) => item.meta && item.meta.title && item.meta.breadcrumb !== false
+      );
+    };
+
+    getBreadcrumb();
+
+    watch(
+      () => route.path,
+      () => getBreadcrumb()
+    );
+
+    const pathCompile = (path: string): string | Object => {
+      const { params } = route;
+      var toPath = pathToRegexp.compile(path);
+      return toPath(params);
+    };
+
+    const handleLink = (item: RouteLocationMatched): any => {
+      const { redirect, path } = item;
+      if (redirect) {
+        router.push(redirect.toString());
+        return;
+      }
+      router.push(pathCompile(path));
+    };
+
+    return { levelList, handleLink };
+  },
+});
+</script>
+
+<style lang="scss" scoped >
+.app-breadcrumb.el-breadcrumb {
+  display: inline-block;
+  font-size: 14px;
+  line-height: 50px;
+  margin-left: 8px;
+
+  .no-redirect {
+    color: #97a8be;
+    cursor: text;
+  }
+}
+</style>

+ 50 - 0
frontend/vue-ts/src/components/hamBurger/index.vue

@@ -0,0 +1,50 @@
+<template>
+  <div style="padding: 0 15px" @click="toggleClick">
+    <svg
+      :class="{ 'is-active': isActive }"
+      class="hamburger"
+      viewBox="0 0 1024 1024"
+      xmlns="http://www.w3.org/2000/svg"
+      width="64"
+      height="64"
+    >
+      <path
+        d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
+      />
+    </svg>
+  </div>
+</template>
+
+<script>
+import { defineComponent } from "vue";
+export default defineComponent({
+  name: "hamBurger",
+  props: {
+    isActive: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  emits: ["toggleClick"],
+  setup(props, ctx) {
+    const toggleClick = () => {
+      ctx.emit("toggleClick");
+    };
+
+    return { toggleClick };
+  },
+});
+</script>
+
+<style scoped>
+.hamburger {
+  display: inline-block;
+  vertical-align: middle;
+  width: 20px;
+  height: 20px;
+}
+
+.hamburger.is-active {
+  transform: rotate(180deg);
+}
+</style>

+ 207 - 0
frontend/vue-ts/src/components/info/index.vue

@@ -0,0 +1,207 @@
+<template>
+  <div class="info">
+    <el-form :model="ruleForm" :rules="rules" ref="ruleForm" class="rule-form">
+      <el-form-item prop="userName">
+        <el-input
+          clearable
+          v-model="ruleForm.userName"
+          placeholder="请输入用户名"
+          prefix-icon="el-icon-user"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="passWord">
+        <el-input
+          clearable
+          type="password"
+          show-password
+          v-model="ruleForm.passWord"
+          placeholder="请输入密码"
+          prefix-icon="el-icon-lock"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="verify">
+        <el-input
+          maxlength="2"
+          onkeyup="this.value=this.value.replace(/[^\d.]/g,'');"
+          v-model.number="ruleForm.verify"
+          placeholder="请输入验证码"
+        ></el-input>
+        <span
+          class="verify"
+          title="刷新"
+          v-html="ruleForm.svg"
+          @click.prevent="refreshVerify"
+        ></span>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click.prevent="onBehavior">
+          {{ tipsFalse }}
+        </el-button>
+        <el-button @click="resetForm">重置</el-button>
+        <span class="tips" @click="changPage">{{ tips }}</span>
+      </el-form-item>
+      <span title="测试用户 直接登录" class="secret" @click="noSecret"
+        >免密登录</span
+      >
+    </el-form>
+  </div>
+</template>
+
+<script lang='ts'>
+import {
+  ref,
+  defineComponent,
+  PropType,
+  onBeforeMount,
+  getCurrentInstance,
+  watch,
+  nextTick,
+} from "vue";
+import { storageSession } from "../../utils/storage";
+
+export interface ContextProps {
+  userName: string;
+  passWord: string;
+  verify: number | null;
+  svg: any;
+  telephone?: number;
+  dynamicText?: string;
+}
+
+import { useRouter, useRoute } from "vue-router";
+
+export default defineComponent({
+  props: {
+    ruleForm: {
+      type: Object as PropType<ContextProps>,
+      require: true,
+    },
+  },
+  emits: ["onBehavior", "refreshVerify"],
+  setup(props, ctx) {
+    let vm: any;
+
+    let tips = ref("注册");
+    let tipsFalse = ref("登录");
+
+    const route = useRoute();
+    const router = useRouter();
+
+    watch(
+      route,
+      async ({ path }, prevRoute: unknown): Promise<void> => {
+        await nextTick();
+        path.includes("register")
+          ? (tips.value = "登录") && (tipsFalse.value = "注册")
+          : (tips.value = "注册") && (tipsFalse.value = "登录");
+      },
+      { immediate: true }
+    );
+
+    const rules: Object = ref({
+      userName: [{ required: true, message: "请输入用户名", trigger: "blur" }],
+      passWord: [
+        { required: true, message: "请输入密码", trigger: "blur" },
+        { min: 6, message: "密码长度必须不小于6位", trigger: "blur" },
+      ],
+      verify: [
+        { required: true, message: "请输入验证码", trigger: "blur" },
+        { type: "number", message: "验证码必须是数字类型", trigger: "blur" },
+      ],
+    });
+
+    onBeforeMount(() => {
+      vm = getCurrentInstance(); //获取组件实例
+    });
+
+    // 点击登录或注册
+    const onBehavior = (evt: Object): void => {
+      vm.refs.ruleForm.validate((valid: Boolean) => {
+        if (valid) {
+          ctx.emit("onBehavior", evt);
+        } else {
+          return false;
+        }
+      });
+    };
+
+    // 刷新验证码
+    const refreshVerify = (): void => {
+      ctx.emit("refreshVerify");
+    };
+
+    // 表单重置
+    const resetForm = (): void => {
+      vm.refs.ruleForm.resetFields();
+    };
+
+    // 登录、注册页面切换
+    const changPage = (): void => {
+      tips.value === "注册" ? router.push("/register") : router.push("/login");
+    };
+
+    const noSecret = (): void => {
+      storageSession.setItem("info", {
+        username: "测试用户",
+        accessToken: "eyJhbGciOiJIUzUxMiJ9.test",
+      });
+      router.push("/");
+    };
+
+    return {
+      rules,
+      tips,
+      tipsFalse,
+      resetForm,
+      onBehavior,
+      refreshVerify,
+      changPage,
+      noSecret,
+    };
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.info {
+  width: 30vw;
+  height: 48vh;
+  background: url("../../assets/login.png") no-repeat center;
+  background-size: cover;
+  position: absolute;
+  border-radius: 20px;
+  right: 100px;
+  top: 30vh;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  @media screen and (max-width: 750px){
+    width: 88vw;
+    right: 25px;
+    top: 22vh;
+  }
+  .rule-form {
+    width: 80%;
+    .verify {
+      position: absolute;
+      margin: -10px 0 0 -120px;
+      &:hover {
+        cursor: pointer;
+      }
+    }
+    .tips {
+      color: #409eff;
+      float: right;
+      &:hover {
+        cursor: pointer;
+      }
+    }
+  }
+  .secret {
+    color: #409eff;
+    &:hover {
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 216 - 0
frontend/vue-ts/src/components/splitPane/index.vue

@@ -0,0 +1,216 @@
+<template>
+  <div
+    :style="{ cursor, userSelect }"
+    class="vue-splitter-container clearfix"
+    @mouseup="onMouseUp"
+    @mousemove="onMouseMove"
+  >
+    <div
+      :class="leftClass"
+      :split="splitSet.split"
+      :style="{ [type]: percent + '%' }"
+    >
+      <slot name="paneL"></slot>
+    </div>
+
+    <resizer
+      :style="{ [resizeType]: percent + '%' }"
+      :split="splitSet.split"
+      @mousedown.prevent="onMouseDown"
+      @click.prevent="onClick"
+    ></resizer>
+
+    <div
+      :class="rightClass"
+      :split="splitSet.split"
+      :style="{ [type]: 100 - percent + '%' }"
+    >
+      <slot name="paneR"></slot>
+    </div>
+
+    <div v-if="active" class="vue-splitter-container-mask"></div>
+  </div>
+</template>
+
+<script lang='ts'>
+import {
+  defineComponent,
+  ref,
+  getCurrentInstance,
+  computed,
+  watch,
+  PropType,
+  onBeforeMount,
+} from "vue";
+import resizer from "./resizer.vue";
+
+export interface ContextProps {
+  minPercent: number;
+  defaultPercent: number;
+  split: string;
+}
+
+export default defineComponent({
+  name: "splitPane",
+  components: { resizer },
+  props: {
+    splitSet: {
+      type: Object as PropType<ContextProps>,
+      require: true,
+    },
+  },
+  emits: ["resize"],
+  setup(props, ctx) {
+    let active = ref(false);
+    let hasMoved = ref(false);
+    let height = ref(null);
+    let percent = ref(props.splitSet?.defaultPercent);
+    let type = props.splitSet?.split === "vertical" ? "width" : "height";
+    let resizeType = props.splitSet?.split === "vertical" ? "left" : "top";
+
+    let leftClass = ref([
+      "splitter-pane splitter-paneL",
+      props.splitSet?.split,
+    ]);
+
+    let rightClass = ref([
+      "splitter-pane splitter-paneR",
+      props.splitSet?.split,
+    ]);
+
+    const userSelect = computed(() => {
+      return active.value ? "none" : "";
+    });
+
+    const cursor = computed(() => {
+      return active.value
+        ? props.splitSet?.split === "vertical"
+          ? "col-resize"
+          : "row-resize"
+        : "";
+    });
+
+    const onClick = (): void => {
+      if (!hasMoved.value) {
+        percent.value = 50;
+        ctx.emit("resize", percent.value);
+      }
+    };
+
+    const onMouseDown = (): void => {
+      active.value = true;
+      hasMoved.value = false;
+    };
+
+    const onMouseUp = (): void => {
+      active.value = false;
+    };
+
+    const onMouseMove = (e: any): void => {
+      if (e.buttons === 0 || e.which === 0) {
+        active.value = false;
+      }
+
+      if (active.value) {
+        let offset = 0;
+        let target = e.currentTarget;
+        if (props.splitSet?.split === "vertical") {
+          while (target) {
+            offset += target.offsetLeft;
+            target = target.offsetParent;
+          }
+        } else {
+          while (target) {
+            offset += target.offsetTop;
+            target = target.offsetParent;
+          }
+        }
+
+        const currentPage =
+          props.splitSet?.split === "vertical" ? e.pageX : e.pageY;
+        const targetOffset =
+          props.splitSet?.split === "vertical"
+            ? e.currentTarget.offsetWidth
+            : e.currentTarget.offsetHeight;
+        const percents =
+          Math.floor(((currentPage - offset) / targetOffset) * 10000) / 100;
+
+        if (
+          percents > props.splitSet?.minPercent &&
+          percents < 100 - props.splitSet?.minPercent
+        ) {
+          percent.value = percents;
+        }
+
+        ctx.emit("resize", percent.value);
+
+        hasMoved.value = true;
+      }
+    };
+
+    return {
+      userSelect,
+      cursor,
+      active,
+      hasMoved,
+      height,
+      percent,
+      type,
+      resizeType,
+      onClick,
+      onMouseDown,
+      onMouseUp,
+      onMouseMove,
+      leftClass: leftClass.value.join(" "),
+      rightClass: rightClass.value.join(" "),
+    };
+  },
+});
+</script>
+
+<style scoped>
+.clearfix:after {
+  visibility: hidden;
+  display: block;
+  font-size: 0;
+  content: " ";
+  clear: both;
+  height: 0;
+}
+.vue-splitter-container {
+  height: 100%;
+  position: relative;
+}
+.vue-splitter-container-mask {
+  z-index: 9999;
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  top: 0;
+  left: 0;
+}
+
+.splitter-pane.vertical.splitter-paneL {
+  position: absolute;
+  left: 0px;
+  height: 100%;
+  padding-right: 3px;
+}
+.splitter-pane.vertical.splitter-paneR {
+  position: absolute;
+  right: 0px;
+  height: 100%;
+  padding-left: 3px;
+}
+.splitter-pane.horizontal.splitter-paneL {
+  position: absolute;
+  top: 0px;
+  width: 100%;
+}
+.splitter-pane.horizontal.splitter-paneR {
+  position: absolute;
+  bottom: 0px;
+  width: 100%;
+  padding-top: 3px;
+}
+</style>

+ 59 - 0
frontend/vue-ts/src/components/splitPane/resizer.vue

@@ -0,0 +1,59 @@
+<template>
+  <div :class="classes"></div>
+</template>
+
+<script lang='ts'>
+import { computed, defineComponent } from "vue";
+export default defineComponent({
+  name: "resizer",
+  props: {
+    split: {
+      type: String,
+      required: true,
+    },
+    className: {
+      type: String,
+      default: "",
+    },
+  },
+  setup(props, ctx) {
+    let classes = computed(() => {
+      return ["splitter-pane-resizer", props.split, props.className].join(" ");
+    });
+    return {
+      classes,
+    };
+  },
+});
+</script>
+
+<style scoped>
+.splitter-pane-resizer {
+  -moz-box-sizing: border-box;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  background: #000;
+  position: absolute;
+  opacity: 0.2;
+  z-index: 1;
+  -moz-background-clip: padding;
+  -webkit-background-clip: padding;
+  background-clip: padding-box;
+}
+.splitter-pane-resizer.horizontal {
+  height: 11px;
+  margin: -5px 0;
+  border-top: 5px solid rgba(255, 255, 255, 0);
+  border-bottom: 5px solid rgba(255, 255, 255, 0);
+  cursor: row-resize;
+  width: 100%;
+}
+.splitter-pane-resizer.vertical {
+  width: 11px;
+  height: 100%;
+  margin-left: -5px;
+  border-left: 5px solid rgba(255, 255, 255, 0);
+  border-right: 5px solid rgba(255, 255, 255, 0);
+  cursor: col-resize;
+}
+</style>

+ 46 - 0
frontend/vue-ts/src/layout/components/AppMain.vue

@@ -0,0 +1,46 @@
+<template>
+  <section class="app-main">
+    <router-view :key="key" v-slot="{ Component }">
+      <transition appear name="fade-transform" mode="out-in">
+        <keep-alive>
+          <component :is="Component" />
+        </keep-alive>
+      </transition>
+    </router-view>
+  </section>
+</template>
+
+<script>
+import { computed, defineComponent } from "vue";
+import { useRoute } from "vue-router";
+export default defineComponent({
+  name: "AppMain",
+  setup() {
+    const route = useRoute();
+    const key = computed(() => route.path);
+
+    return { key };
+  },
+});
+</script>
+
+<style scoped>
+.app-main {
+  min-height: calc(100vh - 50px);
+  width: 100%;
+  position: relative;
+  overflow: hidden;
+  margin: 10px;
+}
+.fixed-header + .app-main {
+  padding-top: 50px;
+}
+</style>
+
+<style lang="scss">
+.el-popup-parent--hidden {
+  .fixed-header {
+    padding-right: 15px;
+  }
+}
+</style>

+ 178 - 0
frontend/vue-ts/src/layout/components/Navbar.vue

@@ -0,0 +1,178 @@
+<template>
+  <div class="navbar">
+    <hamburger
+      :is-active="sidebar.opened"
+      class="hamburger-container"
+      @toggleClick="toggleSideBar"
+    />
+
+    <breadcrumb class="breadcrumb-container" />
+
+    <div class="right-menu">
+      <screenfull />
+      <div class="inter" :title="langs ? '中文' : '英文'" @click="toggleLang">
+        <img :src="langs ? ch : en" />
+      </div>
+      <el-dropdown>
+        <span class="el-dropdown-link">
+          <img :src="favicon" />
+          <p>{{ usename }}</p>
+        </span>
+        <template #dropdown>
+          <el-dropdown-menu>
+            <el-dropdown-item icon="el-icon-switch-button" @click="logout">
+              {{ $t("LoginOut") }}
+            </el-dropdown-item>
+          </el-dropdown-menu>
+        </template>
+      </el-dropdown>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { ref, reactive, defineComponent, onMounted, nextTick } from "vue";
+import Breadcrumb from "../../components/breadCrumb/index.vue";
+import Hamburger from "../../components/hamBurger/index.vue";
+import screenfull from "../components/screenfull/index.vue";
+import { useMapGetters } from "../store";
+import { useRoute, useRouter } from "vue-router";
+import { mapGetters, useStore } from "vuex";
+import { storageSession } from "../../utils/storage";
+import { useI18n } from "vue-i18n";
+import ch from "/@/assets/ch.png";
+import en from "/@/assets/en.png";
+import favicon from "/favicon.ico";
+export default defineComponent({
+  name: "Navbar",
+  components: {
+    Breadcrumb,
+    Hamburger,
+    screenfull,
+  },
+  setup() {
+    let langs = ref(true);
+
+    const store = useStore();
+    const router = useRouter();
+
+    let usename = storageSession.getItem("info").username;
+
+    const { locale } = useI18n();
+
+    // 国际化语言切换
+    const toggleLang = (): void => {
+      langs.value = !langs.value;
+      langs.value ? (locale.value = "ch") : (locale.value = "en");
+    };
+
+    // 退出登录
+    const logout = (): void => {
+      storageSession.removeItem("info");
+      router.push("/login");
+    };
+
+    onMounted(() => {
+      document
+        .querySelector(".el-dropdown__popper")
+        ?.setAttribute("class", "resetTop");
+      document
+        .querySelector(".el-popper__arrow")
+        ?.setAttribute("class", "hidden");
+    });
+
+    return {
+      // @ts-ignore
+      ...useMapGetters(["sidebar"]),
+      toggleSideBar() {
+        store.dispatch("app/toggleSideBar");
+      },
+      langs,
+      usename,
+      toggleLang,
+      logout,
+      ch,
+      en,
+      favicon
+    };
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.navbar {
+  height: 50px;
+  overflow: hidden;
+  position: relative;
+  background: #fff;
+  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
+
+  .hamburger-container {
+    line-height: 46px;
+    height: 100%;
+    float: left;
+    cursor: pointer;
+    transition: background 0.3s;
+    -webkit-tap-highlight-color: transparent;
+
+    &:hover {
+      background: rgba(0, 0, 0, 0.025);
+    }
+  }
+
+  .breadcrumb-container {
+    float: left;
+  }
+
+  .right-menu {
+    float: right;
+    display: flex;
+    align-items: center;
+    .inter {
+      width: 40px;
+      height: 48px;
+      display: flex;
+      align-items: center;
+      justify-content: space-around;
+      margin-right: 5px;
+      &:hover {
+        cursor: pointer;
+        background: #f0f0f0;
+      }
+      img {
+        width: 25px;
+      }
+    }
+    .el-dropdown-link {
+      width: 80px;
+      height: 48px;
+      display: flex;
+      align-items: center;
+      justify-content: space-around;
+      margin-right: 20px;
+      p {
+        font-size: 13px;
+      }
+      &:hover {
+        background: #f0f0f0;
+      }
+      img {
+        width: 22px;
+        height: 22px;
+      }
+    }
+  }
+}
+// single element-plus reset
+.el-dropdown-menu__item {
+  padding: 0 10px;
+}
+.el-dropdown-menu {
+  padding: 0;
+}
+.el-dropdown-menu__item:focus,
+.el-dropdown-menu__item:not(.is-disabled):hover {
+  color: #606266;
+  background: #f0f0f0;
+}
+</style>

+ 4 - 0
frontend/vue-ts/src/layout/components/index.ts

@@ -0,0 +1,4 @@
+export { default as Navbar } from './Navbar.vue'
+export { default as Sidebar } from './sidebar/index.vue'
+export { default as AppMain } from './AppMain.vue'
+export { default as setting } from './setting/index.vue'

+ 133 - 0
frontend/vue-ts/src/layout/components/panel/index.vue

@@ -0,0 +1,133 @@
+<template>
+  <div ref="right-panel" :class="{ show: show }" class="right-panel-container">
+    <div class="right-panel-background" />
+    <div class="right-panel">
+      <div
+        class="handle-button"
+        :title="show ? '关闭设置' : '打开设置'"
+        @click="show = !show"
+      >
+        <i :class="show ? 'el-icon-close' : 'el-icon-setting'" />
+      </div>
+      <div class="right-panel-items">
+        <slot />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang='ts'>
+import { addClass, removeClass } from "../../../utils/operate";
+import { ref, watch, getCurrentInstance, onMounted, onBeforeMount } from "vue";
+export default {
+  name: "panel",
+  setup() {
+    let vm: any;
+
+    let show = ref(false);
+
+    watch(
+      show,
+      (val, prevVal) => {
+        val ? addEventClick() : () => {};
+        if (val) {
+          addClass(document.body, "showright-panel");
+        } else {
+          removeClass(document.body, "showright-panel");
+        }
+      },
+      { immediate: true }
+    );
+
+    const addEventClick = (): void => {
+      window.addEventListener("click", closeSidebar);
+    };
+
+    const closeSidebar = (evt: any): void => {
+      const parent = evt.target.closest(".right-panel");
+      if (!parent) {
+        show.value = false;
+        window.removeEventListener("click", closeSidebar);
+      }
+    };
+
+    onBeforeMount(() => {
+      vm = getCurrentInstance();
+    });
+
+    return {
+      show,
+    };
+  },
+};
+</script>
+
+<style>
+.showright-panel {
+  overflow: hidden;
+  position: relative;
+  width: calc(100% - 15px);
+}
+</style>
+
+<style lang="scss" scoped>
+.right-panel-background {
+  position: fixed;
+  top: 0;
+  left: 0;
+  opacity: 0;
+  transition: opacity 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
+  background: rgba(0, 0, 0, 0.2);
+  z-index: -1;
+}
+
+.right-panel {
+  width: 100%;
+  max-width: 260px;
+  height: 100vh;
+  position: fixed;
+  top: 0;
+  right: 0;
+  box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.05);
+  transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
+  transform: translate(100%);
+  background: #fff;
+  z-index: 40000;
+}
+
+.show {
+  transition: all 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
+
+  .right-panel-background {
+    z-index: 20000;
+    opacity: 1;
+    width: 100%;
+    height: 100%;
+  }
+
+  .right-panel {
+    transform: translate(0);
+  }
+}
+
+.handle-button {
+  width: 48px;
+  height: 48px;
+  position: absolute;
+  left: -48px;
+  text-align: center;
+  font-size: 24px;
+  border-radius: 6px 0 0 6px !important;
+  z-index: 0;
+  pointer-events: auto;
+  cursor: pointer;
+  color: #fff;
+  line-height: 48px;
+  top: 45%;
+  background: rgb(24, 144, 255);
+  i {
+    font-size: 24px;
+    line-height: 48px;
+  }
+}
+</style>

+ 77 - 0
frontend/vue-ts/src/layout/components/screenfull/index.vue

@@ -0,0 +1,77 @@
+<template>
+  <div class="screen-full" @click="onClick">
+    <i
+      :title="isFullscreen ? '退出全屏' : '全屏'"
+      :class="
+        isFullscreen
+          ? 'iconfont team-iconexit-fullscreen'
+          : 'iconfont team-iconfullscreen'
+      "
+    ></i>
+  </div>
+</template>
+
+<script>
+import screenfull from "screenfull";
+import {
+  ref,
+  onBeforeMount,
+  onUnmounted,
+  defineComponent,
+  onMounted,
+} from "vue";
+export default defineComponent({
+  name: "screenfull",
+  setup() {
+    let isFullscreen = ref(false);
+
+    const onClick = () => {
+      if (!screenfull.isEnabled) return;
+      screenfull.toggle();
+    };
+
+    const change = () => {
+      isFullscreen.value = screenfull.isFullscreen;
+    };
+
+    const init = () => {
+      if (screenfull.isEnabled) {
+        screenfull.on("change", change);
+      }
+    };
+
+    const destroy = () => {
+      if (screenfull.isEnabled) {
+        screenfull.off("change", change);
+      }
+    };
+
+    onMounted(() => {
+      init();
+    });
+
+    onUnmounted(() => {
+      destroy();
+    });
+
+    return {
+      isFullscreen,
+      onClick,
+    };
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.screen-full {
+  width: 40px;
+  height: 48px;
+  display: flex;
+  align-items: center;
+  justify-content: space-around;
+  &:hover {
+    cursor: pointer;
+    background: #f0f0f0;
+  }
+}
+</style>

+ 74 - 0
frontend/vue-ts/src/layout/components/setting/index.vue

@@ -0,0 +1,74 @@
+<template>
+  <panel>
+    <el-divider>界面显示</el-divider>
+    <ul class="setting">
+      <li>
+        <span>灰色模式</span>
+        <vxe-switch
+          v-model="greyVal"
+          open-label="开"
+          close-label="关"
+          @change="greyChange"
+        ></vxe-switch>
+      </li>
+    </ul>
+  </panel>
+</template>
+
+<script lang='ts'>
+import panel from "../panel/index.vue";
+import { onMounted, reactive, toRefs } from "vue";
+import { storageLocal } from "../../../utils/storage";
+export default {
+  name: "setting",
+  components: { panel },
+  setup() {
+    const localOperate = (key: string, value?: any, model?: string): any => {
+      model && model === "set"
+        ? storageLocal.setItem(key, value)
+        : storageLocal.getItem(key);
+    };
+
+    const settings = reactive({
+      greyVal: storageLocal.getItem("greyVal"),
+    });
+
+    settings.greyVal === null
+      ? localOperate("greyVal", false, "set")
+      : document.querySelector("html")?.setAttribute("class", "html-grey");
+
+    // 灰色模式设置
+    const greyChange = ({ value }): void => {
+      if (value) {
+        localOperate("greyVal", true, "set");
+        document.querySelector("html")?.setAttribute("class", "html-grey");
+      } else {
+        localOperate("greyVal", false, "set");
+        document.querySelector("html")?.removeAttribute("class");
+      }
+    };
+
+    return {
+      ...toRefs(settings),
+      localOperate,
+      greyChange,
+    };
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.setting {
+  width: 100%;
+  li {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin: 16px;
+  }
+}
+:deep(.el-divider__text) {
+  font-size: 16px;
+  font-weight: 700;
+}
+</style>

+ 30 - 0
frontend/vue-ts/src/layout/components/sidebar/Link.vue

@@ -0,0 +1,30 @@
+<template>
+  <component :is="type" v-bind="linkProps(to)">
+    <slot />
+  </component>
+</template>
+
+<script>
+import { computed, defineComponent } from "vue";
+
+export default defineComponent({
+  name: "Link",
+  props: {
+    to: {
+      type: String,
+      required: true,
+    },
+  },
+  setup(props) {
+    const linkProps = (to) => {
+      return {
+        to: to,
+      };
+    };
+    return {
+      type: "router-link",
+      linkProps,
+    };
+  },
+});
+</script>

+ 101 - 0
frontend/vue-ts/src/layout/components/sidebar/SidebarItem.vue

@@ -0,0 +1,101 @@
+<template>
+  <div v-if="!item.hidden">
+    <template
+      v-if="
+        hasOneShowingChild(item.children, item) &&
+        (!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
+        !item.alwaysShow
+      "
+    >
+      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
+        <el-menu-item
+          :index="resolvePath(onlyOneChild.path)"
+          :class="{ 'submenu-title-noDropdown': !isNest }"
+        >
+          <i :class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
+          <template #title>
+            <span>{{ $t(onlyOneChild.meta.title) }}</span>
+          </template>
+        </el-menu-item>
+      </app-link>
+    </template>
+
+    <el-submenu
+      v-else
+      ref="subMenu"
+      :index="resolvePath(item.path)"
+      popper-append-to-body
+    >
+      <template #title>
+        <i :class="item.meta.icon"></i>
+        <span>{{ $t(item.meta.title) }}</span>
+      </template>
+      <sidebar-item
+        v-for="child in item.children"
+        :key="child.path"
+        :is-nest="true"
+        :item="child"
+        :base-path="resolvePath(child.path)"
+        class="nest-menu"
+      />
+    </el-submenu>
+  </div>
+</template>
+
+<script lang="ts">
+import path from "path";
+import AppLink from "./Link.vue";
+import { defineComponent, PropType, ref } from "vue";
+import { RouteRecordRaw } from "vue-router";
+export default defineComponent({
+  name: "SidebarItem",
+  components: { AppLink },
+  props: {
+    item: {
+      type: Object as PropType<RouteRecordRaw>,
+      required: true,
+    },
+    isNest: {
+      type: Boolean,
+      default: false,
+    },
+    basePath: {
+      type: String,
+      default: "",
+    },
+  },
+  setup(props) {
+    const onlyOneChild = ref<RouteRecordRaw>({} as any);
+
+    function hasOneShowingChild(
+      children: RouteRecordRaw[] = [],
+      parent: RouteRecordRaw
+    ) {
+      const showingChildren = children.filter((item) => {
+        if (item.hidden) {
+          return false;
+        } else {
+          onlyOneChild.value = item;
+          return true;
+        }
+      });
+
+      if (showingChildren.length === 1) {
+        return true;
+      }
+
+      if (showingChildren.length === 0) {
+        onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
+        return true;
+      }
+      return false;
+    }
+
+    const resolvePath = (routePath: string) => {
+      return path.resolve(props.basePath, routePath);
+    };
+
+    return { hasOneShowingChild, resolvePath, onlyOneChild };
+  },
+});
+</script>

+ 55 - 0
frontend/vue-ts/src/layout/components/sidebar/index.vue

@@ -0,0 +1,55 @@
+<template>
+  <el-scrollbar wrap-class="scrollbar-wrapper">
+    <el-menu
+      :default-active="activeMenu"
+      :collapse="isCollapse"
+      background-color="#304156"
+      text-color="#bfcbd9"
+      :unique-opened="false"
+      active-text-color="#409EFF"
+      :collapse-transition="false"
+      mode="vertical"
+    >
+      <sidebar-item
+        v-for="route in routes"
+        :key="route.path"
+        :item="route"
+        :base-path="route.path"
+      />
+    </el-menu>
+  </el-scrollbar>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import { useStore } from "vuex";
+import SidebarItem from "./SidebarItem.vue";
+import { algorithm } from "../../../utils/algorithm";
+
+export default defineComponent({
+  name: "sidebar",
+  components: { SidebarItem },
+  setup() {
+    const router = useRouter().options.routes;
+
+    const store = useStore();
+
+    const route = useRoute();
+
+    const activeMenu = computed(() => {
+      const { meta, path } = route;
+      if (meta.activeMenu) {
+        return meta.activeMenu;
+      }
+      return path;
+    });
+
+    return {
+      routes: computed(() => algorithm.increaseIndexes(router)),
+      activeMenu,
+      isCollapse: computed(() => !store.getters.sidebar.opened),
+    };
+  },
+});
+</script>

+ 51 - 0
frontend/vue-ts/src/layout/components/tag/index.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="tags">
+    <el-tag
+      size="medium"
+      v-for="tag in tags"
+      :key="tag.name"
+      closable
+      :type="tag.type"
+      >{{ tag.name }}</el-tag
+    >
+  </div>
+</template>
+
+<script lang='ts'>
+import { ref, defineComponent, onUnmounted, onMounted } from "vue";
+export default defineComponent({
+  name: "tag",
+  setup() {
+    let flag = ref(true);
+
+    const tags = ref([
+      { name: "首页", type: "info" },
+      { name: "基础管理", type: "info" },
+    ]);
+
+    return {
+      tags,
+      flag,
+    };
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.tags {
+  height: 32px;
+  float: right;
+  border: 1px solid #f0f0f0;
+  display: flex;
+  align-items: center;
+  transition: 0.18s;
+}
+:deep(.el-tag) {
+  background-color: #fff;
+  border: 1px solid #d0d7e7;
+  margin-left: 4px;
+  &:first-child {
+    margin-left: 8px;
+  }
+}
+</style>

+ 176 - 0
frontend/vue-ts/src/layout/index.vue

@@ -0,0 +1,176 @@
+<template>
+  <div :class="classes" class="app-wrapper">
+    <div
+      v-if="device === 'mobile' && sidebar.opened"
+      class="drawer-bg"
+      @click="handleClickOutside"
+    />
+    <!-- 侧边栏 -->
+    <sidebar class="sidebar-container" />
+    <div class="main-container">
+      <div :class="{ 'fixed-header': fixedHeader }">
+        <!-- 顶部导航栏 -->
+        <navbar />
+      </div>
+      <!-- 主体内容 -->
+      <app-main />
+    </div>
+    <!-- 系统设置 -->
+    <setting />
+  </div>
+</template>
+
+<script lang="ts">
+import { Navbar, Sidebar, AppMain, setting } from "./components";
+import {
+  ref,
+  reactive,
+  computed,
+  toRefs,
+  watch,
+  watchEffect,
+  onMounted,
+  onBeforeMount,
+  onBeforeUnmount,
+} from "vue";
+import { useStore } from "vuex";
+interface setInter {
+  sidebar: any;
+  device: String;
+  fixedHeader: Boolean;
+  classes: any;
+}
+
+export default {
+  name: "layout",
+  components: {
+    Navbar,
+    Sidebar,
+    AppMain,
+    setting,
+  },
+  setup() {
+    const store = useStore();
+
+    const WIDTH = ref(992);
+
+    const set: setInter = reactive({
+      sidebar: computed(() => {
+        return store.state.app.sidebar;
+      }),
+
+      device: computed(() => {
+        return store.state.app.device;
+      }),
+
+      fixedHeader: computed(() => {
+        return store.state.settings.fixedHeader;
+      }),
+
+      classes: computed(() => {
+        return {
+          hideSidebar: !set.sidebar.opened,
+          openSidebar: set.sidebar.opened,
+          withoutAnimation: set.sidebar.withoutAnimation,
+          mobile: set.device === "mobile",
+        };
+      }),
+    });
+
+    watchEffect(() => {
+      if (set.device === "mobile" && !set.sidebar.opened) {
+        store.dispatch("app/closeSideBar", { withoutAnimation: false });
+      }
+    })
+ 
+    const handleClickOutside = () => {
+      store.dispatch("app/closeSideBar", { withoutAnimation: false });
+    };
+
+    const $_isMobile = () => {
+      const rect = document.body.getBoundingClientRect();
+      return rect.width - 1 < WIDTH.value;
+    };
+
+    const $_resizeHandler = () => {
+      if (!document.hidden) {
+        const isMobile = $_isMobile();
+        store.dispatch("app/toggleDevice", isMobile ? "mobile" : "desktop");
+
+        if (isMobile) {
+          store.dispatch("app/closeSideBar", { withoutAnimation: true });
+        }
+      }
+    };
+
+    onMounted(() => {
+      const isMobile = $_isMobile();
+      if (isMobile) {
+        store.dispatch("app/toggleDevice", "mobile");
+        store.dispatch("app/closeSideBar", { withoutAnimation: true });
+      }
+    });
+
+    onBeforeMount(() => {
+      window.addEventListener("resize", $_resizeHandler);
+    });
+
+    onBeforeUnmount(() => {
+      window.removeEventListener("resize", $_resizeHandler);
+    });
+
+    return {
+      ...toRefs(set),
+      handleClickOutside,
+    };
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@mixin clearfix {
+  &:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+}
+$sideBarWidth: 210px;
+
+.app-wrapper {
+  @include clearfix;
+  position: relative;
+  height: 100%;
+  width: 100%;
+  &.mobile.openSidebar {
+    position: fixed;
+    top: 0;
+  }
+}
+.drawer-bg {
+  background: #000;
+  opacity: 0.3;
+  width: 100%;
+  top: 0;
+  height: 100%;
+  position: absolute;
+  z-index: 999;
+}
+
+.fixed-header {
+  position: fixed;
+  top: 0;
+  right: 0;
+  z-index: 9;
+  width: calc(100% - #{$sideBarWidth});
+  transition: width 0.28s;
+}
+
+.hideSidebar .fixed-header {
+  width: calc(100% - 54px);
+}
+
+.mobile .fixed-header {
+  width: 100%;
+}
+</style>

+ 14 - 0
frontend/vue-ts/src/layout/store.ts

@@ -0,0 +1,14 @@
+import { computed, ComputedRef } from "vue";
+import { useStore } from "vuex";
+
+export function useMapGetters<T extends string>(keys: T[]) {
+  const res: Record<string, ComputedRef> = {}
+  // @ts-ignore
+  const { getters } = useStore()
+  keys.map(key => {
+    if (Reflect.has(getters, key)) {
+      res[key] = computed(() => getters[key])
+    }
+  })
+  return res as any as Record<T, ComputedRef>
+}

+ 11 - 0
frontend/vue-ts/src/locales/ch.json

@@ -0,0 +1,11 @@
+{
+  "home": "首页",
+  "LoginOut": "退出系统",
+  "usermanagement": "用户管理",
+  "baseinfo": "基础信息",
+  "error": "错误页面",
+  "404": "404",
+  "401": "401",
+  "components": "组件",
+  "split-pane": "切割面板"
+}

+ 11 - 0
frontend/vue-ts/src/locales/en.json

@@ -0,0 +1,11 @@
+{
+  "home": "Home",
+  "LoginOut": "Login Out",
+  "usermanagement": "User Manage",
+  "baseinfo": "Base Info",
+  "error": "Error Page",
+  "404": "404",
+  "401": "401",
+  "components": "Components",
+  "split-pane": "Split Pane"
+}

+ 35 - 0
frontend/vue-ts/src/main.ts

@@ -0,0 +1,35 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+import store from './store'
+
+// 内置ElementPlus
+import ElementPlus from 'element-plus'
+import 'element-plus/lib/theme-chalk/index.css'
+
+// 内置vxe-table
+import 'xe-utils'
+import VXETable from 'vxe-table'
+import 'vxe-table/lib/style.css'
+
+// 内置国际化语言包
+import { createI18n } from 'vue-i18n'
+import ch from "./locales/ch.json"
+import en from "./locales/en.json"
+const i18n = createI18n({
+  locale: 'ch', //默认使用中文
+  messages: {
+    ch,
+    en
+  }
+})
+
+// 导入公共样式
+import './style/index.scss'
+// 导入字体图标
+import "./assets/iconfont/iconfont.js"
+import "./assets/iconfont/iconfont.css"
+
+const app = createApp(App)
+
+app.use(store).use(router).use(i18n).use(ElementPlus).use(VXETable).mount('#app')

+ 171 - 0
frontend/vue-ts/src/router/index.ts

@@ -0,0 +1,171 @@
+import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"
+
+import Layout from '../layout/index.vue'
+
+import { storageSession } from "../utils/storage"
+
+const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: 'home',
+    component: Layout,
+    redirect: "/welcome",
+    children: [{
+      path: '/welcome',
+      name: 'welcome',
+      component: () => import(/* webpackChunkName: "home" */ '../views/welcome.vue'),
+      meta: {
+        title: 'home',
+        showLink: true,
+        savedPosition: false
+      }
+    }],
+    meta: {
+      icon: 'el-icon-s-home',
+      showLink: true,
+      savedPosition: false,
+    }
+  },
+  {
+    path: '/components',
+    name: 'components',
+    component: Layout,
+    redirect: '/components/split-pane',
+    children: [
+      {
+        path: '/components/split-pane',
+        component: () => import(/* webpackChunkName: "components" */ '../views/components/split-pane/index.vue'),
+        meta: {
+          title: 'split-pane',
+          showLink: false,
+          savedPosition: true
+        }
+      },
+    ],
+    meta: {
+      icon: 'el-icon-menu',
+      title: 'components',
+      showLink: true,
+      savedPosition: true
+    }
+  },
+  {
+    path: '/user',
+    name: 'user',
+    component: Layout,
+    redirect: '/user/base',
+    children: [
+      {
+        path: '/user/base',
+        component: () => import(/* webpackChunkName: "user" */ '../views/user.vue'),
+        meta: {
+          // icon: 'el-icon-user',
+          title: 'baseinfo',
+          showLink: false,
+          savedPosition: true
+        }
+      },
+    ],
+    meta: {
+      icon: 'el-icon-user',
+      title: 'usermanagement',
+      showLink: true,
+      savedPosition: true
+    }
+  },
+  {
+    path: '/error',
+    name: 'error',
+    component: Layout,
+    redirect: '/error/401',
+    children: [
+      {
+        path: '/error/401',
+        component: () => import(/* webpackChunkName: "error" */ '../views/error/401.vue'),
+        meta: {
+          title: '401',
+          showLink: false,
+          savedPosition: true
+        }
+      },
+      {
+        path: '/error/404',
+        component: () => import(/* webpackChunkName: "error" */ '../views/error/404.vue'),
+        meta: {
+          title: '404',
+          showLink: false,
+          savedPosition: true
+        }
+      },
+    ],
+    meta: {
+      icon: 'el-icon-position',
+      title: 'error',
+      showLink: true,
+      savedPosition: true
+    }
+  },
+  {
+    path: '/login',
+    name: 'login',
+    component: () => import(/* webpackChunkName: "login" */ '../views/login.vue'),
+    meta: {
+      title: '登陆',
+      showLink: false
+    }
+  },
+  {
+    path: '/register',
+    name: 'register',
+    component: () => import(/* webpackChunkName: "register" */ '../views/register.vue'),
+    meta: {
+      title: '注册',
+      showLink: false
+    }
+  },
+  {
+    // 找不到路由重定向到404页面
+    path: '/:pathMatch(.*)',
+    component: Layout,
+    redirect: "/error/404",
+    meta: {
+      icon: 'el-icon-s-home',
+      title: '首页',
+      showLink: false,
+      savedPosition: false,
+    }
+  },
+]
+
+const router = createRouter({
+  history: createWebHistory(process.env.BASE_URL),
+  routes,
+  scrollBehavior(to, from, savedPosition) {
+    return new Promise((resolve, reject) => {
+      if (savedPosition) {
+        return savedPosition
+      } else {
+        if (from.meta.saveSrollTop) {
+          const top: number = document.documentElement.scrollTop || document.body.scrollTop
+          resolve({ left: 0, top })
+        }
+      }
+    })
+  }
+})
+
+import NProgress from "../utils/progress"
+
+const whiteList = ["/login", "/register"]
+
+router.beforeEach((to, _from, next) => {
+  NProgress.start()
+  document.title = to.meta.title // 动态title
+  whiteList.indexOf(to.path) !== -1 || storageSession.getItem("info") ? next() : next("/login") // 全部重定向到登录页
+})
+
+router.afterEach(() => {
+  NProgress.done()
+})
+
+export default router

+ 9 - 0
frontend/vue-ts/src/settings.ts

@@ -0,0 +1,9 @@
+export default {
+
+  title: 'CURE Admin',
+
+  fixedHeader: false,
+
+  sidebarLogo: false
+
+}

+ 10 - 0
frontend/vue-ts/src/shims-vue.d.ts

@@ -0,0 +1,10 @@
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue'
+  const component: DefineComponent<{}, {}, any>
+  export default component
+}
+
+declare module '*.scss' {
+  const scss: Record<string, string>
+  export default scss;
+}

+ 6 - 0
frontend/vue-ts/src/store/getters.ts

@@ -0,0 +1,6 @@
+const getters = {
+  sidebar: (state: any) => state.app.sidebar,
+  device: (state: any) => state.app.device,
+}
+
+export default getters

+ 12 - 0
frontend/vue-ts/src/store/index.ts

@@ -0,0 +1,12 @@
+import { createStore } from 'vuex'
+import getters from './getters'
+import app from './modules/app'
+import settings from './modules/settings'
+
+export default createStore({
+  getters,
+  modules: {
+    app,
+    settings
+  }
+})

+ 58 - 0
frontend/vue-ts/src/store/modules/app.ts

@@ -0,0 +1,58 @@
+import { storageLocal } from "../../utils/storage"
+interface stateInter {
+  sidebar: {
+    opened: Boolean,
+    withoutAnimation: Boolean
+  },
+  device: String
+}
+
+const state = {
+  sidebar: {
+    opened: storageLocal.getItem('sidebarStatus') ? !!+storageLocal.getItem('sidebarStatus') : true,
+    withoutAnimation: false
+  },
+  device: 'desktop'
+}
+
+const mutations = {
+  TOGGLE_SIDEBAR: (state: stateInter): void => {
+    state.sidebar.opened = !state.sidebar.opened
+    state.sidebar.withoutAnimation = false
+     if (state.sidebar.opened) {
+      storageLocal.setItem('sidebarStatus', 1)
+    } else {
+      storageLocal.setItem('sidebarStatus', 0)
+    }
+  },
+  CLOSE_SIDEBAR: (state: stateInter, withoutAnimation: Boolean) => {
+    storageLocal.setItem('sidebarStatus', 0)
+    state.sidebar.opened = false
+    state.sidebar.withoutAnimation = withoutAnimation
+  },
+  TOGGLE_DEVICE: (state: stateInter, device: String) => {
+    state.device = device
+  }
+}
+
+const actions = {
+  // @ts-ignore
+  toggleSideBar({ commit }) {
+    commit('TOGGLE_SIDEBAR')
+  },
+  // @ts-ignore
+  closeSideBar({ commit }, { withoutAnimation }) {
+    commit('CLOSE_SIDEBAR', withoutAnimation)
+  },
+  // @ts-ignore
+  toggleDevice({ commit }, device) {
+    commit('TOGGLE_DEVICE', device)
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}

+ 29 - 0
frontend/vue-ts/src/store/modules/settings.ts

@@ -0,0 +1,29 @@
+import defaultSettings from '../../settings'
+
+const state = {
+  title: defaultSettings.title,
+  fixedHeader: defaultSettings.fixedHeader,
+  sidebarLogo: defaultSettings.sidebarLogo
+}
+
+const mutations = {
+  CHANGE_SETTING: (state: any, { key, value }) => {
+    if (state.hasOwnProperty(key)) {
+      state[key] = value
+    }
+  }
+}
+
+const actions = {
+  changeSetting({ commit }, data) {
+    commit('CHANGE_SETTING', data)
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}
+

+ 48 - 0
frontend/vue-ts/src/style/element-ui.scss

@@ -0,0 +1,48 @@
+// cover some element-plus styles
+
+.el-breadcrumb__inner,
+.el-breadcrumb__inner a {
+  font-weight: 400 !important;
+}
+
+.el-upload {
+  input[type="file"] {
+    display: none !important;
+  }
+}
+
+.el-upload__input {
+  display: none;
+}
+
+
+.el-dialog {
+  transform: none;
+  left: 0;
+  position: relative;
+  margin: 0 auto;
+}
+
+// refine element ui upload
+.upload-container {
+  .el-upload {
+    width: 100%;
+
+    .el-upload-dragger {
+      width: 100%;
+      height: 200px;
+    }
+  }
+}
+
+// dropdown
+.el-dropdown-menu {
+  a {
+    display: block
+  }
+}
+
+// to fix el-date-picker css style
+.el-range-separator {
+  box-sizing: content-box;
+}

+ 102 - 0
frontend/vue-ts/src/style/index.scss

@@ -0,0 +1,102 @@
+@import './variables.scss';
+@import './mixin.scss';
+@import './transition.scss';
+@import './element-ui.scss';
+@import './sidebar.scss';
+
+body {
+  height: 100%;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
+  text-rendering: optimizeLegibility;
+  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+}
+
+label {
+  font-weight: 700;
+}
+
+html {
+  overflow: hidden;
+  height: 100%;
+  box-sizing: border-box;
+}
+
+#app {
+  height: 100%;
+  overflow: hidden;
+}
+
+*,
+*:before,
+*:after {
+  box-sizing: inherit;
+}
+
+a:focus,
+a:active {
+  outline: none;
+}
+
+a,
+a:focus,
+a:hover {
+  cursor: pointer;
+  color: inherit;
+  text-decoration: none;
+}
+
+div:focus {
+  outline: none;
+}
+
+ul {
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+
+.clearfix {
+  &:after {
+    visibility: hidden;
+    display: block;
+    font-size: 0;
+    content: " ";
+    clear: both;
+    height: 0;
+  }
+}
+
+// main-container global css
+.app-container {
+  padding: 20px;
+}
+
+.login,
+.register {
+  width: 100%;
+  height: 100%;
+  overflow-x: hidden;
+  background: url("../assets/bg.png") no-repeat center;
+  background-size: cover;
+}
+
+/* 头部用户信息样式重置 */
+.hidden {
+  display: none !important;
+}
+
+.resetTop {
+  top: 48px !important;
+}
+
+.html-grey {
+    filter: grayscale(100%);
+    -webkit-filter: grayscale(100%);
+    -moz-filter: grayscale(100%);
+    -ms-filter: grayscale(100%);
+    -o-filter: grayscale(100%);
+    filter: url("data:image/svg+xml;utf8,#grayscale");
+    filter: progid:DXImageTransform.Microsoft.BasicImage(grayscale=1);
+    -webkit-filter: grayscale(1);
+}

+ 28 - 0
frontend/vue-ts/src/style/mixin.scss

@@ -0,0 +1,28 @@
+@mixin clearfix {
+  &:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+}
+
+@mixin scrollBar {
+  &::-webkit-scrollbar-track-piece {
+    background: #d3dce6;
+  }
+
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background: #99a9bf;
+    border-radius: 20px;
+  }
+}
+
+@mixin relative {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}

+ 201 - 0
frontend/vue-ts/src/style/sidebar.scss

@@ -0,0 +1,201 @@
+#app {
+
+  .main-container {
+    min-height: 100%;
+    transition: margin-left .28s;
+    margin-left: $sideBarWidth;
+    position: relative;
+  }
+
+  .sidebar-container {
+    transition: width 0.28s;
+    width: $sideBarWidth !important;
+    background-color: $menuBg;
+    height: 100%;
+    position: fixed;
+    font-size: 0px;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    z-index: 1001;
+    overflow: hidden;
+
+    // reset element-plus css
+    .horizontal-collapse-transition {
+      transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
+    }
+
+    .scrollbar-wrapper {
+      overflow-x: hidden !important;
+    }
+
+    .el-scrollbar__bar.is-vertical {
+      right: 0px;
+    }
+
+    .el-scrollbar {
+      height: 100%;
+    }
+
+    &.has-logo {
+      .el-scrollbar {
+        height: calc(100% - 50px);
+      }
+    }
+
+    .is-horizontal {
+      display: none;
+    }
+
+    a {
+      display: inline-block;
+      width: 100%;
+      overflow: hidden;
+    }
+
+    .el-menu {
+      border: none;
+      height: 100%;
+      // width: 100% !important;
+    }
+
+    // menu hover
+    .submenu-title-noDropdown,
+    .el-submenu__title {
+      &:hover {
+        background-color: $menuHover !important;
+      }
+    }
+
+    .is-active>.el-submenu__title {
+      color: $subMenuActiveText !important;
+    }
+
+    & .nest-menu .el-submenu>.el-submenu__title,
+    & .el-submenu .el-menu-item {
+      min-width: $sideBarWidth !important;
+      background-color: $subMenuBg !important;
+
+      &:hover {
+        background-color: $subMenuHover !important;
+      }
+    }
+  }
+
+  .hideSidebar {
+   
+    .sidebar-container {
+      width: 54px !important;
+    }
+
+    .main-container {
+      margin-left: 54px;
+    }
+
+    .submenu-title-noDropdown {
+      padding: 0 !important;
+      position: relative;
+
+      .el-tooltip {
+        padding: 0 !important;
+        
+      }
+    }
+
+    .el-submenu {
+      overflow: hidden;
+      &>.el-submenu__title {
+
+        .el-submenu__icon-arrow {
+          display: none;
+        }
+      }
+    }
+
+    .el-menu--collapse {
+      margin-left: -5px; //需优化的地方
+      .el-submenu {
+        &>.el-submenu__title {
+          &>span {
+            height: 0;
+            width: 0;
+            overflow: hidden;
+            visibility: hidden;
+            display: inline-block;
+          }
+        }
+      }
+    }
+  }
+
+  .el-menu--collapse .el-menu .el-submenu {
+    min-width: $sideBarWidth !important;
+  }
+
+  // mobile responsive
+  .mobile {
+    .main-container {
+      margin-left: 0px;
+    }
+
+    .sidebar-container {
+      transition: transform .28s;
+      width: $sideBarWidth !important;
+    }
+
+    &.hideSidebar {
+      .sidebar-container {
+        pointer-events: none;
+        transition-duration: 0.3s;
+        transform: translate3d(-$sideBarWidth, 0, 0);
+      }
+    }
+  }
+
+  .withoutAnimation {
+
+    .main-container,
+    .sidebar-container {
+      transition: none;
+    }
+  }
+}
+
+// when menu collapsed
+.el-menu--vertical {
+  &>.el-menu {
+    i {
+      margin-right: 16px;
+    }
+  }
+
+  .nest-menu .el-submenu>.el-submenu__title,
+  .el-menu-item {
+    &:hover {
+      // you can use $subMenuHover
+      background-color: $menuHover !important;
+    }
+  }
+
+  // the scroll bar appears when the subMenu is too long
+  >.el-menu--popup {
+    max-height: 100vh;
+    overflow-y: auto;
+
+    &::-webkit-scrollbar-track-piece {
+      background: #d3dce6;
+    }
+
+    &::-webkit-scrollbar {
+      width: 6px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: #99a9bf;
+      border-radius: 20px;
+    }
+  }
+  
+}
+
+.el-scrollbar__wrap { overflow: auto; height: 100%; }

+ 44 - 0
frontend/vue-ts/src/style/transition.scss

@@ -0,0 +1,44 @@
+// global transition css
+
+/* fade */
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.28s;
+}
+
+.fade-enter,
+.fade-leave-active {
+  opacity: 0;
+}
+
+/* fade-transform */
+.fade-transform-leave-active,
+.fade-transform-enter-active {
+  transition: all .5s;
+}
+
+.fade-transform-enter-from {
+  opacity: 0;
+  transform: translateX(-30px);
+}
+
+.fade-transform-leave-to {
+  opacity: 0;
+  transform: translateX(30px);
+}
+
+/* breadcrumb transition */
+.breadcrumb-enter-active,
+.breadcrumb-leave-active {
+  transition: all .5s;
+}
+
+.breadcrumb-enter-from,
+.breadcrumb-leave-active {
+  opacity: 0;
+  transform: translateX(20px);
+}
+
+.breadcrumb-leave-active {
+  position: absolute;
+}

+ 22 - 0
frontend/vue-ts/src/style/variables.scss

@@ -0,0 +1,22 @@
+// sidebar
+$menuText:#bfcbd9;
+$menuActiveText:#409EFF;
+$subMenuActiveText:#f4f4f5; 
+$menuBg:#304156;
+$menuHover:#263445;
+
+$subMenuBg:#1f2d3d;
+$subMenuHover:#001528;
+
+$sideBarWidth: 210px;
+
+:export {
+  menuText: $menuText;
+  menuActiveText: $menuActiveText;
+  subMenuActiveText: $subMenuActiveText;
+  menuBg: $menuBg;
+  menuHover: $menuHover;
+  subMenuBg: $subMenuBg;
+  subMenuHover: $subMenuHover;
+  sideBarWidth: $sideBarWidth;
+}

+ 24 - 0
frontend/vue-ts/src/utils/algorithm/index.ts

@@ -0,0 +1,24 @@
+interface ProxyAlgorithm {
+  increaseIndexes<T>(val: Array<T>): Array<T>
+}
+
+class algorithmProxy implements ProxyAlgorithm {
+
+  constructor() { }
+
+  // 数组每一项添加索引字段
+  public increaseIndexes<T>(val: Array<T>): Array<T> {
+    return Object.keys(val)
+      .map((v) => {
+        return {
+          // @ts-ignore
+          ...val[v],
+          key: v
+        }
+      })
+      .filter(v => v.meta.showLink)
+  }
+
+}
+
+export const algorithm = new algorithmProxy()

+ 11 - 0
frontend/vue-ts/src/utils/debounce/index.ts

@@ -0,0 +1,11 @@
+// 延迟函数
+export const delay = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout))
+
+// 防抖函数
+export const debounce = (fn: () => any, timeout: number) => {
+  let timmer: any
+  return () => {
+    timmer ? clearTimeout(timmer) : null
+    timmer = setTimeout(fn, timeout)
+  }
+}

+ 31 - 0
frontend/vue-ts/src/utils/http/config.ts

@@ -0,0 +1,31 @@
+import { AxiosRequestConfig } from "axios"
+import { excludeProps } from "./utils"
+/**
+ * 默认配置
+ */
+export const defaultConfig: AxiosRequestConfig = {
+  baseURL: '/api',
+  timeout: 10000, //10秒超时
+  headers: {
+    Accept: "application/json, text/plain, */*",
+    "Content-Type": "application/json",
+    "X-Requested-With": "XMLHttpRequest"
+  },
+}
+
+export function genConfig(config?: AxiosRequestConfig): AxiosRequestConfig {
+  if (!config) {
+    return defaultConfig
+  }
+
+  const { headers } = config
+  if (headers && typeof headers === "object") {
+    defaultConfig.headers = {
+      ...defaultConfig.headers,
+      ...headers
+    }
+  }
+  return { ...excludeProps(config!, "headers"), ...defaultConfig }
+}
+
+export const METHODS = ["post", "get", "put", "delete", "option", "patch"]

+ 244 - 0
frontend/vue-ts/src/utils/http/core.ts

@@ -0,0 +1,244 @@
+
+import Axios, {
+  AxiosRequestConfig,
+  CancelTokenStatic,
+  AxiosInstance,
+  Canceler
+} from "axios"
+
+import NProgress from "../progress"
+
+import { genConfig } from "./config"
+
+import { transformConfigByMethod } from "./utils"
+
+import {
+  cancelTokenType,
+  RequestMethods,
+  EnclosureHttpRequestConfig,
+  EnclosureHttpResoponse,
+  EnclosureHttpError
+} from "./types.d"
+
+class EnclosureHttp {
+  constructor() {
+    this.httpInterceptorsRequest()
+    this.httpInterceptorsResponse()
+  }
+  // 初始化配置对象
+  private static initConfig: EnclosureHttpRequestConfig = {};
+
+  // 保存当前Axios实例对象
+  private static axiosInstance: AxiosInstance = Axios.create(genConfig());
+
+  // 保存 EnclosureHttp实例
+  private static EnclosureHttpInstance: EnclosureHttp
+
+  // axios取消对象
+  private CancelToken: CancelTokenStatic = Axios.CancelToken;
+
+  // 取消的凭证数组
+  private sourceTokenList: Array<cancelTokenType> = [];
+
+  // 记录当前这一次cancelToken的key
+  private currentCancelTokenKey = "";
+
+  private beforeRequestCallback: EnclosureHttpRequestConfig["beforeRequestCallback"] = undefined;
+
+  private beforeResponseCallback: EnclosureHttpRequestConfig["beforeResponseCallback"] = undefined;
+
+  public get cancelTokenList(): Array<cancelTokenType> {
+    return this.sourceTokenList
+  }
+
+  // eslint-disable-next-line class-methods-use-this
+  public set cancelTokenList(value) {
+    throw new Error("cancelTokenList不允许赋值")
+  }
+
+  /**
+   * @description 私有构造不允许实例化
+   * @returns void 0
+   */
+  // constructor() {}
+
+  /**
+   * @description 生成唯一取消key
+   * @param config axios配置
+   * @returns string
+   */
+  // eslint-disable-next-line class-methods-use-this
+  private genUniqueKey(config: EnclosureHttpRequestConfig): string {
+    return `${config.url}--${JSON.stringify(config.data)}`
+  }
+
+  /**
+   * @description 取消重复请求
+   * @returns void 0
+   */
+  private cancelRepeatRequest(): void {
+    const temp: { [key: string]: boolean } = {}
+
+    this.sourceTokenList = this.sourceTokenList.reduce<Array<cancelTokenType>>(
+      (res: Array<cancelTokenType>, cancelToken: cancelTokenType) => {
+        const { cancelKey, cancelExecutor } = cancelToken
+        if (!temp[cancelKey]) {
+          temp[cancelKey] = true
+          res.push(cancelToken)
+        } else {
+          cancelExecutor()
+        }
+        return res
+      },
+      []
+    )
+  }
+
+  /**
+   * @description 删除指定的CancelToken
+   * @returns void 0
+   */
+  private deleteCancelTokenByCancelKey(cancelKey: string): void {
+    this.sourceTokenList =
+      this.sourceTokenList.length < 1
+        ? this.sourceTokenList.filter(
+          cancelToken => cancelToken.cancelKey !== cancelKey
+        )
+        : []
+  }
+
+  /**
+   * @description 拦截请求
+   * @returns void 0
+   */
+
+  private httpInterceptorsRequest(): void {
+    EnclosureHttp.axiosInstance.interceptors.request.use(
+      (config: EnclosureHttpRequestConfig) => {
+        const $config = config
+        NProgress.start()   // 每次切换页面时,调用进度条
+        const cancelKey = this.genUniqueKey($config)
+        $config.cancelToken = new this.CancelToken((cancelExecutor: (cancel: any) => void) => {
+          this.sourceTokenList.push({ cancelKey, cancelExecutor })
+        })
+        this.cancelRepeatRequest()
+        this.currentCancelTokenKey = cancelKey
+        // 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
+        if (typeof this.beforeRequestCallback === "function") {
+          this.beforeRequestCallback($config)
+          this.beforeRequestCallback = undefined
+          return $config
+        }
+        if (EnclosureHttp.initConfig.beforeRequestCallback) {
+          EnclosureHttp.initConfig.beforeRequestCallback($config)
+          return $config
+        }
+        return $config
+      },
+      error => {
+        return Promise.reject(error)
+      }
+    )
+  }
+
+  /**
+   * @description 清空当前cancelTokenList
+   * @returns void 0
+   */
+  public clearCancelTokenList(): void {
+    this.sourceTokenList.length = 0
+  }
+
+  /**
+   * @description 拦截相应
+   * @returns void 0
+   */
+  private httpInterceptorsResponse(): void {
+    const instance = EnclosureHttp.axiosInstance
+    instance.interceptors.response.use(
+      (response: EnclosureHttpResoponse) => {
+        // 请求每次成功一次就删除当前canceltoken标记
+        const cancelKey = this.genUniqueKey(response.config)
+        this.deleteCancelTokenByCancelKey(cancelKey)
+        // 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
+        if (typeof this.beforeResponseCallback === "function") {
+          this.beforeResponseCallback(response)
+          this.beforeResponseCallback = undefined
+          return response.data
+        }
+        if (EnclosureHttp.initConfig.beforeResponseCallback) {
+          EnclosureHttp.initConfig.beforeResponseCallback(response)
+          return response.data
+        }
+        NProgress.done()
+        return response.data
+      },
+      (error: EnclosureHttpError) => {
+        const $error = error
+        // 判断当前的请求中是否在 取消token数组理存在,如果存在则移除(单次请求流程)
+        if (this.currentCancelTokenKey) {
+          const haskey = this.sourceTokenList.filter(
+            cancelToken => cancelToken.cancelKey === this.currentCancelTokenKey
+          ).length
+          if (haskey) {
+            this.sourceTokenList = this.sourceTokenList.filter(
+              cancelToken =>
+                cancelToken.cancelKey !== this.currentCancelTokenKey
+            )
+            this.currentCancelTokenKey = ""
+          }
+        }
+        $error.isCancelRequest = Axios.isCancel($error)
+        // 所有的响应异常 区分来源为取消请求/非取消请求
+        return Promise.reject($error)
+      }
+    )
+  }
+
+  public request<T>(
+    method: RequestMethods,
+    url: string,
+    param?: AxiosRequestConfig,
+    axiosConfig?: EnclosureHttpRequestConfig,
+  ): Promise<T> {
+    const config = transformConfigByMethod(param, {
+      method,
+      url,
+      ...axiosConfig
+    } as EnclosureHttpRequestConfig)
+    // 单独处理自定义请求/响应回掉
+    if (axiosConfig?.beforeRequestCallback) {
+      this.beforeRequestCallback = axiosConfig.beforeRequestCallback
+    }
+    if (axiosConfig?.beforeResponseCallback) {
+      this.beforeResponseCallback = axiosConfig.beforeResponseCallback
+    }
+    return new Promise((resolve, reject) => {
+      EnclosureHttp.axiosInstance.request(config)
+        .then((response: EnclosureHttpResoponse) => {
+          resolve(response)
+        })
+        .catch((error: any) => {
+          reject(error)
+        })
+    })
+  }
+
+  public post<T>(
+    url: string,
+    params?: T,
+    config?: EnclosureHttpRequestConfig
+  ): Promise<T> {
+    return this.request<T>("post", url, params, config)
+  }
+
+  public get<T>(
+    url: string,
+    params?: T,
+    config?: EnclosureHttpRequestConfig
+  ): Promise<T> {
+    return this.request<T>("get", url, params, config)
+  }
+}
+
+export default EnclosureHttp

+ 3 - 0
frontend/vue-ts/src/utils/http/index.ts

@@ -0,0 +1,3 @@
+import EnclosureHttp from "./core"
+export const http = new EnclosureHttp()
+

+ 48 - 0
frontend/vue-ts/src/utils/http/types.d.ts

@@ -0,0 +1,48 @@
+import Axios, {
+    AxiosRequestConfig,
+    Canceler,
+    AxiosResponse,
+    Method,
+    AxiosError
+} from "axios"
+
+import { METHODS } from './config'
+
+export type cancelTokenType = { cancelKey: string, cancelExecutor: Canceler }
+
+export type RequestMethods = Extract<Method, 'get' | 'post' | 'put' | 'delete' | 'patch' |
+    'option' | 'head'>
+
+export interface EnclosureHttpRequestConfig extends AxiosRequestConfig {
+    beforeRequestCallback?: (request: EnclosureHttpRequestConfig) => void // 请求发送之前
+    beforeResponseCallback?: (response: EnclosureHttpResoponse) => void // 相应返回之前
+}
+
+export interface EnclosureHttpResoponse extends AxiosResponse {
+    config: EnclosureHttpRequestConfig
+}
+
+export interface EnclosureHttpError extends AxiosError {
+    isCancelRequest?: boolean
+}
+
+export default class EnclosureHttp {
+    cancelTokenList: Array<cancelTokenType>
+    clearCancelTokenList(): void
+    request<T>(
+        method: RequestMethods,
+        url: string,
+        param?: AxiosRequestConfig,
+        axiosConfig?: EnclosureHttpRequestConfig
+    ): Promise<T>
+    post<T>(
+        url: string,
+        params?: T,
+        config?: EnclosureHttpRequestConfig
+    ): Promise<T>
+    get<T>(
+        url: string,
+        params?: T,
+        config?: EnclosureHttpRequestConfig
+    ): Promise<T>
+}

+ 29 - 0
frontend/vue-ts/src/utils/http/utils.ts

@@ -0,0 +1,29 @@
+import { EnclosureHttpRequestConfig } from "./types.d"
+
+export function excludeProps<T extends { [key: string]: any }>(
+  origin: T,
+  prop: string
+): { [key: string]: T } {
+  return Object.keys(origin)
+    .filter(key => !prop.includes(key))
+    .reduce((res, key) => {
+      res[key] = origin[key]
+      return res
+    }, {} as { [key: string]: T })
+}
+
+export function transformConfigByMethod(
+  params: any,
+  config: EnclosureHttpRequestConfig
+): EnclosureHttpRequestConfig {
+  const { method } = config
+  const props = ["delete", "get", "head", "options"].includes(
+    method!.toLocaleLowerCase()
+  )
+    ? "params"
+    : "data"
+  return {
+    ...config,
+    [props]: params
+  }
+}

+ 54 - 0
frontend/vue-ts/src/utils/loaders/index.ts

@@ -0,0 +1,54 @@
+interface ProxyLoader {
+  loadCss(src: string): any
+  loadScript(src: string): Promise<any>
+  loadScriptConcurrent(src: Array<string>): Promise<any>
+}
+
+class loaderProxy implements ProxyLoader {
+
+  constructor() { }
+
+  protected scriptLoaderCache: Array<string> = []
+
+  public loadCss = (src: string): any => {
+    let element = document.createElement("link")
+    element.rel = "stylesheet"
+    element.href = src
+    document.body.appendChild(element)
+  }
+
+  public loadScript = async (src: string): Promise<any> => {
+    if (this.scriptLoaderCache.includes(src)) {
+      return src
+    } else {
+      let element: Element = document.createElement("script")
+      element.src = src
+      document.body.appendChild(element)
+      element.onload = () => {
+        return this.scriptLoaderCache.push(src)
+      }
+    }
+  }
+
+  public loadScriptConcurrent = async (srcList: Array<string>): Promise<any> => {
+    if (Array.isArray(srcList)) {
+      const len: number = srcList.length
+      if (len > 0) {
+        let count: number = 0
+        srcList.map(src => {
+          if (src) {
+            this.loadScript(src).then(() => {
+              count++
+              if (count === len) {
+                return
+              }
+            })
+          }
+        })
+      }
+    }
+  }
+
+}
+
+export const loader = new loaderProxy()

+ 43 - 0
frontend/vue-ts/src/utils/message/index.ts

@@ -0,0 +1,43 @@
+import { ElMessage } from "element-plus"
+
+// 消息
+const Message = (message: string): any => {
+  return ElMessage({
+    showClose: true,
+    message
+  })
+}
+
+// 成功
+const successMessage = (message: string): any => {
+  return ElMessage({
+    showClose: true,
+    message,
+    type: "success"
+  })
+}
+
+// 警告
+const warnMessage = (message: string): any => {
+  return ElMessage({
+    showClose: true,
+    message,
+    type: "warning"
+  })
+}
+
+// 失败
+const errorMessage = (message: string): any => {
+  return ElMessage({
+    showClose: true,
+    message,
+    type: "error"
+  })
+}
+
+export {
+  Message,
+  successMessage,
+  warnMessage,
+  errorMessage
+}

+ 14 - 0
frontend/vue-ts/src/utils/operate/index.ts

@@ -0,0 +1,14 @@
+export const hasClass = (ele: Element, cls:string) :any => {
+  return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
+}
+
+export const addClass = (ele: Element, cls:string) :any =>  {
+  if (!hasClass(ele, cls)) ele.className += ' ' + cls
+}
+
+export const removeClass =(ele: Element, cls:string) :any => {
+  if (hasClass(ele, cls)) {
+    const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
+    ele.className = ele.className.replace(reg, ' ')
+  }
+}

+ 12 - 0
frontend/vue-ts/src/utils/progress/index.ts

@@ -0,0 +1,12 @@
+import NProgress from "nprogress"
+import "nprogress/nprogress.css"
+
+NProgress.configure({
+  easing: 'ease', // 动画方式    
+  speed: 500, // 递增进度条的速度    
+  showSpinner: true, // 是否显示加载ico    
+  trickleSpeed: 200, // 自动递增间隔    
+  minimum: 0.3 // 初始化时的最小百分比
+})
+
+export default NProgress

+ 32 - 0
frontend/vue-ts/src/utils/resize/index.ts

@@ -0,0 +1,32 @@
+import ResizeObserver from 'resize-observer-polyfill'
+
+const isServer = typeof window === 'undefined'
+
+const resizeHandler = (entries: any[]): void => {
+  for (const entry of entries) {
+    const listeners = entry.target.__resizeListeners__ || []
+    if (listeners.length) {
+      listeners.forEach((fn: () => any) => {
+        fn()
+      })
+    }
+  }
+}
+
+export const addResizeListener = (element: any, fn: () => any): any => {
+  if (isServer) return
+  if (!element.__resizeListeners__) {
+    element.__resizeListeners__ = []
+    element.__ro__ = new ResizeObserver(resizeHandler)
+    element.__ro__.observe(element)
+  }
+  element.__resizeListeners__.push(fn)
+}
+
+export const removeResizeListener = (element: any, fn: () => any): any => {
+  if (!element || !element.__resizeListeners__) return
+  element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1)
+  if (!element.__resizeListeners__.length) {
+    element.__ro__.disconnect()
+  }
+}

+ 42 - 0
frontend/vue-ts/src/utils/storage/index.ts

@@ -0,0 +1,42 @@
+interface ProxyStorage {
+  getItem(key: string): any
+  setItem(Key: string, value: string): void
+  removeItem(key: string): void
+}
+
+//sessionStorage operate
+class sessionStorageProxy implements ProxyStorage {
+
+  protected storage: ProxyStorage
+
+  constructor(storageModel: ProxyStorage) {
+    this.storage = storageModel
+  }
+
+  // 存 
+  public setItem(key: string, value: any): void {
+    this.storage.setItem(key, JSON.stringify(value))
+  }
+
+  // 取
+  public getItem(key: string): any {
+    return JSON.parse(this.storage.getItem(key)) || null
+  }
+
+  // 删
+  public removeItem(key: string): void {
+    this.storage.removeItem(key)
+  }
+
+}
+
+//localStorage operate
+class localStorageProxy extends sessionStorageProxy implements ProxyStorage {
+  constructor(localStorage: ProxyStorage) {
+    super(localStorage)
+  }
+}
+
+export const storageSession = new sessionStorageProxy(sessionStorage)
+
+export const storageLocal = new localStorageProxy(localStorage)

+ 85 - 0
frontend/vue-ts/src/views/components/split-pane/index.vue

@@ -0,0 +1,85 @@
+<template>
+  <div class="split-pane">
+    <splitpane :splitSet="settingLR">
+      <!-- #paneL 表示指定该组件为左侧面板 -->
+      <template #paneL>
+        <!-- 自定义左侧面板的内容 -->
+        <div class="dv-a">A</div>
+      </template>
+      <!-- #paneR 表示指定该组件为右侧面板 -->
+      <template #paneR>
+        <!-- 再次将右侧面板进行拆分 -->
+        <splitpane :splitSet="settingTB">
+          <template #paneL>
+            <div class="dv-b">B</div>
+          </template>
+          <template #paneR>
+            <div class="dv-c">C</div>
+          </template>
+        </splitpane>
+      </template>
+    </splitpane>
+  </div>
+</template>
+
+<script lang="ts">
+import splitpane, {
+  ContextProps,
+} from "../../../components/splitPane/index.vue";
+import { reactive } from "vue";
+export default {
+  name: "split",
+  components: {
+    splitpane,
+  },
+  setup() {
+    const settingLR: ContextProps = reactive({
+      minPercent: 20,
+      defaultPercent: 40,
+      split: "vertical",
+    });
+
+    const settingTB: ContextProps = reactive({
+      minPercent: 20,
+      defaultPercent: 40,
+      split: "horizontal",
+    });
+
+    return {
+      settingLR,
+      settingTB,
+    };
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+$W: 100%;
+$H: 80vh;
+.split-pane {
+  width: 98%;
+  height: $H;
+  margin-top: 20px;
+  text-align: center;
+  font-size: 50px;
+  color: #fff;
+  .dv-a,
+  .dv-b,
+  .dv-c {
+    width: $W;
+    height: $W;
+    background: rgba($color: dodgerblue, $alpha: 0.8);
+    line-height: $H;
+  }
+  .dv-b,
+  .dv-c {
+    line-height: 250px;
+  }
+  .dv-b {
+    background: rgba($color: #000, $alpha: 0.8);
+  }
+  .dv-c {
+    background: rgba($color: #ce272d, $alpha: 0.8);
+  }
+}
+</style>

+ 73 - 0
frontend/vue-ts/src/views/error/401.vue

@@ -0,0 +1,73 @@
+<template>
+  <div class="errPage-container">
+    <el-row>
+      <el-col :span="12">
+        <h1 class="text-jumbo text-ginormous">CURD Admin</h1>
+        <h2>你没有权限去该页面</h2>
+        <h6>如有不满请联系你领导</h6>
+      </el-col>
+      <el-col :span="12">
+        <img
+          :src="img"
+          width="313"
+          height="428"
+          alt="Girl has dropped her ice cream."
+        />
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script lang='ts'>
+import imgs from '/@/assets/401.gif'
+import { ref } from "vue"
+export default {
+  name: "401",
+  setup() {
+    const img = ref(`${imgs}?${new Date()}`)
+    return {
+      img
+    };
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.errPage-container {
+  width: 800px;
+  max-width: 100%;
+  margin: 100px auto;
+  .pan-back-btn {
+    background: #008489;
+    color: #fff;
+    border: none !important;
+  }
+  .pan-gif {
+    margin: 0 auto;
+    display: block;
+  }
+  .pan-img {
+    display: block;
+    margin: 0 auto;
+    width: 100%;
+  }
+  .text-jumbo {
+    font-size: 60px;
+    font-weight: 700;
+    color: #484848;
+  }
+  .list-unstyled {
+    font-size: 14px;
+    li {
+      padding-bottom: 5px;
+    }
+    a {
+      color: #008489;
+      text-decoration: none;
+      &:hover {
+        text-decoration: underline;
+      }
+    }
+  }
+}
+</style>

+ 236 - 0
frontend/vue-ts/src/views/error/404.vue

@@ -0,0 +1,236 @@
+<template>
+  <div class="wscn-http404-container">
+    <div class="wscn-http404">
+      <div class="pic-404">
+        <img class="pic-404__parent" :src="four" alt="404" />
+        <img class="pic-404__child left" :src="four_cloud" alt="404" />
+        <img class="pic-404__child mid" :src="four_cloud" alt="404" />
+        <img class="pic-404__child right" :src="four_cloud" alt="404" />
+      </div>
+      <div class="bullshit">
+        <div class="bullshit__oops">CURD Admin</div>
+        <div class="bullshit__headline">{{ message }}</div>
+        <div class="bullshit__info">
+          Please check that the URL you entered is correct, or click the button
+          below to return to the homepage.
+        </div>
+        <a href="" class="bullshit__return-home">Back to home</a>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { computed } from "vue";
+import four from "/@/assets/404.png";
+import four_cloud from "/@/assets/404_cloud.png";
+export default {
+  name: "404",
+  setup() {
+    const message = computed(() => {
+      return "The webmaster said that you can not enter this page...";
+    });
+
+    return {
+      message,
+      four,
+      four_cloud,
+    };
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.wscn-http404-container {
+  transform: translate(-50%, -50%);
+  position: absolute;
+  top: 40%;
+  left: 50%;
+}
+.wscn-http404 {
+  position: relative;
+  width: 1200px;
+  padding: 0 50px;
+  overflow: hidden;
+  .pic-404 {
+    position: relative;
+    float: left;
+    width: 600px;
+    overflow: hidden;
+    &__parent {
+      width: 100%;
+    }
+    &__child {
+      position: absolute;
+      &.left {
+        width: 80px;
+        top: 17px;
+        left: 220px;
+        opacity: 0;
+        animation-name: cloudLeft;
+        animation-duration: 2s;
+        animation-timing-function: linear;
+        animation-fill-mode: forwards;
+        animation-delay: 1s;
+      }
+      &.mid {
+        width: 46px;
+        top: 10px;
+        left: 420px;
+        opacity: 0;
+        animation-name: cloudMid;
+        animation-duration: 2s;
+        animation-timing-function: linear;
+        animation-fill-mode: forwards;
+        animation-delay: 1.2s;
+      }
+      &.right {
+        width: 62px;
+        top: 100px;
+        left: 500px;
+        opacity: 0;
+        animation-name: cloudRight;
+        animation-duration: 2s;
+        animation-timing-function: linear;
+        animation-fill-mode: forwards;
+        animation-delay: 1s;
+      }
+      @keyframes cloudLeft {
+        0% {
+          top: 17px;
+          left: 220px;
+          opacity: 0;
+        }
+        20% {
+          top: 33px;
+          left: 188px;
+          opacity: 1;
+        }
+        80% {
+          top: 81px;
+          left: 92px;
+          opacity: 1;
+        }
+        100% {
+          top: 97px;
+          left: 60px;
+          opacity: 0;
+        }
+      }
+      @keyframes cloudMid {
+        0% {
+          top: 10px;
+          left: 420px;
+          opacity: 0;
+        }
+        20% {
+          top: 40px;
+          left: 360px;
+          opacity: 1;
+        }
+        70% {
+          top: 130px;
+          left: 180px;
+          opacity: 1;
+        }
+        100% {
+          top: 160px;
+          left: 120px;
+          opacity: 0;
+        }
+      }
+      @keyframes cloudRight {
+        0% {
+          top: 100px;
+          left: 500px;
+          opacity: 0;
+        }
+        20% {
+          top: 120px;
+          left: 460px;
+          opacity: 1;
+        }
+        80% {
+          top: 180px;
+          left: 340px;
+          opacity: 1;
+        }
+        100% {
+          top: 200px;
+          left: 300px;
+          opacity: 0;
+        }
+      }
+    }
+  }
+  .bullshit {
+    position: relative;
+    float: left;
+    width: 300px;
+    padding: 30px 0;
+    overflow: hidden;
+    &__oops {
+      font-size: 32px;
+      font-weight: bold;
+      line-height: 40px;
+      color: #1482f0;
+      opacity: 0;
+      margin-bottom: 20px;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-fill-mode: forwards;
+    }
+    &__headline {
+      font-size: 20px;
+      line-height: 24px;
+      color: #222;
+      font-weight: bold;
+      opacity: 0;
+      margin-bottom: 10px;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-delay: 0.1s;
+      animation-fill-mode: forwards;
+    }
+    &__info {
+      font-size: 13px;
+      line-height: 21px;
+      color: grey;
+      opacity: 0;
+      margin-bottom: 30px;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-delay: 0.2s;
+      animation-fill-mode: forwards;
+    }
+    &__return-home {
+      display: block;
+      float: left;
+      width: 110px;
+      height: 36px;
+      background: #1482f0;
+      border-radius: 100px;
+      text-align: center;
+      color: #ffffff;
+      opacity: 0;
+      font-size: 14px;
+      line-height: 36px;
+      cursor: pointer;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-delay: 0.3s;
+      animation-fill-mode: forwards;
+    }
+    @keyframes slideUp {
+      0% {
+        transform: translateY(60px);
+        opacity: 0;
+      }
+      100% {
+        transform: translateY(0);
+        opacity: 1;
+      }
+    }
+  }
+}
+</style>

+ 84 - 0
frontend/vue-ts/src/views/login.vue

@@ -0,0 +1,84 @@
+<template>
+  <div class="login">
+    <info
+      :ruleForm="contextInfo"
+      @on-behavior="onLogin"
+      @refreshVerify="refreshVerify"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  ref,
+  reactive,
+  onMounted,
+  onBeforeMount,
+  getCurrentInstance,
+} from "vue";
+import info, { ContextProps } from "../components/info/index.vue";
+import { getVerify, getLogin } from "../api/user";
+import { useRouter } from "vue-router";
+import { storageSession } from "../utils/storage";
+import { warnMessage, successMessage } from "../utils/message";
+export default {
+  name: "login",
+  components: {
+    info,
+  },
+  setup() {
+    const router = useRouter();
+
+    // 刷新验证码
+    const refreshGetVerify = async () => {
+      let { svg } = await getVerify();
+      contextInfo.svg = svg;
+    };
+
+    const contextInfo: ContextProps = reactive({
+      userName: "",
+      passWord: "",
+      verify: null,
+      svg: null,
+    });
+
+    const toPage = (info: Object): void => {
+      storageSession.setItem("info", info);
+      router.push("/");
+    };
+
+    // 登录
+    const onLogin = async () => {
+      let { userName, passWord, verify } = contextInfo;
+      let { code, info, accessToken } = await getLogin({
+        username: userName,
+        password: passWord,
+        verify: verify,
+      });
+      code === 0
+        ? successMessage(info) &&
+          toPage({
+            username: userName,
+            accessToken,
+          })
+        : warnMessage(info);
+    };
+
+    const refreshVerify = (): void => {
+      refreshGetVerify();
+    };
+
+    onBeforeMount(() => {
+      refreshGetVerify();
+    });
+
+    return {
+      contextInfo,
+      onLogin,
+      router,
+      toPage,
+      refreshVerify,
+    };
+  },
+};
+</script>

+ 73 - 0
frontend/vue-ts/src/views/register.vue

@@ -0,0 +1,73 @@
+<template>
+  <div class="register">
+    <info
+      :ruleForm="contextInfo"
+      @on-behavior="onRegist"
+      @refreshVerify="refreshVerify"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  ref,
+  reactive,
+  onMounted,
+  onBeforeMount,
+  getCurrentInstance,
+} from "vue";
+import info, { ContextProps } from "../components/info/index.vue";
+import { getRegist, getVerify } from "../api/user";
+import { useRouter } from "vue-router";
+import { warnMessage, successMessage } from "../utils/message";
+export default {
+  name: "register",
+  components: {
+    info,
+  },
+  setup() {
+    const router = useRouter();
+
+    // 刷新验证码
+    const refreshGetVerify = async () => {
+      let { svg } = await getVerify();
+      contextInfo.svg = svg;
+    };
+
+    const contextInfo: ContextProps = reactive({
+      userName: "",
+      passWord: "",
+      verify: null,
+      svg: null,
+    });
+
+    // 注册
+    const onRegist = async () => {
+      let { userName, passWord, verify } = contextInfo;
+      let { code, info } = await getRegist({
+        username: userName,
+        password: passWord,
+        verify: verify,
+      });
+      code === 0
+        ? successMessage(info) && router.push("/login")
+        : warnMessage(info);
+    };
+
+    const refreshVerify = (): void => {
+      refreshGetVerify();
+    };
+
+    onBeforeMount(() => {
+      refreshGetVerify();
+    });
+
+    return {
+      contextInfo,
+      onRegist,
+      router,
+      refreshVerify,
+    };
+  },
+};
+</script>

+ 15 - 0
frontend/vue-ts/src/views/user.vue

@@ -0,0 +1,15 @@
+<template>
+  <div>用户管理页面</div>
+</template>
+
+<script lang='ts'>
+export default {
+  name: "user",
+  setup() {
+    return {};
+  },
+};
+</script>
+
+<style scoped>
+</style>

+ 26 - 0
frontend/vue-ts/src/views/welcome.vue

@@ -0,0 +1,26 @@
+<template>
+  <div class="welcome">
+    <a
+      title="欢迎Star"
+      href="https://github.com/xiaoxian521/CURD-TS/tree/vue-ts"
+      target="_blank"
+      >点击打开仓库地址</a
+    >
+  </div>
+</template>
+
+<script lang='ts'>
+export default {
+  name: "welcome",
+};
+</script>
+
+<style scoped>
+.welcome {
+  width: 100%;
+  height: 100%;
+  background: url("../assets/welcome.png") no-repeat center;
+  background-size: cover;
+  position: absolute;
+}
+</style>

+ 45 - 0
frontend/vue-ts/tsconfig.json

@@ -0,0 +1,45 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "module": "esnext",
+    "strict": false,
+    "jsx": "preserve",
+    "importHelpers": true,
+    "moduleResolution": "node",
+    "experimentalDecorators": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "forceConsistentCasingInFileNames": true,
+    "sourceMap": true,
+    "baseUrl": ".",
+    "allowJs": true,
+    "resolveJsonModule": true, // 包含导入的模块。json的扩展
+    "lib": [
+      "dom",
+      "esnext"
+    ],
+    "incremental": true,
+    "paths": {
+      "/@/*": [
+        "src/*"
+      ]
+    },
+    "types": ["node"],
+    "typeRoots": [
+      "node_modules/@types"
+    ],
+  },
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.tsx",
+    "src/**/*.vue",
+    "tests/**/*.ts",
+    "src/utils/path.js"    
+  ],
+  "exclude": [
+    "node_modules",
+    "dist",
+    "**/*.js"
+  ],
+}

+ 45 - 0
frontend/vue-ts/vite.config.ts

@@ -0,0 +1,45 @@
+
+import { resolve } from 'path'
+import vue from '@vitejs/plugin-vue'
+import type { UserConfig } from 'vite'
+import { loadEnv } from './build/utils'
+import { createProxy } from './build/proxy'
+
+const pathResolve = (dir: string): any =>  {
+  return resolve(__dirname, '.', dir)
+}
+
+const { VITE_PORT, VITE_PUBLIC_PATH, VITE_PROXY, VITE_OPEN } = loadEnv()
+
+const alias: Record<string, string> = {
+  '/@': pathResolve('src'),
+}
+
+const root: string = process.cwd()
+
+const viteConfig: UserConfig = {
+  /**
+   * 基本公共路径
+   * @default '/'
+   */
+  base: process.env.NODE_ENV === "production" ? "./" : VITE_PUBLIC_PATH,
+  root,
+  alias,
+  // 服务端渲染
+  server: {
+    // 是否开启 https
+    https: false,
+    /**
+     * 端口号
+     * @default 3000
+     */
+    port: VITE_PORT,
+    // 本地跨域代理
+    proxy: createProxy(VITE_PROXY)
+  },
+  plugins: [
+    vue(),
+  ],
+}
+
+export default viteConfig

Some files were not shown because too many files changed in this diff