zhangjie пре 2 година
комит
6b54967a7a
52 измењених фајлова са 3089 додато и 0 уклоњено
  1. 3 0
      .browserslistrc
  2. 6 0
      .env
  3. 5 0
      .env.production
  4. 3 0
      .eslintignore
  5. 25 0
      .eslintrc.js
  6. 22 0
      .gitignore
  7. 5 0
      .prettierrc
  8. 3 0
      CHANGE.md
  9. 29 0
      README.md
  10. 3 0
      babel.config.js
  11. 51 0
      package.json
  12. BIN
      public/favicon.ico
  13. 25 0
      public/index.html
  14. 5 0
      src/App.vue
  15. BIN
      src/assets/images/bg-home.png
  16. BIN
      src/assets/images/icon-base.png
  17. BIN
      src/assets/images/icon-password.png
  18. BIN
      src/assets/images/logo.png
  19. 356 0
      src/assets/styles/base.scss
  20. 322 0
      src/assets/styles/element-ui-costom.scss
  21. 184 0
      src/assets/styles/home.scss
  22. 5 0
      src/assets/styles/index.scss
  23. 50 0
      src/assets/styles/login.scss
  24. 31 0
      src/assets/styles/variables.scss
  25. 103 0
      src/components/UploadButton.vue
  26. 15 0
      src/components/ViewFooter.vue
  27. 6 0
      src/config.js
  28. 20 0
      src/constants/app.js
  29. 6 0
      src/constants/enumerate.js
  30. 19 0
      src/constants/navs.js
  31. 30 0
      src/main.js
  32. 128 0
      src/modules/admin/api.js
  33. 222 0
      src/modules/admin/components/ModifyApp.vue
  34. 9 0
      src/modules/admin/router.js
  35. 143 0
      src/modules/admin/views/AppManage.vue
  36. 5 0
      src/modules/login/api.js
  37. 13 0
      src/modules/login/router.js
  38. 106 0
      src/modules/login/views/Login.vue
  39. 45 0
      src/plugins/VueCharts.js
  40. 308 0
      src/plugins/axios.js
  41. 45 0
      src/plugins/crypto.js
  42. 14 0
      src/plugins/filters.js
  43. 84 0
      src/plugins/formRules.js
  44. 24 0
      src/plugins/globalVuePlugins.js
  45. 23 0
      src/plugins/md5.js
  46. 18 0
      src/plugins/mixins.js
  47. 37 0
      src/plugins/syncServerTime.js
  48. 257 0
      src/plugins/utils.js
  49. 67 0
      src/router.js
  50. 22 0
      src/store.js
  51. 153 0
      src/views/Home.vue
  52. 34 0
      vue.config.js

+ 3 - 0
.browserslistrc

@@ -0,0 +1,3 @@
+> 1%
+last 2 versions
+not dead

+ 6 - 0
.env

@@ -0,0 +1,6 @@
+NODE_ENV=development
+VUE_APP_DOMAIN=
+VUE_APP_TIMEOUT=600000
+VUE_APP_PAGE_SIZE=10
+VUE_APP_AUTH_TIMEOUT=72000000
+VUE_APP_DEV_PROXY=http://192.168.10.239:9888

+ 5 - 0
.env.production

@@ -0,0 +1,5 @@
+NODE_ENV=production
+VUE_APP_DOMAIN=
+VUE_APP_TIMEOUT=600000
+VUE_APP_PAGE_SIZE=10
+VUE_APP_AUTH_TIMEOUT=7200000

+ 3 - 0
.eslintignore

@@ -0,0 +1,3 @@
+
+public
+vue.config.js

+ 25 - 0
.eslintrc.js

@@ -0,0 +1,25 @@
+module.exports = {
+  root: true,
+  env: {
+    node: true
+  },
+  extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"],
+  parserOptions: {
+    parser: "babel-eslint"
+  },
+  rules: {
+    "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
+    "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
+    "no-unused-vars": [
+      "error",
+      { vars: "all", args: "none", ignoreRestSiblings: false }
+    ],
+    "vue/no-parsing-error": [2, { "x-invalid-end-tag": false }],
+    "vue/no-use-v-if-with-v-for": [
+      "error",
+      {
+        allowUsingIterationVar: true
+      }
+    ]
+  }
+};

+ 22 - 0
.gitignore

@@ -0,0 +1,22 @@
+.DS_Store
+node_modules
+/dist
+dev-proxy.js
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw*

+ 5 - 0
.prettierrc

@@ -0,0 +1,5 @@
+{
+  "semi": true,
+  "singleQuote": false,
+  "jsxBracketSameLine": false
+}

+ 3 - 0
CHANGE.md

@@ -0,0 +1,3 @@
+# 1.0.0
+
+- 初始版本

+ 29 - 0
README.md

@@ -0,0 +1,29 @@
+# ops-web
+
+**运维管理中心web前端**
+
+## 项目操作
+
+#### 项目安装
+
+```
+yarn install
+```
+
+#### 开发模式
+
+```
+yarn run serve
+```
+
+#### 项目打包
+
+```
+yarn run build
+```
+
+#### lint 项目文件,并修正格式
+
+```
+yarn run lint
+```

+ 3 - 0
babel.config.js

@@ -0,0 +1,3 @@
+module.exports = {
+  presets: ["@vue/cli-plugin-babel/preset"]
+};

+ 51 - 0
package.json

@@ -0,0 +1,51 @@
+{
+  "name": "ops-web",
+  "version": "1.0.0",
+  "scripts": {
+    "start": "npm run serve",
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "axios": "^0.19.2",
+    "core-js": "^3.6.4",
+    "crypto-js": "^4.0.0",
+    "deepmerge": "^4.2.2",
+    "element-ui": "^2.13.1",
+    "js-md5": "^0.7.3",
+    "vue": "^2.6.11",
+    "vue-json-viewer": "^2.2.19",
+    "vue-ls": "^3.2.1",
+    "vue-router": "^3.1.6",
+    "vuex": "^3.1.3"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "~4.3.0",
+    "@vue/cli-plugin-eslint": "~4.3.0",
+    "@vue/cli-plugin-router": "~4.3.0",
+    "@vue/cli-plugin-vuex": "~4.3.0",
+    "@vue/cli-service": "~4.3.0",
+    "@vue/composition-api": "^1.0.0-rc.12",
+    "@vue/eslint-config-prettier": "^6.0.0",
+    "babel-eslint": "^10.1.0",
+    "eslint": "^6.7.2",
+    "eslint-plugin-prettier": "^3.1.1",
+    "eslint-plugin-vue": "^6.2.2",
+    "lint-staged": "^9.5.0",
+    "prettier": "^1.19.1",
+    "sass": "^1.26.3",
+    "sass-loader": "^8.0.2",
+    "terser-webpack-plugin": "^1.2.3",
+    "vue-template-compiler": "^2.6.10"
+  },
+  "gitHooks": {
+    "pre-commit": "lint-staged"
+  },
+  "lint-staged": {
+    "*.{js,jsx,vue}": [
+      "vue-cli-service lint",
+      "git add"
+    ]
+  }
+}

BIN
public/favicon.ico


+ 25 - 0
public/index.html

@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width,initial-scale=1.0,
+    maximum-scale=1.0,minimum-scale=1.0, user-scalable=no"" />
+    <meta name=" renderer" content="webkit|ie-comp|ie-stand" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+    <meta name="apple-mobile-web-app-capable" content="yes" />
+    <meta name="apple-mobile-web-app-status-bar-style" content="black" />
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
+    <title><%= htmlWebpackPlugin.options.title %></title>
+  </head>
+  <body>
+    <noscript>
+      <strong
+        >We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
+        properly without JavaScript enabled. Please enable it to
+        continue.</strong
+      >
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 5 - 0
src/App.vue

@@ -0,0 +1,5 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>

BIN
src/assets/images/bg-home.png


BIN
src/assets/images/icon-base.png


BIN
src/assets/images/icon-password.png


BIN
src/assets/images/logo.png


+ 356 - 0
src/assets/styles/base.scss

@@ -0,0 +1,356 @@
+/* reset */
+body,
+div,
+ul,
+ol,
+li,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+input,
+p,
+tr,
+th,
+td,
+span,
+a,
+header,
+footer,
+i {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
+}
+li {
+  list-style: none;
+}
+em,
+i,
+u {
+  font-style: normal;
+}
+input {
+  outline: none;
+  border: none;
+  font-family: $--font-family;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  font-size: 100%;
+  font-weight: normal;
+}
+fieldset,
+img {
+  border: 0;
+}
+abbr {
+  border: 0;
+  font-variant: normal;
+}
+a {
+  text-decoration: none;
+  color: inherit;
+  *color: $--color-text-secondary;
+}
+img {
+  vertical-align: middle;
+}
+
+/* common-style */
+input:-webkit-autofill {
+  box-shadow: 0 0 0 1000px white inset;
+}
+input[type="text"]:focus,
+input[type="password"]:focus,
+input[type="number"]:focus,
+textarea:focus {
+  box-shadow: 0 0 0 1000px white inset;
+}
+
+/* browse style */
+::-webkit-scrollbar {
+  width: 5px;
+  height: 5px;
+  background: transparent;
+}
+::-webkit-scrollbar-button {
+  display: none;
+}
+::-webkit-scrollbar-track {
+  background: transparent;
+}
+::-webkit-scrollbar-thumb {
+  border-radius: 5px;
+  background: $--color-text-secondary;
+}
+::-webkit-scrollbar-corner {
+  background: transparent;
+}
+::-webkit-scrollbar-resizer {
+  background: transparent;
+}
+
+body {
+  font-family: $--font-family;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  font-size: $--font-size-base;
+  color: $--color-text-primary;
+}
+
+/* part */
+// part-box
+.part-box {
+  padding: 20px;
+  margin-bottom: 20px;
+  background-color: #fff;
+  border-radius: 6px;
+
+  &-head {
+    margin-bottom: 20px;
+    line-height: 32px;
+
+    &::after {
+      content: "";
+      display: block;
+      clear: both;
+      visibility: hidden;
+    }
+
+    &-left {
+      float: left;
+
+      > h1 {
+        color: #202b4b;
+        font-size: 18px;
+        font-weight: 600;
+        margin: 0;
+      }
+    }
+    &-right {
+      float: right;
+    }
+  }
+}
+.part {
+  &-filter {
+    margin-bottom: 20px;
+    background-color: #fff;
+    border-radius: 6px;
+
+    > div[class^="part-filter"]:nth-of-type(2) {
+      border-top: 1px solid #f0f4f9;
+    }
+    .line-split {
+      display: inline-block;
+      margin: 0 5px;
+      color: #ccd4e2;
+      font-weight: 600;
+    }
+
+    &-info {
+      display: flex;
+      align-items: stretch;
+      justify-content: space-between;
+      padding: 10px 20px;
+
+      &-sub {
+        padding-left: 20px;
+      }
+    }
+
+    &-form {
+      display: flex;
+      align-items: stretch;
+      justify-content: space-between;
+      padding: 20px 20px 10px;
+
+      &-action {
+        padding-bottom: 15px;
+        white-space: nowrap;
+        display: flex;
+        align-items: flex-end;
+      }
+
+      .el-form-item {
+        margin-bottom: 15px;
+      }
+      .el-form-item__label {
+        margin-bottom: 0;
+        display: none;
+      }
+      .el-button {
+        vertical-align: top;
+      }
+    }
+  }
+}
+
+.part-page {
+  margin-top: 20px;
+  text-align: right;
+  &::after {
+    content: "";
+    display: block;
+    clear: both;
+    visibility: hidden;
+  }
+
+  .page-info {
+    float: left;
+    line-height: 32px;
+  }
+  .el-pagination {
+    float: right;
+    padding: 0;
+  }
+}
+
+.clear-float {
+  &::after {
+    content: "";
+    display: block;
+    clear: both;
+    visibility: hidden;
+  }
+}
+
+/* table */
+.table {
+  width: 100%;
+  border-spacing: 0;
+  border-collapse: collapse;
+  text-align: center;
+  margin-bottom: 30px;
+}
+.table th {
+  padding: 10px;
+  line-height: 20px;
+  letter-spacing: 1px;
+  border: 1px solid $--border-color-light;
+}
+.table td {
+  padding: 10px;
+  line-height: 20px;
+  border: 1px solid $--border-color-light;
+}
+.table .td-th {
+  font-weight: 600;
+  color: $--color-text-primary;
+}
+
+/* list */
+.list-lr-right {
+  float: right;
+  width: 300px;
+}
+.list-lr-left {
+  margin-right: 320px;
+}
+
+/* user reset */
+h3.account-title {
+  text-align: center;
+  font-weight: 600;
+}
+.account-form {
+  width: 60%;
+  min-width: 600px;
+  margin: 50px auto;
+}
+.vlcode {
+  height: 36px;
+}
+.vlcode-left {
+  margin-right: 135px;
+}
+.vlcode-right {
+  float: right;
+  width: 120px;
+}
+.logo-image {
+  display: inline-block;
+  padding: 15px;
+  border-radius: 5px;
+  background-color: #f0f4f9;
+}
+.logo-view {
+  display: block;
+  max-width: 200px;
+  height: auto;
+}
+
+// vue-echarts
+.echarts {
+  width: 100% !important;
+}
+
+// other
+.tips-info {
+  font-size: 14px;
+  height: 25px;
+  line-height: 25px;
+  color: $--color-text-secondary;
+}
+.tips-error {
+  color: $--color-danger;
+}
+
+// color
+.color-primary {
+  color: $--color-primary !important;
+}
+.color-success {
+  color: $--color-success;
+}
+.color-warning {
+  color: $--color-warning;
+}
+.color-danger {
+  color: $--color-danger;
+}
+.color-info {
+  color: $--color-info;
+}
+.color-white {
+  color: #fff;
+}
+.md-1 {
+  margin-bottom: 5px;
+}
+.md-2 {
+  margin-bottom: 10px;
+}
+
+// other
+.flex-between {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.icon {
+  display: inline-block;
+  vertical-align: middle;
+  width: 16px;
+  height: 16px;
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+
+  &-base {
+    background-image: url(../images/icon-base.png);
+  }
+  &-password {
+    background-image: url(../images/icon-password.png);
+    width: 14px;
+    height: 14px;
+  }
+}

+ 322 - 0
src/assets/styles/element-ui-costom.scss

@@ -0,0 +1,322 @@
+// el-input
+.el-input {
+  // &.is-focus {
+  //   .el-input__inner {
+  //     border-color: $--color-primary !important;
+  //   }
+  // }
+  .el-input__inner {
+    border-radius: 6px;
+    border-color: #e8edf3;
+    background-color: #f0f4f9;
+  }
+}
+.el-input-group--append {
+  .el-input__inner {
+    border-radius: 6px 0 0 6px;
+  }
+  .el-input-group__append {
+    border-color: #e8edf3;
+  }
+}
+.el-input-number {
+  &.is-without-controls {
+    .el-input__inner {
+      padding: 0 12px;
+    }
+  }
+}
+// el-textarea
+.el-textarea {
+  .el-textarea__inner {
+    border-color: #e8edf3;
+    background-color: #f0f4f9;
+  }
+}
+
+// datepicker
+.el-date-editor.el-input__inner {
+  border-radius: 6px;
+  border-color: #e8edf3;
+  background-color: #f0f4f9;
+
+  .el-range-input {
+    background-color: #f0f4f9;
+  }
+}
+
+// .el-button
+.el-button {
+  border-radius: 6px;
+
+  .icon {
+    margin-right: 5px;
+    width: 13px;
+    height: 13px;
+  }
+  .icon-view {
+    height: 10px;
+  }
+  .icon-upload {
+    height: 12px;
+  }
+  span {
+    display: inline-block;
+    vertical-align: middle;
+    line-height: 1;
+  }
+}
+.el-button--small,
+.el-button--small.is-round {
+  padding: 8px 12px 9px;
+}
+
+// .el-table
+.el-table {
+  color: #626a82;
+  font-weight: 400;
+  background-color: #fff;
+  font-size: 14px;
+  border-radius: 6px;
+
+  &__header {
+    th {
+      color: #202b4b;
+      font-weight: bold;
+    }
+  }
+  tr.el-table__row {
+    border-top: 1px solid #f0f4f9;
+  }
+  td,
+  th {
+    padding-top: 16px;
+    padding-bottom: 16px;
+  }
+  // .el-table__row.row-danger {
+  //   color: $--color-danger;
+  // }
+
+  .cell {
+    .el-checkbox {
+      margin-bottom: 0;
+    }
+    .btn-table-icon {
+      padding: 5px 12px 6px;
+      font-size: 14px;
+      &.el-button--primary {
+        background-color: #5fc9fa;
+        border-color: #5fc9fa;
+
+        &:hover {
+          background-color: mix(#fff, #5fc9fa, 10%);
+          border-color: mix(#fff, #5fc9fa, 10%);
+        }
+      }
+    }
+  }
+}
+
+// el-pagination
+.el-pagination-li {
+  width: 32px;
+  height: 32px;
+  border-radius: 6px;
+  overflow: hidden;
+  background-color: #fff;
+}
+.el-pagination {
+  &.is-background {
+    .btn-prev,
+    .btn-next {
+      @extend .el-pagination-li;
+    }
+
+    .el-pager li {
+      color: #8c94ac;
+      margin: 0 4px;
+      line-height: 32px;
+
+      @extend .el-pagination-li;
+      &:not(.disabled).active {
+        color: #fff;
+        background-color: #5fc9fa;
+      }
+    }
+  }
+  span:not([class*="suffix"]) {
+    height: 32px;
+    line-height: 32px;
+  }
+  &__total {
+    color: #8c94ac;
+    margin: 0 16px 0 6px;
+  }
+  &__sizes {
+    color: #8c94ac;
+    background-color: #fff;
+    .el-input__inner {
+      background-color: #fff;
+      border-color: #fff;
+    }
+  }
+  &__jump {
+    margin-left: 6px;
+    color: #8c94ac;
+    .el-input__inner {
+      background-color: #fff;
+      border-color: #fff;
+      padding-left: 10px;
+      padding-right: 10px;
+    }
+  }
+  &__editor {
+    height: 32px;
+    &.el-input .el-input__inner {
+      height: 32px;
+    }
+  }
+}
+
+// el-dialog
+.el-dialog {
+  background: #ffffff;
+  border-radius: 10px;
+  color: #202b4b;
+  .el-dialog__header {
+    padding: 16px 20px;
+    border-bottom: 1px solid rgba(240, 244, 249, 1);
+  }
+  .el-dialog__body {
+    padding: 30px;
+  }
+  .el-dialog__footer {
+    .el-button {
+      width: 83px;
+    }
+  }
+}
+.fullscreen-dialog {
+  .el-dialog {
+    border-radius: 0;
+
+    .el-dialog__header {
+      position: absolute;
+      padding: 14px 20px;
+      height: 60px;
+      width: 100%;
+      background-color: #fff;
+      h3 {
+        font-size: 18px;
+        line-height: 30px;
+        margin: 0;
+      }
+    }
+    .el-dialog__body {
+      position: absolute;
+      top: 60px;
+      bottom: 0;
+      left: 0;
+      right: 0;
+      background-color: #e8edf3;
+      padding: 20px;
+      overflow: auto;
+    }
+    .el-dialog__footer {
+      display: none;
+    }
+  }
+}
+
+// el-message-box
+.el-message-box {
+  color: #202b4b;
+  border-radius: 10px;
+  .el-message-box__header {
+    padding: 16px 20px;
+    border-bottom: 1px solid rgba(240, 244, 249, 1);
+  }
+  .el-message-box__content {
+    padding: 30px;
+    min-height: 140px;
+  }
+  .el-message-box__btns {
+    .el-button {
+      width: 83px;
+    }
+  }
+  .el-message-box__message {
+    padding-left: 50px;
+    padding-top: 4px;
+    font-size: 16px;
+    color: #202b4b;
+    line-height: 22px;
+  }
+  .el-message-box__status {
+    font-size: 32px !important;
+    top: 0;
+    left: 0;
+    transform: none;
+  }
+}
+.el-message-box__error {
+  width: 540px;
+  .el-message-box__status {
+    color: #fe5863;
+  }
+}
+.el-message-box__warning {
+  .el-message-box__status {
+    color: #5fc9fa;
+  }
+}
+
+// el-tabs
+.el-tabs {
+  .el-tabs__header {
+    margin-bottom: 0;
+  }
+  .el-tabs__content {
+    background-color: #fff;
+    padding: 30px;
+  }
+
+  .el-tabs__item {
+    color: #626a82;
+    border: none;
+    background-color: #fff;
+    border-radius: 6px 6px 0 0;
+    margin-right: 12px;
+
+    &.is-active {
+      font-weight: 600;
+      color: #000;
+
+      &::before {
+        content: "";
+        display: inline-block;
+        vertical-align: middle;
+        width: 10px;
+        height: 10px;
+        background-color: $--color-primary;
+        border-radius: 50%;
+        margin-right: 5px;
+      }
+    }
+  }
+}
+.el-tabs--card > .el-tabs__header .el-tabs__nav {
+  border: none;
+}
+
+// .el-form
+.el-form {
+  &-item__label {
+    color: #202b4b;
+  }
+}
+.el-form--label-top {
+  .el-form-item__label {
+    line-height: 1;
+  }
+}

+ 184 - 0
src/assets/styles/home.scss

@@ -0,0 +1,184 @@
+/* home */
+.home {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  z-index: auto;
+  min-width: 1200px;
+}
+.home-body {
+  position: absolute;
+  left: 0;
+  top: 61px;
+  right: 0;
+  bottom: 0;
+  overflow: auto;
+  background: $--color-background;
+  z-index: 98;
+}
+.home-main {
+  position: relative;
+  padding: 20px 20px 61px 240px;
+  min-height: 100%;
+}
+
+/* view-footer */
+.home-footer {
+  position: absolute;
+  width: 100%;
+  height: 60px;
+  bottom: 0;
+  left: 0;
+  z-index: auto;
+  padding: 20px 0;
+  line-height: 20px;
+  color: $--color-text-secondary;
+  text-align: center;
+  font-size: 13px;
+}
+.home-footer a {
+  color: $--color-text-secondary;
+}
+.home-footer a:hover {
+  color: $--color-text-primary;
+}
+
+/* navs */
+.home-navs {
+  position: absolute;
+  width: 220px;
+  top: 61px;
+  left: 0;
+  bottom: 0;
+  z-index: 99;
+  overflow: auto;
+  background: #fff;
+  padding: 15px 0;
+  text-align: left;
+  transition: width 0.2s ease;
+  border-right: 1px solid $--border-color-light;
+}
+.nav-item {
+  overflow: hidden;
+}
+.nav-item-main {
+  padding: 15px 35px 15px 45px;
+  line-height: 20px;
+  min-height: 50px;
+  position: relative;
+  font-weight: 600;
+}
+.nav-item-icon {
+  display: block;
+  position: absolute;
+  width: 20px;
+  height: 20px;
+  top: 14px;
+  text-align: center;
+}
+.nav-item-icon-left {
+  left: 15px;
+  font-size: 18px;
+  transition: all 0.2s ease;
+}
+.nav-item-sublist {
+  overflow: hidden;
+  transition: height 0.2s linear;
+}
+
+.nav-item-sub {
+  line-height: 20px;
+  padding: 8px 5px 8px 45px;
+  cursor: pointer;
+  position: relative;
+}
+.nav-item-sub:hover {
+  color: $--color-primary;
+}
+.nav-item-sub-act {
+  font-weight: 600;
+  color: $--color-primary !important;
+}
+
+/* head */
+.home-header {
+  position: absolute;
+  width: 100%;
+  padding: 14px 0;
+  height: 60px;
+  top: 0;
+  left: 0;
+  z-index: 100;
+  background: #fff;
+  box-shadow: 0 0 1px 0 $--border-color-base;
+  line-height: 32px;
+}
+.head-logo {
+  width: 220px;
+  float: left;
+  color: $--color-text-primary;
+  height: 32px;
+  padding: 0 20px;
+  font-size: 20px;
+}
+.head-info {
+  float: left;
+  .el-breadcrumb {
+    line-height: 32px;
+  }
+  .el-breadcrumb__inner {
+    color: $--color-text-secondary;
+  }
+}
+
+.head-user {
+  padding-right: 20px;
+  float: right;
+  height: 32px;
+  line-height: 32px;
+  position: relative;
+  color: $--color-text-regular;
+  cursor: pointer;
+}
+.user-name {
+  display: inline-block;
+  vertical-align: top;
+  margin-left: 10px;
+  min-width: 60px;
+  max-width: 120px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-weight: 600;
+}
+.user-logout {
+  display: inline-block;
+  vertical-align: top;
+  margin-left: 10px;
+  font-size: 20px;
+  cursor: pointer;
+}
+.user-logout i {
+  vertical-align: middle;
+  margin-top: -3px;
+}
+.user-logout:hover {
+  color: $--color-danger;
+}
+
+// pages ---------------------->
+// modify-app
+.modify-app {
+  .el-form {
+    &-item__label {
+      line-height: 20px;
+      padding-top: 6px;
+      padding-bottom: 6px;
+    }
+  }
+  .el-input-number {
+    .el-input__inner {
+      text-align: left;
+    }
+  }
+}

+ 5 - 0
src/assets/styles/index.scss

@@ -0,0 +1,5 @@
+@import "./variables.scss";
+@import "./base.scss";
+@import "./home.scss";
+@import "./login.scss";
+@import "./element-ui-costom.scss";

+ 50 - 0
src/assets/styles/login.scss

@@ -0,0 +1,50 @@
+/* login */
+.login-home {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  z-index: 8;
+  background-image: url(../images/bg-home.png);
+  background-repeat: no-repeat;
+  background-size: cover;
+  overflow: auto;
+}
+
+.login-box {
+  position: absolute;
+  width: 390px;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  z-index: 9;
+
+  background-color: #fff;
+  box-shadow: 0px 25px 50px 0px rgba(0, 31, 208, 0.3);
+  overflow: hidden;
+  padding: 50px;
+}
+
+.login-title {
+  text-align: center;
+  margin-bottom: 26px;
+  h1 {
+    font-size: 16px;
+  }
+
+  img {
+    display: block;
+    width: 64px;
+    height: 64px;
+    margin: 0 auto;
+  }
+}
+.login-form {
+  .el-form-item:last-child {
+    margin-bottom: 0;
+  }
+  .el-input__prefix {
+    left: 9px;
+  }
+}

+ 31 - 0
src/assets/styles/variables.scss

@@ -0,0 +1,31 @@
+// color ------------------->
+$--color-text-primary: #303133 !default;
+$--color-text-regular: #606266 !default;
+$--color-text-secondary: #909399 !default;
+$--color-text-placeholder: #cccccc !default;
+$--border-color-base: #dcdfe6 !default;
+$--border-color-light: #e4e7ed !default;
+$--border-color-lighter: #ebeef5 !default;
+$--border-color-extra-light: #f2f6fc !default;
+// status
+$--color-primary: #409eff !default;
+$--color-success: #1cd1a1 !default;
+$--color-warning: #e6a23c !default;
+$--color-danger: #f56c6c !default;
+$--color-info: #909399 !default;
+// skin
+$--color-background: #f0f4f9;
+$--color-background-dark: #21252b;
+$--color-background-act-dark: #2c313a;
+// text
+$--color-text-base: #abb2bf;
+// shadow
+$--shadow-light: 0 0 1px rgba(0, 0, 0, 0.15) !default;
+
+// size ------------------->
+$--font-size-base: 14px !default;
+$--font-size-large: 18px !default;
+$--font-size-medium: 16px !default;
+
+$--font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
+  "Microsoft YaHei", "微软雅黑", Arial, sans-serif;

+ 103 - 0
src/components/UploadButton.vue

@@ -0,0 +1,103 @@
+<template>
+  <el-upload
+    class="upload-button"
+    :action="uploadUrl"
+    :max-size="maxSize"
+    :before-upload="handleBeforeUpload"
+    :show-file-list="false"
+    :disabled="disabled"
+    ref="UploadComp"
+  >
+    <el-button type="primary" :disabled="disabled" :loading="loading">{{
+      btnContent
+    }}</el-button>
+  </el-upload>
+</template>
+
+<script>
+import { fileMD5 } from "@/plugins/md5";
+
+export default {
+  name: "upload-button",
+  props: {
+    btnContent: {
+      type: String,
+      default: "选择"
+    },
+    format: {
+      type: Array,
+      default() {
+        return ["jpg", "png"];
+      }
+    },
+    maxSize: {
+      type: Number,
+      default: 20 * 1024 * 1024
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      uploadUrl: "",
+      res: {}
+    };
+  },
+  methods: {
+    checkFileFormat(fileType) {
+      const _file_format = fileType
+        .split(".")
+        .pop()
+        .toLocaleLowerCase();
+
+      if (!this.format.length) return true;
+
+      return this.format.some(
+        item => item.toLocaleLowerCase() === _file_format
+      );
+    },
+    async handleBeforeUpload(file) {
+      this.attachmentName = file.name;
+
+      if (file.size > this.maxSize) {
+        this.handleExceededSize();
+        return Promise.reject();
+      }
+
+      if (!this.checkFileFormat(file.name)) {
+        this.handleFormatError();
+        return Promise.reject();
+      }
+
+      const md5 = await fileMD5(file);
+      this.$emit("file-change", {
+        file,
+        md5
+      });
+      return false;
+    },
+    handleFormatError() {
+      const content = "只支持文件格式为" + this.format.join("/");
+      this.res = {
+        success: false,
+        message: content
+      };
+      this.loading = false;
+      this.$emit("upload-error", this.res);
+    },
+    handleExceededSize() {
+      const content =
+        "文件大小不能超过" + Math.floor(this.maxSize / 1024) + "M";
+      this.res = {
+        success: false,
+        message: content
+      };
+      this.loading = false;
+      this.$emit("upload-error", this.res);
+    }
+  }
+};
+</script>

+ 15 - 0
src/components/ViewFooter.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="view-footer home-footer">
+    <p>
+      Copyright ©
+      <a href="https://www.qmth.com.cn/" target="_blank">启明泰和</a>, All
+      Rights Reserved.
+    </p>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "view-footer"
+};
+</script>

+ 6 - 0
src/config.js

@@ -0,0 +1,6 @@
+export default {
+  domain: process.env.VUE_APP_DOMAIN || window.location.origin,
+  timeout: process.env.VUE_APP_TIMEOUT * 1,
+  pageSize: process.env.VUE_APP_PAGE_SIZE * 1,
+  authTimeout: process.env.VUE_APP_AUTH_TIMEOUT * 1
+};

+ 20 - 0
src/constants/app.js

@@ -0,0 +1,20 @@
+const MD5 = require("js-md5");
+
+// domain
+let domain;
+if (process.env.VUE_APP_SELF_DEFINE_DOMAIN === "true") {
+  domain = window.localStorage.getItem("domain_in_url");
+}
+if (!domain) domain = window.location.hostname.split(".")[0];
+export const ORG_CODE = domain;
+
+const ADMIN_CODE = "admin";
+
+export const IS_ADMIN_SYSTEM = ADMIN_CODE === ORG_CODE;
+
+export const PLATFORM = "WEB";
+
+if (!localStorage.getItem("deviceId")) {
+  localStorage.setItem("deviceId", MD5(Math.random() + "-" + Date.now()));
+}
+export const DEVICE_ID = localStorage.getItem("deviceId");

+ 6 - 0
src/constants/enumerate.js

@@ -0,0 +1,6 @@
+
+// 启用/禁用
+export const ABLE_TYPE = {
+  0: "禁用",
+  1: "启用"
+};

+ 19 - 0
src/constants/navs.js

@@ -0,0 +1,19 @@
+const navs = [
+  {
+    title: "管理",
+    icon: "icon-base",
+    showList: true,
+    children: [
+      {
+        title: "应用管理",
+        router: "AppManage"
+      },
+      {
+        title: "用户管理",
+        router: "UserManage"
+      },
+    ]
+  }
+];
+
+export default navs;

+ 30 - 0
src/main.js

@@ -0,0 +1,30 @@
+import Vue from "vue";
+import App from "./App.vue";
+import router from "./router";
+import store from "./store";
+import GLOBAL from "./config";
+import globalVuePlugins from "./plugins/globalVuePlugins";
+
+import "./plugins/axios";
+import "./plugins/filters";
+
+// https://github.com/RobinCK/vue-ls
+import VueLocalStorage from "vue-ls";
+import ElementUI from "element-ui";
+import "element-ui/lib/theme-chalk/index.css";
+import "./assets/styles/index.scss";
+import JsonViewr from "vue-json-viewer";
+
+Vue.use(ElementUI, { size: "small" });
+Vue.use(VueLocalStorage, { storage: "session" });
+Vue.use(globalVuePlugins);
+Vue.use(JsonViewr);
+
+Vue.prototype.GLOBAL = GLOBAL;
+Vue.config.productionTip = false;
+
+new Vue({
+  router,
+  store,
+  render: h => h(App)
+}).$mount("#app");

+ 128 - 0
src/modules/admin/api.js

@@ -0,0 +1,128 @@
+import { $get, $post, $postParam } from "@/plugins/axios";
+
+// common-select
+export const appTypeList = datas => {
+  return $get("/api/admin/app/types", datas);
+};
+export const appDeployList = datas => {
+  return $get("/api/admin/app/deploy_modes", datas);
+};
+
+// app-manage
+export const appQuery = datas => {
+  return $get("/api/admin/app/query", datas);
+};
+export const appInsertOrUpdate = datas => {
+  if (datas.id) {
+    return $post("/api/admin/app/update", datas);
+  } else {
+    return $post("/api/admin/app/insert", datas);
+  }
+};
+export const appBindOrg = datas => {
+  return $postParam("/api/admin/app/bind_org", datas);
+};
+export const appResetSecret = id => {
+  return $postParam("/api/admin/app/reset_secret", { id });
+};
+export const appControlKeys = () => {
+  return $get("/api/admin/app/control_keys", {});
+};
+export const appLicenseDownload = ({ appId, deviceId }) => {
+  return $postParam(
+    "/api/admin/app/license/download",
+    { appId, deviceId },
+    { responseType: "blob" }
+  );
+};
+// app-devive manage
+export const appDeviceList = appId => {
+  return $postParam("/api/admin/app/device/list", { appId });
+};
+export const appDeviceInfo = deviceId => {
+  return $postParam("/api/admin/app/device/info", { deviceId });
+};
+export const appDeviceSave = datas => {
+  let formData = new FormData();
+  Object.keys(datas).forEach(key => {
+    formData.append(key, datas[key]);
+  });
+  return $post("/api/admin/app/device/save", formData);
+};
+export const appDeviceDelete = ({ appId, deviceId }) => {
+  return $postParam("/api/admin/app/device/delete", { appId, deviceId });
+};
+
+// org-manage
+export const orgTypesList = datas => {
+  return $get("/api/admin/org/types", datas);
+};
+export const orgSubTypesList = datas => {
+  return $get("/api/admin/org/sub_types", datas);
+};
+export const orgQuery = datas => {
+  return $get("/api/admin/org/query", datas);
+};
+export const orgInsertOrUpdate = datas => {
+  let formData = new FormData();
+  Object.entries(datas).forEach(([key, val]) => {
+    // if (val === null || val === "null" || val === "") return;
+
+    if (key === "subTypes") {
+      if (val.length) {
+        val.forEach(type => formData.append("subTypes", type));
+      } else {
+        formData.append("subTypes", "");
+      }
+    } else {
+      formData.append(key, datas[key]);
+    }
+  });
+
+  if (datas.id) {
+    return $post("/api/admin/org/update", formData);
+  } else {
+    return $post("/api/admin/org/insert", formData);
+  }
+};
+// 启用/禁用
+export const orgToggle = datas => {
+  return $get("/api/admin/org/toggle", datas);
+};
+
+/**
+ * @description 微信小程序查询
+ * @params { string } id  // 应用ID
+ * @params { string } nameStartWith  // 名称前缀
+ * @params { string } pageNumber  // 页码
+ * @params { string } pageSize  // 数量
+ */
+export const getWeChatAppList = data => {
+  const params = Object.entries(data).reduce((p, [key, val]) => {
+    if (val) {
+      p[key] = val;
+    }
+    return p;
+  }, {});
+  return $post("/api/admin/wxapp/query", new URLSearchParams(params));
+};
+
+/**
+ * @description 微信小程序新增
+ * @params { string } id  // 应用ID
+ * @params { string } name  // 名称
+ * @params { string } secret  // 密钥
+ */
+export const insertWeChatApp = data => {
+  return $post("/api/admin/wxapp/insert", new URLSearchParams(data));
+};
+
+/**
+ * @description 微信小程序修改
+ * @params { string } id  // 应用ID
+ * @params { string } name  // 名称
+ * @params { string } secret  // 密钥
+ */
+export const updateWeChatApp = data => {
+  return $post("/api/admin/wxapp/update", new URLSearchParams(data));
+};

+ 222 - 0
src/modules/admin/components/ModifyApp.vue

@@ -0,0 +1,222 @@
+<template>
+  <el-dialog
+    class="modify-app"
+    :visible.sync="modalIsShow"
+    :title="title"
+    top="10px"
+    width="540px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @opened="visibleChange"
+  >
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :rules="rules"
+      :key="modalForm.id"
+      label-width="110px"
+    >
+      <el-form-item prop="name" label="名称">
+        <el-input
+          v-model.trim="modalForm.name"
+          placeholder="请输入名称"
+          clearable
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="type" label="分类">
+        <el-select
+          v-model="modalForm.type"
+          placeholder="选择应用分类"
+          clearable
+          style="width: 100%"
+        >
+          <el-option
+            v-for="item in datas.types"
+            :key="item.code"
+            :value="item.code"
+            :label="item.name"
+          >
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item prop="deploy" label="部署分类">
+        <el-select
+          v-model="modalForm.deploy"
+          placeholder="选择应用分类"
+          clearable
+          style="width: 100%"
+        >
+          <el-option
+            v-for="item in datas.deploys"
+            :key="item.code"
+            :value="item.code"
+            :label="item.name"
+          >
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="IP白名单">
+        <el-input
+          v-model.trim="modalForm.ipAllow"
+          type="textarea"
+          placeholder="请输入访问IP白名单"
+        ></el-input>
+      </el-form-item>
+
+      <template v-for="cont in datas.controlKeys">
+        <el-form-item :key="cont.code" :label="cont.name">
+          <app-control-key
+            v-model="modalForm.control[cont.code]"
+            :key-type="cont.type"
+          ></app-control-key>
+        </el-form-item>
+      </template>
+    </el-form>
+    <div class="flex-between" slot="footer">
+      <el-button type="danger" plain @click="toResetKey">重置密钥</el-button>
+      <div>
+        <el-button type="danger" @click="cancel" plain>取消</el-button>
+        <el-button type="primary" :disabled="isSubmit" @click="submit"
+          >确认</el-button
+        >
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { clearData } from "../../../plugins/utils";
+import { appInsertOrUpdate, appResetSecret } from "../api";
+import AppControlKey from "./AppControlKey";
+
+const initModalForm = {
+  id: "",
+  name: "",
+  type: "",
+  deploy: "",
+  ipAllow: "",
+  control: {}
+};
+
+export default {
+  name: "modify-app",
+  components: { AppControlKey },
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      }
+    },
+    datas: {
+      type: Object,
+      default() {
+        return {
+          types: [],
+          deploys: [],
+          controlKeys: []
+        };
+      }
+    }
+  },
+  computed: {
+    isEdit() {
+      return !!this.instance.id;
+    },
+    title() {
+      return (this.isEdit ? "编辑" : "新增") + "应用";
+    }
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { ...initModalForm },
+      rules: {
+        name: [
+          {
+            required: true,
+            message: "请输入应用名称",
+            trigger: "change"
+          }
+        ],
+        type: [
+          {
+            required: true,
+            message: "请选择分类",
+            trigger: "change"
+          }
+        ],
+        deploy: [
+          {
+            required: true,
+            message: "请选择部署分类",
+            trigger: "change"
+          }
+        ]
+      }
+    };
+  },
+  methods: {
+    initData(val) {
+      if (val.id) {
+        this.modalForm = this.$objAssign(initModalForm, val);
+      } else {
+        this.modalForm = { ...initModalForm };
+      }
+      const controlVals = val.control || {};
+      let control = {};
+      this.datas.controlKeys.forEach(cont => {
+        const defaultVal = cont.type === "number" ? undefined : null;
+        control[cont.code] =
+          controlVals[cont.code] || controlVals[cont.code] === 0
+            ? controlVals[cont.code]
+            : defaultVal;
+      });
+      this.modalForm.control = control;
+
+      this.$refs.modalFormComp.clearValidate();
+    },
+    visibleChange() {
+      this.initData(this.instance);
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    toResetKey() {
+      this.$confirm("确定要重置当前应用密钥吗?", "操作警告", {
+        type: "warning",
+        callback: async action => {
+          if (action !== "confirm") return;
+          await appResetSecret(this.instance.id);
+          this.$message.success("重置成功!");
+          this.$emit("modified");
+        }
+      });
+    },
+    async submit() {
+      const valid = await this.$refs.modalFormComp.validate();
+      if (!valid) return;
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      let datas = { ...this.modalForm };
+      datas.control = clearData(this.modalForm.control);
+      const data = await appInsertOrUpdate(datas).catch(() => {
+        this.isSubmit = false;
+      });
+
+      if (!data) return;
+
+      this.isSubmit = false;
+      this.$message.success(this.title + "成功!");
+      this.$emit("modified");
+      this.cancel();
+    }
+  }
+};
+</script>

+ 9 - 0
src/modules/admin/router.js

@@ -0,0 +1,9 @@
+import AppManage from "./views/AppManage.vue";
+
+export default [
+  {
+    path: "app-manage",
+    name: "AppManage",
+    component: AppManage
+  },
+];

+ 143 - 0
src/modules/admin/views/AppManage.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="app-manage">
+    <div class="part-filter">
+      <div class="part-filter-form">
+        <el-form
+          ref="FilterForm"
+          label-position="left"
+          label-width="80px"
+          inline
+        >
+          <el-form-item label="应用分类">
+            <el-select
+              v-model="filter.type"
+              placeholder="选择应用分类"
+              clearable
+            >
+              <el-option
+                v-for="item in appTypes"
+                :key="item.code"
+                :value="item.code"
+                :label="item.name"
+              >
+              </el-option>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="模糊查询">
+            <el-input
+              v-model="filter.nameStartWith"
+              placeholder="名称前缀"
+            ></el-input>
+          </el-form-item>
+          <el-form-item label-width="0px">
+            <el-button type="primary" icon="ios-search" @click="toPage(1)"
+              >查询</el-button
+            >
+            <el-button type="success" icon="md-add" @click="toAdd"
+              >新增</el-button
+            >
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+
+    <div class="part-box">
+      <el-table ref="TableList" :data="dataList" border>
+        <el-table-column prop="id" label="ID" width="80"></el-table-column>
+        <el-table-column prop="name" label="名称"> </el-table-column>
+        <el-table-column prop="typeName" label="分类"> </el-table-column>
+        <el-table-column prop="createTime" label="创建时间" width="160">
+          <span slot-scope="scope">{{
+            scope.row.createTime | timestampFilter
+          }}</span>
+        </el-table-column>
+        <el-table-column prop="updateTime" label="修改时间" width="160">
+          <span slot-scope="scope">{{
+            scope.row.updateTime | timestampFilter
+          }}</span>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="260">
+          <template slot-scope="scope">
+            <el-button size="mini" type="primary" @click="toEdit(scope.row)"
+              >编辑</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="part-page">
+        <el-pagination
+          background
+          layout="total,prev, pager, next"
+          :current-page="current"
+          :total="total"
+          :page-size="size"
+          @current-change="toPage"
+        >
+        </el-pagination>
+      </div>
+    </div>
+
+    <!-- modify-app -->
+    <ModifyApp
+      :datas="typeSources"
+      :instance="curRow"
+      @modified="getList"
+      ref="ModifyApp"
+    />
+  </div>
+</template>
+
+<script>
+import { CONTROL_KEYS } from "../../../constants/enumerate";
+import { appQuery, appResetSecret, appTypeList, appDeployList } from "../api";
+import ModifyApp from "../components/ModifyApp";
+
+export default {
+  name: "app-manage",
+  components: { ModifyApp },
+  data() {
+    return {
+      filter: {
+        nameStartWith: "",
+        type: ""
+      },
+      current: 1,
+      size: this.GLOBAL.pageSize,
+      total: 0,
+      dataList: [],
+      curRow: {},
+      loading: false
+    };
+  },
+  created() {
+    this.initData();
+  },
+  methods: {
+     initData() {
+      this.toPage(1);
+    },
+    async getList() {
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current,
+        pageSize: this.size
+      };
+      const data = await appQuery(datas);
+      this.dataList = data.records
+      this.total = data.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    toAdd() {
+      this.curRow = {};
+      this.$refs.ModifyApp.open();
+    },
+    toEdit(row) {
+      this.curRow = row;
+      this.$refs.ModifyApp.open();
+    },
+  }
+};
+</script>

+ 5 - 0
src/modules/login/api.js

@@ -0,0 +1,5 @@
+import { $postParam } from "@/plugins/axios";
+
+export const login = datas => {
+  return $postParam("/api/admin/login", datas);
+};

+ 13 - 0
src/modules/login/router.js

@@ -0,0 +1,13 @@
+import Login from "./views/Login";
+
+export default [
+  {
+    path: "/admin/login",
+    name: "Login",
+    component: Login,
+    meta: {
+      title: "登录",
+      noRequire: true
+    }
+  }
+];

+ 106 - 0
src/modules/login/views/Login.vue

@@ -0,0 +1,106 @@
+<template>
+  <div class="login-home">
+    <div class="login-box" @keyup.enter="submit('loginForm')">
+      <div class="login-title">
+        <img src="../../../assets/images/logo.png" alt="logo" />
+        <h1>运维管理中心</h1>
+      </div>
+      <div class="login-form">
+        <el-form
+          ref="loginForm"
+          :model="loginModel"
+          :rules="loginRules"
+          @submit.native.prevent
+        >
+          <el-form-item prop="password">
+            <el-input
+              type="password"
+              v-model.trim="loginModel.password"
+              placeholder="请输入密码"
+              clearable
+              autofocus
+            >
+              <i class="icon icon-password" slot="prefix"></i
+            ></el-input>
+          </el-form-item>
+          <el-form-item>
+            <el-button
+              style="width:100%;"
+              type="primary"
+              :disabled="isSubmit"
+              @click="submit('loginForm')"
+              >登录</el-button
+            >
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { login } from "../api";
+import { orgTypesList } from "../../../modules/admin/api";
+
+export default {
+  name: "login",
+  data() {
+    return {
+      loginModel: {
+        password: ""
+      },
+      loginRules: {
+        password: [
+          {
+            required: true,
+            message: "请输入密码",
+            trigger: "change"
+          }
+        ]
+      },
+      isSubmit: false
+    };
+  },
+  mounted() {
+    this.$ls.clear();
+  },
+  methods: {
+    async getOrgTypes() {
+      const types = await orgTypesList();
+      let orgTypeMap = {};
+      types.forEach(item => {
+        let kv = { name: item.name, subType: {} };
+        item.subTypes.forEach(elem => {
+          kv.subType[elem.code] = elem.name;
+        });
+        orgTypeMap[item.code] = kv;
+      });
+      this.$ls.set("orgTypes", types || []);
+      this.$ls.set("orgTypeMap", orgTypeMap);
+    },
+    async submit(name) {
+      const valid = await this.$refs[name].validate();
+      if (!valid) return;
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      const data = await login(this.loginModel).catch(() => {
+        this.isSubmit = false;
+      });
+      if (!data) return;
+
+      this.isSubmit = false;
+      if (!data.token) {
+        this.$message.error("用户数据错误!");
+        return;
+      }
+      this.$ls.set("token", data.token);
+      this.$ls.set("user", data);
+      await this.getOrgTypes();
+      this.$router.push({
+        name: "AppManage"
+      });
+    }
+  }
+};
+</script>

+ 45 - 0
src/plugins/VueCharts.js

@@ -0,0 +1,45 @@
+import Vue from "vue";
+import ECharts from "vue-echarts";
+import { use } from "echarts/core";
+
+// import ECharts modules manually to reduce bundle size
+// 按需模块:https://github.com/apache/echarts/blob/master/src/echarts.all.ts
+// render type
+import { CanvasRenderer } from "echarts/renderers";
+
+// charts
+import {
+  BarChart,
+  LineChart,
+  PieChart,
+  ScatterChart,
+  RadarChart,
+  BoxplotChart
+} from "echarts/charts";
+
+// component
+import {
+  TitleComponent,
+  LegendComponent,
+  DataZoomComponent,
+  MarkLineComponent,
+  TooltipComponent
+} from "echarts/components";
+
+use([
+  CanvasRenderer,
+  BarChart,
+  LineChart,
+  PieChart,
+  ScatterChart,
+  RadarChart,
+  BoxplotChart,
+  TitleComponent,
+  LegendComponent,
+  DataZoomComponent,
+  MarkLineComponent,
+  TooltipComponent
+]);
+
+// register component to use
+Vue.component("v-chart", ECharts);

+ 308 - 0
src/plugins/axios.js

@@ -0,0 +1,308 @@
+import axios from "axios";
+import qs from "qs";
+import Vue from "vue";
+import { Message, MessageBox, Notification } from "element-ui";
+import router from "../router";
+
+import GLOBAL from "../config";
+import { getAuthorization } from "./crypto";
+import { PLATFORM, DEVICE_ID } from "../constants/app";
+import { initSyncTime, fetchTime } from "./syncServerTime";
+
+// axios interceptors
+var load = "";
+// 同一时间有多个请求时,会形成队列。在第一个请求创建loading,在最后一个响应关闭loading
+var queue = [];
+// 设置延迟时效
+axios.defaults.timeout = GLOBAL.timeout;
+axios.interceptors.request.use(
+  config => {
+    // 显示loading提示
+    if (!queue.length) {
+      load = Message({
+        customClass: "el-message-loading",
+        iconClass: "el-message__icon el-icon-loading",
+        message: "Loading...",
+        duration: 0
+      });
+    }
+    queue.push(1);
+
+    // 为请求头添加鉴权信息
+    let token = Vue.ls.get("token");
+    if (token) {
+      // 新版鉴权
+      const sessionId = Vue.ls.get("user", { session: "" }).session;
+      const timestamp = fetchTime();
+      const Authorization = getAuthorization(
+        {
+          token: token,
+          timestamp,
+          account: sessionId,
+          uri: config.url.split("?")[0],
+          method: config.method
+        },
+        "token"
+      );
+      config.headers["Authorization"] = Authorization;
+      config.headers["time"] = timestamp;
+    }
+    config.headers["deviceId"] = DEVICE_ID;
+    config.headers["platform"] = PLATFORM;
+    config.headers["domain"] = window.location.origin;
+
+    return config;
+  },
+  error => {
+    // 关闭loading提示
+    // 串联并发请求,延时处理是为防止多个loading实例闪屏。
+    setTimeout(() => {
+      queue.shift();
+      if (!queue.length) load.close();
+    }, 100);
+    return Promise.reject(error);
+  }
+);
+axios.interceptors.response.use(
+  response => {
+    initSyncTime(new Date(response.headers.date).getTime());
+    // 关闭loading提示
+    setTimeout(() => {
+      queue.shift();
+      if (!queue.length) load.close();
+    }, 100);
+    return response;
+  },
+  error => {
+    initSyncTime(new Date(error.response.headers.date).getTime());
+    // 关闭loading提示
+    setTimeout(() => {
+      queue.shift();
+      if (!queue.length) load.close();
+    }, 100);
+    return Promise.reject(error);
+  }
+);
+
+// request instance
+// 防止鉴权失效之后多次弹窗。
+let unauthMsgBoxIsShow = false;
+
+const mdData = datas => {
+  let nData = {};
+  if (!datas) return nData;
+  Object.entries(datas).forEach(([key, val]) => {
+    if (val === null || val === "null" || val === "") return;
+    nData[key] = val;
+  });
+  return nData;
+};
+
+/**
+ * errorCallback 请求失败的回调
+ * @param {Object} error 请求失败时的错误信息
+ */
+const errorCallback = error => {
+  if (error.response) {
+    return errorDataCallback(error.response);
+  }
+
+  if (error.request) {
+    let message = "请求错误";
+    if (error.message.indexOf("timeout") > -1) {
+      message = "请求超时";
+    }
+    Notification.error({ title: "错误提示", message });
+  }
+
+  return error;
+};
+
+/**
+ * errorDataCallback 请求成功,结果有误的回调
+ * @param {Object} response Response信息
+ */
+const errorDataCallback = response => {
+  const error = response.data;
+  let message = error.message || error.error || "请求错误";
+  const unauthCodes = [401, 403];
+
+  if (unauthCodes.includes(response.status)) {
+    if (unauthMsgBoxIsShow) return error;
+    unauthMsgBoxIsShow = true;
+    message = "身份验证失效,请重新登录";
+    MessageBox.confirm(message, "重新登陆?", {
+      type: "warning",
+      cancelButtonClass: "el-button--danger is-plain",
+      confirmButtonClass: "el-button--primary",
+      closeOnClickModal: false,
+      closeOnPressEscape: false,
+      showClose: false,
+      callback: action => {
+        unauthMsgBoxIsShow = false;
+        if (action !== "confirm") return;
+        Vue.ls.clear();
+        router.push({ name: "Login" });
+      }
+    });
+  } else {
+    Notification.error({ title: "错误提示", message });
+  }
+
+  return error;
+};
+
+/**
+ * response format
+ *  {
+      config, header, data, request, status, statusText
+    }
+ * 
+ */
+
+/**
+ * successCallback 请求成功的回调
+ * @param {Object} data Response中的data信息
+ */
+const successCallback = data => {
+  return data;
+  // if (data.code === 200) {
+  //   return data.data;
+  // } else {
+  //   return Promise.reject(data.data);
+  // }
+};
+
+/**
+ * get请求
+ * @param {String} url 请求地址
+ * @param {Object} datas 请求数据
+ */
+const $get = (url, datas) => {
+  const sqDatas = qs.stringify(mdData(datas), {
+    arrayFormat: "repeat"
+  });
+  url += "?" + sqDatas;
+
+  return axios
+    .get(url)
+    .then(rep => {
+      return successCallback(rep.data);
+    })
+    .catch(error => {
+      return Promise.reject(errorCallback(error));
+    });
+};
+
+/**
+ * get请求
+ * @param {String} url 请求地址
+ * @param {Object} datas 请求数据
+ */
+const $postParam = (url, datas, config = {}) => {
+  const sqDatas = qs.stringify(datas, {
+    arrayFormat: "repeat"
+  });
+  if (sqDatas) url += "?" + sqDatas;
+
+  return axios
+    .post(url, {}, config)
+    .then(rep => {
+      if (config["responseType"] === "blob") return rep;
+
+      return successCallback(rep.data);
+    })
+    .catch(error => {
+      return Promise.reject(errorCallback(error));
+    });
+};
+
+/**
+ * post请求
+ * @param {String} url 请求地址
+ * @param {Object} datas 请求数据
+ */
+const $post = (url, datas, config = {}) => {
+  let sqDatas = {};
+  if (datas.constructor === Object) {
+    sqDatas = mdData(datas);
+  } else {
+    sqDatas = datas;
+  }
+
+  return axios
+    .post(url, sqDatas, config)
+    .then(rep => {
+      if (config["responseType"] === "blob") return rep;
+
+      return successCallback(rep.data);
+    })
+    .catch(error => {
+      return Promise.reject(errorCallback(error));
+    });
+};
+
+/**
+ * delete请求
+ * @param {String} url
+ * @param {Object} datas
+ */
+const $del = (url, datas) => {
+  let sqDatas = "";
+  if (datas) {
+    sqDatas = qs.stringify(datas, { arrayFormat: "repeat" });
+    url += "?" + sqDatas;
+  }
+  return axios
+    .delete(url)
+    .then(rep => {
+      return rep.data;
+    })
+    .catch(error => {
+      return Promise.reject(errorCallback(error));
+    });
+};
+
+/**
+ * put 请求
+ * @param {String} url 请求地址
+ * @param {Object} datas 请求数据
+ */
+const $put = (url, datas) => {
+  let sqDatas = "";
+  if (datas) {
+    sqDatas = qs.stringify(datas);
+  }
+
+  return axios
+    .put(url, sqDatas)
+    .then(rep => {
+      return rep.data;
+    })
+    .catch(error => {
+      return Promise.reject(errorCallback(error));
+    });
+};
+
+/**
+ * patch请求
+ * @param {String} url 请求地址
+ * @param {Object} datas 请求数据
+ */
+const $patch = (url, datas) => {
+  let sqDatas = "";
+  if (datas) {
+    sqDatas = qs.stringify(datas);
+  }
+
+  return axios
+    .patch(url, sqDatas)
+    .then(rep => {
+      return rep.data;
+    })
+    .catch(error => {
+      return Promise.reject(errorCallback(error));
+    });
+};
+
+export { $get, $postParam, $post, $del, $put, $patch };

+ 45 - 0
src/plugins/crypto.js

@@ -0,0 +1,45 @@
+const CryptoJS = require("crypto-js");
+
+export const Base64 = content => {
+  const words = CryptoJS.enc.Utf8.parse(content);
+  const base64Str = CryptoJS.enc.Base64.stringify(words);
+
+  return base64Str;
+};
+
+export const AES = content => {
+  const KEY = "1234567890123456";
+  const IV = "1234567890123456";
+
+  var key = CryptoJS.enc.Utf8.parse(KEY);
+  var iv = CryptoJS.enc.Utf8.parse(IV);
+  var encrypted = CryptoJS.AES.encrypt(content, key, { iv: iv });
+  return encrypted.toString();
+};
+
+/**
+ * 获取authorisation
+ * @param {Object} infos 相关信息
+ * @param {String} type 类别:secret、token两种
+ */
+export const getAuthorization = (infos, type) => {
+  // {type} {invoker}:base64(sha1(method&uri&timestamp&{secret}))
+  const timestamp = infos.timestamp;
+  if (type === "secret") {
+    // accessKey | method&uri&timestamp&accessSecret
+    const str = `${infos.method.toLowerCase()}&${infos.uri}&${timestamp}&${
+      infos.accessSecret
+    }`;
+    const sign = CryptoJS.enc.Base64.stringify(CryptoJS.SHA1(str));
+
+    return `Secret ${infos.accessKey}:${sign}`;
+  } else if (type === "token") {
+    // userId | method&uri&timestamp&token
+    const str = `${infos.method.toLowerCase()}&${infos.uri}&${timestamp}&${
+      infos.token
+    }`;
+    const sign = CryptoJS.enc.Base64.stringify(CryptoJS.SHA1(str));
+
+    return `Token ${infos.account}:${sign}`;
+  }
+};

+ 14 - 0
src/plugins/filters.js

@@ -0,0 +1,14 @@
+import Vue from "vue";
+import { formatDate } from "./utils";
+
+const DEFAULT_FIELD = "";
+
+Vue.filter("ableTypeFilter", function(val) {
+  return val ? "启用" : "禁用";
+});
+Vue.filter("defaultFieldFilter", function(val) {
+  return val === "" || val === null || val === undefined ? DEFAULT_FIELD : val;
+});
+Vue.filter("timestampFilter", function(val) {
+  return val ? formatDate("YYYY-MM-DD HH:mm:ss", new Date(val)) : DEFAULT_FIELD;
+});

+ 84 - 0
src/plugins/formRules.js

@@ -0,0 +1,84 @@
+// async-validator rules
+// to view at https://github.com/yiminghe/async-validator
+
+const username = [
+  {
+    required: true,
+    pattern: /^[a-zA-Z0-9][a-zA-Z0-9_]{2,19}$/,
+    message: "用户名必须以字母或数字开头,长度为3-20位,允许字母数字下划线",
+    trigger: "change"
+  }
+];
+
+const commonCode = ({ prop, min = 3, max = 20 }) => {
+  return [
+    {
+      required: true,
+      pattern: new RegExp(`^[a-zA-Z0-9_]{${min},${max}}$`),
+      message: `${prop}只能由数字、字母和下划线组成,长度${min}-${max}个字符`,
+      trigger: "change"
+    }
+  ];
+};
+
+const email = [
+  {
+    required: true,
+    type: "email",
+    message: "邮箱格式不正确",
+    trigger: "change"
+  }
+];
+
+const password = [
+  {
+    required: true,
+    pattern: /^[a-zA-Z0-9_]{6,20}$/,
+    message: "密码只能由数字、字母和下划线组成,长度6-20个字符",
+    trigger: "change"
+  }
+];
+
+const phone = [
+  {
+    required: true,
+    pattern: /^1\d{10}$/,
+    message: "请输入合适的手机号码",
+    trigger: "change"
+  }
+];
+
+const smscode = [
+  {
+    required: true,
+    pattern: /^[a-zA-Z0-9]{4}$/,
+    message: "请输入4位短信验证码",
+    trigger: "change"
+  }
+];
+
+const numberValidator = message => {
+  return [
+    {
+      required: true,
+      validator: (rule, value, callback) => {
+        if (!value && value !== 0) {
+          callback(new Error(message));
+        } else {
+          callback();
+        }
+      },
+      trigger: "change"
+    }
+  ];
+};
+
+export {
+  username,
+  commonCode,
+  email,
+  password,
+  phone,
+  smscode,
+  numberValidator
+};

+ 24 - 0
src/plugins/globalVuePlugins.js

@@ -0,0 +1,24 @@
+import { objAssign, randomCode, tableAction } from "@/plugins/utils";
+import globalMixins from "./mixins";
+import ViewFooter from "@/components/ViewFooter.vue";
+
+const components = {
+  ViewFooter
+};
+
+export default {
+  install: function(Vue) {
+    // 实例方法
+    Vue.prototype.$tableAction = tableAction;
+    Vue.prototype.$objAssign = objAssign;
+    Vue.prototype.$randomCode = randomCode;
+
+    // 注册全局组件
+    Object.keys(components).forEach(key => {
+      Vue.component(key, components[key]);
+    });
+
+    //全局 mixins
+    Vue.mixin(globalMixins);
+  }
+};

+ 23 - 0
src/plugins/md5.js

@@ -0,0 +1,23 @@
+const md5 = require("js-md5");
+
+/**
+ *
+ * @param {any} str 字符串
+ */
+export const MD5 = content => {
+  return md5(content);
+};
+
+export const fileMD5 = file => {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onloadend = function() {
+      const arrayBuffer = reader.result;
+      resolve(md5(arrayBuffer));
+    };
+    reader.onerror = function(err) {
+      reject(err);
+    };
+    reader.readAsArrayBuffer(file);
+  });
+};

+ 18 - 0
src/plugins/mixins.js

@@ -0,0 +1,18 @@
+export default {
+  methods: {
+    deletePageLastItem() {
+      let page = this.current || 1;
+      if (this.$refs.TableList.data.length === 1) {
+        page = page > 1 ? page - 1 : 1;
+      }
+      this.toPage && this.toPage(page);
+    },
+    goback() {
+      window.history.back();
+    },
+    getRouterPath(location) {
+      const { href } = this.$router.resolve(location);
+      return href;
+    }
+  }
+};

+ 37 - 0
src/plugins/syncServerTime.js

@@ -0,0 +1,37 @@
+/**
+ *
+ *
+ * 方案:
+ * 每次初始化服务端时间时记录本地时间与服务端时间差。
+ * 缺陷:在下次初始化服务端时间前,默认用户不会自己修改系统时间
+ * 弥补:在每次请求响应之后,修正时间差。
+ */
+
+let initLocalTime = null;
+let initServerTime = null;
+
+function getStorgeTime() {
+  const st = localStorage.getItem("st");
+  const unvalidVals = ["Infinity", "NaN", "null", "undefined"];
+  if (unvalidVals.includes(st + "")) {
+    return [Date.now(), Date.now()];
+  } else {
+    const [s, t] = st.split("_");
+    return [s * 1, t * 1];
+  }
+}
+
+const [serverTime, localTime] = getStorgeTime();
+initSyncTime(serverTime, localTime);
+
+function initSyncTime(serverTime, localTime = Date.now()) {
+  initLocalTime = localTime;
+  initServerTime = serverTime;
+  localStorage.setItem("st", `${initServerTime}_${initLocalTime}`);
+}
+
+function fetchTime() {
+  return Date.now() + initServerTime - initLocalTime;
+}
+
+export { initSyncTime, fetchTime };

+ 257 - 0
src/plugins/utils.js

@@ -0,0 +1,257 @@
+import { RegExp } from "core-js";
+
+const deepmerge = require("deepmerge");
+
+/**
+ * 判断对象类型
+ * @param {*} obj 对象
+ */
+export function objTypeOf(obj) {
+  const toString = Object.prototype.toString;
+  const map = {
+    "[object Boolean]": "boolean",
+    "[object Number]": "number",
+    "[object String]": "string",
+    "[object Function]": "function",
+    "[object Array]": "array",
+    "[object Date]": "date",
+    "[object RegExp]": "regExp",
+    "[object Undefined]": "undefined",
+    "[object Null]": "null",
+    "[object Object]": "object"
+  };
+  return map[toString.call(obj)];
+}
+
+/**
+ * 深拷贝
+ * @param {Object/Array} data 需要拷贝的数据
+ */
+export function deepCopy(data) {
+  const defObj = objTypeOf(data) === "array" ? [] : {};
+  return deepmerge(defObj, data, {
+    arrayMerge: (destinationArray, sourceArray, options) => sourceArray
+  });
+}
+
+/**
+ * 将目标对象中有的属性值与源对象中的属性值合并
+ * @param {Object} target 目标对象
+ * @param {Object} sources 源对象
+ */
+export function objAssign(target, sources) {
+  let targ = { ...target };
+  for (let k in targ) {
+    targ[k] = Object.prototype.hasOwnProperty.call(sources, k)
+      ? sources[k]
+      : targ[k];
+  }
+  return targ;
+}
+
+/**
+ * 文件流下载
+ * @param {Object} option 文件下载设置
+ */
+export function download(option) {
+  let defOpt = {
+    type: "get",
+    url: "",
+    data: "",
+    fileName: "",
+    header: ""
+  };
+  let opt = objAssign(defOpt, option);
+
+  return new Promise((resolve, reject) => {
+    let xhr = new XMLHttpRequest();
+    xhr.open(opt.type.toUpperCase(), opt.url, true);
+    xhr.responseType = "blob";
+
+    // header set
+    if (opt.header && objTypeOf(opt.header) === "object") {
+      for (let key in opt.header) {
+        xhr.setRequestHeader(key, opt.header[key]);
+      }
+    }
+
+    xhr.onload = function() {
+      if (this.readyState === 4 && this.status === 200) {
+        if (this.response.size < 1024) {
+          reject("文件不存在!");
+          return;
+        }
+
+        var blob = this.response;
+        let pdfUrl = "";
+        let uRl = window.URL || window.webkitURL;
+        if (uRl && uRl.createObjectURL) {
+          pdfUrl = uRl.createObjectURL(blob);
+        } else {
+          reject("浏览器不兼容!");
+        }
+        let a = document.createElement("a");
+        a.download = opt.fileName;
+        a.href = pdfUrl;
+        document.body.appendChild(a);
+        a.click();
+        a.parentNode.removeChild(a);
+        resolve();
+      } else {
+        reject("请求错误!");
+      }
+    };
+
+    if (opt.type.toUpperCase() === "POST") {
+      let fromData = new FormData();
+      for (let key in opt.data) {
+        fromData.append(key, opt.data[key]);
+      }
+      xhr.send(fromData);
+    } else {
+      xhr.send();
+    }
+  });
+}
+
+/**
+ * 文件流下载
+ * @param {Function} fetchFunc 下载程序,返回promise
+ * @param {String} fileName 保存的文件名
+ */
+export async function downloadBlob(fetchFunc, fileName) {
+  const res = await fetchFunc().catch(() => {});
+  if (!res) return;
+
+  let filename = fileName;
+  const contentDisposition = res.headers["content-disposition"] || "";
+  if (contentDisposition) {
+    let strs = contentDisposition.split(";");
+    strs
+      .map(item => item.split("="))
+      .find(item => {
+        if (item[0].indexOf("filename") !== -1) {
+          filename = item[1];
+        }
+      });
+  }
+
+  const blobUrl = URL.createObjectURL(new Blob([res.data]));
+  let a = document.createElement("a");
+  a.download = filename;
+  a.href = blobUrl;
+  document.body.appendChild(a);
+  a.click();
+  a.parentNode.removeChild(a);
+
+  return true;
+}
+
+/**
+ * 构建图表btn
+ * @param {Function} h createElement
+ * @param {Array} actions 操作分类数组
+ */
+export function tableAction(h, actions) {
+  return actions.map(item => {
+    let attr = {
+      props: {
+        type: item.type || "primary",
+        size: "small",
+        disabled: !!item.disabled
+      },
+      style: {
+        marginRight: "5px"
+      },
+      on: {
+        click: () => {
+          item.action();
+        }
+      }
+    };
+    return h("el-button", attr, item.name);
+  });
+}
+
+/**
+ * 获取随机code,默认获取16位
+ * @param {Number} len 推荐8的倍数
+ *
+ */
+export function randomCode(len = 16) {
+  if (len <= 0) return;
+  let steps = Math.ceil(len / 8);
+  let stepNums = [];
+  for (let i = 0; i < steps; i++) {
+    let ranNum = Math.random()
+      .toString(32)
+      .slice(-8);
+    stepNums.push(ranNum);
+  }
+
+  return stepNums.join("");
+}
+
+/**
+ * 序列化参数
+ * @param {Object} params 参数对象
+ */
+export function qsParams(params) {
+  return Object.entries(params)
+    .map(el => `${el[0]}=${el[1]}`)
+    .join("&");
+}
+
+/**
+ *
+ * @param {String} format 时间格式
+ * @param {Date} date 需要格式化的时间对象
+ */
+export function formatDate(format = "YYYY/MM/DD HH:mm:ss", date = new Date()) {
+  if (objTypeOf(date) !== "date") return;
+  const options = {
+    "Y+": date.getFullYear(),
+    "M+": date.getMonth() + 1,
+    "D+": date.getDate(),
+    "H+": date.getHours(),
+    "m+": date.getMinutes(),
+    "s+": date.getSeconds()
+  };
+  Object.entries(options).map(([key, val]) => {
+    if (new RegExp("(" + key + ")").test(format)) {
+      const zeros = key === "Y+" ? "0000" : "00";
+      const value = (zeros + val).substr(("" + val).length);
+      format = format.replace(RegExp.$1, value);
+    }
+  });
+  return format;
+}
+
+/**
+ * 获取本地时间,格式:年月日时分秒
+ */
+export function localNowDateTime() {
+  return formatDate("YYYY年MM月DD日HH时mm分ss秒");
+}
+
+/**
+ * 清除html标签
+ * @param {String} str html字符串
+ */
+export function removeHtmlTag(str) {
+  return str.replace(/<[^>]+>/g, "");
+}
+
+/**
+ *
+ * @param {Object} datas 数据
+ */
+export function clearData(datas) {
+  let nData = {};
+  if (!datas) return nData;
+  Object.entries(datas).forEach(([key, val]) => {
+    if (val === null || val === undefined || val === "") return;
+    nData[key] = val;
+  });
+  return nData;
+}

+ 67 - 0
src/router.js

@@ -0,0 +1,67 @@
+import Vue from "vue";
+import Router from "vue-router";
+
+import Home from "./views/Home.vue";
+import login from "./modules/login/router";
+// module-admin
+import admin from "./modules/admin/router";
+
+// ignore NavigationDuplicated. https://github.com/vuejs/vue-router/issues/2881
+const originalPush = Router.prototype.push;
+Router.prototype.push = function push(location, onResolve, onReject) {
+  if (onResolve || onReject)
+    return originalPush.call(this, location, onResolve, onReject);
+  try {
+    return originalPush.call(this, location).catch(err => err);
+  } catch (error) {
+    console.log(error);
+  }
+};
+// end ignore
+
+Vue.use(Router);
+
+let router = new Router({
+  routes: [
+    {
+      path: "/",
+      name: "Index",
+      redirect: { name: "Login" }
+    },
+    ...login,
+    {
+      path: "/admin",
+      name: "Home",
+      component: Home,
+      children: [...admin]
+    }
+    // [lazy-loaded] route level code-splitting
+    // {
+    //   path: "/about",
+    //   name: "about",
+    //   // this generates a separate chunk (about.[hash].js) for this route
+    //   // which is lazy-loaded when the route is visited.
+    //   component: () =>
+    //     import(/* webpackChunkName: "about" */ "./views/About.vue")
+    // }
+  ]
+});
+
+// route interceptor
+router.beforeEach((to, from, next) => {
+  const token = Vue.ls.get("token");
+  if (to.meta.noRequire) {
+    next();
+  } else {
+    // 需要登录的路由
+    if (token) {
+      next();
+    } else {
+      // 登录失效的处理
+      Vue.ls.clear();
+      next({ name: "Login" });
+    }
+  }
+});
+
+export default router;

+ 22 - 0
src/store.js

@@ -0,0 +1,22 @@
+import Vue from "vue";
+import Vuex from "vuex";
+
+Vue.use(Vuex);
+
+// modules
+// import account from "./modules/account/store";
+
+export default new Vuex.Store({
+  state: {
+    user: {}
+  },
+  mutations: {
+    setUser(state, user) {
+      state.user = user;
+    }
+  },
+  actions: {},
+  modules: {
+    // account
+  }
+});

+ 153 - 0
src/views/Home.vue

@@ -0,0 +1,153 @@
+<template>
+  <div class="home">
+    <div class="home-header">
+      <div class="head-logo">
+        <h1>运维管理中心</h1>
+      </div>
+      <div class="head-info">
+        <el-breadcrumb>
+          <el-breadcrumb-item :to="{ name: 'Home' }">
+            <i class="el-icon-s-home" style="margin-top: -4px;"></i>
+          </el-breadcrumb-item>
+          <el-breadcrumb-item
+            v-for="(bread, index) in breadcrumbs"
+            :key="index"
+            >{{ bread.title }}</el-breadcrumb-item
+          >
+        </el-breadcrumb>
+      </div>
+      <div class="head-user">
+        <span class="user-logout" @click="logout">
+          <i class="el-icon-switch-button"></i>
+        </span>
+      </div>
+    </div>
+
+    <div class="home-navs">
+      <ul>
+        <li class="nav-item" v-for="(nav, index) in navs" :key="index">
+          <div class="nav-item-main" @click="switchNav(nav, index)">
+            <span class="nav-item-icon nav-item-icon-left">
+              <i :class="['icon', `${nav.icon}`]"></i>
+            </span>
+            <p class="nav-item-cont">{{ nav.title }}</p>
+          </div>
+          <ul
+            class="nav-item-sublist"
+            v-if="nav.children && nav.children.length"
+          >
+            <li
+              v-for="(subnav, sindex) in nav.children"
+              :key="sindex"
+              :class="[
+                'nav-item-sub',
+                { 'nav-item-sub-act': curSub === index + '-' + sindex }
+              ]"
+              @click="toPage(index, sindex)"
+            >
+              {{ subnav.title }}
+            </li>
+          </ul>
+        </li>
+      </ul>
+    </div>
+
+    <div class="home-body">
+      <div class="home-main">
+        <router-view />
+
+        <view-footer></view-footer>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import navs from "@/constants/navs";
+
+export default {
+  name: "home",
+  data() {
+    return {
+      isCollapsed: false,
+      navs,
+      curMain: 0,
+      curSub: "",
+      avatar: "",
+      breadcrumbs: []
+    };
+  },
+  computed: {
+    rotateIcon() {
+      return [
+        "menu-icon",
+        this.isCollapsed ? "el-icon-s-unfold" : "el-icon-s-fold"
+      ];
+    }
+  },
+  watch: {
+    $route(val) {
+      this.actCurNav();
+    }
+  },
+  mounted() {
+    this.actCurNav();
+  },
+  methods: {
+    collapsedSider() {
+      this.isCollapsed = !this.isCollapsed;
+    },
+    switchNav(item, mainIndex) {
+      if (item.children) {
+        item.showList = !item.showList;
+      } else {
+        this.breadcrumbs = [{ title: item.title, router: item.router }];
+        this.curMain = mainIndex;
+        this.curSub = "";
+        this.$router.push({ name: item.router });
+      }
+    },
+    actCurNav() {
+      let router = this.$route.name;
+      this.navs.forEach((item, index) => {
+        if (item.children && item.children.length) {
+          item.children.forEach((elem, pindex) => {
+            if (elem.router === router) {
+              this.curSub = index + "-" + pindex;
+              this.curMain = index;
+              this.breadcrumbs = [
+                { title: item.title, router: item.router },
+                { title: elem.title, router: elem.router }
+              ];
+            }
+          });
+        } else {
+          if (item.router === router) {
+            this.curMain = index;
+            this.breadcrumbs = [{ title: item.title, router: item.router }];
+          }
+        }
+      });
+      this.navs[this.curMain].showList = true;
+    },
+    toPage(mainIndex, subIndex) {
+      const item = this.navs[mainIndex];
+      const elem = item.children[subIndex];
+      this.breadcrumbs = [
+        { title: item.title, router: item.router },
+        { title: elem.title, router: elem.router }
+      ];
+      this.curMain = mainIndex;
+      this.curSub = mainIndex + "-" + subIndex;
+
+      this.$router.push({
+        name: elem.router
+      });
+    },
+    logout() {
+      this.$ls.clear();
+      this.$router.push({ name: "Login" });
+    }
+  }
+};
+</script>

+ 34 - 0
vue.config.js

@@ -0,0 +1,34 @@
+var TerserPlugin = require("terser-webpack-plugin");
+var devProxy = {
+  "/api/": {
+    target: process.env.VUE_APP_DEV_PROXY,
+    changeOrigin: true
+  }
+};
+
+// 配置手册: https://cli.vuejs.org/zh/config/#vue-config-js
+var config = {
+  // publicPath: './',
+  devServer: {
+    port: 9012,
+    proxy: devProxy
+  },
+  chainWebpack: (config) => {
+    // webpack-chain配置手册:github.com/neutrinojs/webpack-chain#getting-started
+  },
+};
+
+// compress配置手册:https://github.com/mishoo/UglifyJS2/tree/harmony#compress-options
+if (process.env.NODE_ENV === "production") {
+  config.configureWebpack = {
+    optimization: {
+      minimizer: [
+        new TerserPlugin({
+          terserOptions: { compress: { drop_console: true } },
+        }),
+      ],
+    },
+  };
+}
+
+module.exports = config;