lrf402788946 %!s(int64=3) %!d(string=hai) anos
pai
achega
4cdb948e40

+ 3 - 0
.env

@@ -0,0 +1,3 @@
+VUE_APP_AXIOS_BASE_URL = ''
+VUE_APP_ROUTER="qgAdmin"
+VUE_APP_HOST="http://broadcast.waityou24.cn"

+ 33 - 0
.eslintrc.js

@@ -0,0 +1,33 @@
+// https://eslint.org/docs/user-guide/configuring
+
+module.exports = {
+  root: true,
+  env: {
+    node: true,
+  },
+  extends: ['plugin:vue/essential', '@vue/prettier'],
+  plugins: ['vue'],
+  rules: {
+    'max-len': [
+      'warn',
+      {
+        code: 10000,
+      },
+    ],
+    'no-unused-vars': 'off',
+    'no-console': 'off',
+    'prettier/prettier': [
+      'warn',
+      {
+        singleQuote: true,
+        trailingComma: 'es5',
+        bracketSpacing: true,
+        jsxBracketSameLine: true,
+        printWidth: 160,
+      },
+    ],
+  },
+  parserOptions: {
+    parser: 'babel-eslint',
+  },
+};

+ 1 - 1
.gitignore

@@ -1,7 +1,7 @@
 .DS_Store
 node_modules
 /dist
-
+package-lock.json
 
 # local env files
 .env.local

+ 294 - 15
package-lock.json

@@ -1061,11 +1061,19 @@
       "version": "7.14.6",
       "resolved": "https://registry.nlark.com/@babel/runtime/download/@babel/runtime-7.14.6.tgz",
       "integrity": "sha1-U1IDvAiS78fexgvcJ7Ls9uQJBi0=",
-      "dev": true,
       "requires": {
         "regenerator-runtime": "^0.13.4"
       }
     },
+    "@babel/runtime-corejs3": {
+      "version": "7.14.7",
+      "resolved": "https://registry.nlark.com/@babel/runtime-corejs3/download/@babel/runtime-corejs3-7.14.7.tgz?cache=0&sync_timestamp=1624313017816&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40babel%2Fruntime-corejs3%2Fdownload%2F%40babel%2Fruntime-corejs3-7.14.7.tgz",
+      "integrity": "sha1-DvKSu85AygD4dMlyTvF1oSR2Rlw=",
+      "requires": {
+        "core-js-pure": "^3.15.0",
+        "regenerator-runtime": "^0.13.4"
+      }
+    },
     "@babel/template": {
       "version": "7.14.5",
       "resolved": "https://registry.nlark.com/@babel/template/download/@babel/template-7.14.5.tgz?cache=0&sync_timestamp=1623280543555&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40babel%2Ftemplate%2Fdownload%2F%40babel%2Ftemplate-7.14.5.tgz",
@@ -1170,6 +1178,11 @@
       "integrity": "sha1-K1o6s/kYzKSKjHVMCBaOPwPrphs=",
       "dev": true
     },
+    "@popperjs/core": {
+      "version": "2.9.2",
+      "resolved": "https://registry.npm.taobao.org/@popperjs/core/download/@popperjs/core-2.9.2.tgz?cache=0&sync_timestamp=1617291042068&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40popperjs%2Fcore%2Fdownload%2F%40popperjs%2Fcore-2.9.2.tgz",
+      "integrity": "sha1-rep7aVPLs0ZRdmsFSEaOdDxqI1M="
+    },
     "@soda/friendly-errors-webpack-plugin": {
       "version": "1.8.0",
       "resolved": "https://registry.npm.taobao.org/@soda/friendly-errors-webpack-plugin/download/@soda/friendly-errors-webpack-plugin-1.8.0.tgz?cache=0&sync_timestamp=1607927406873&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40soda%2Ffriendly-errors-webpack-plugin%2Fdownload%2F%40soda%2Ffriendly-errors-webpack-plugin-1.8.0.tgz",
@@ -1458,11 +1471,23 @@
         }
       }
     },
+    "@vant/icons": {
+      "version": "1.6.0",
+      "resolved": "https://registry.nlark.com/@vant/icons/download/@vant/icons-1.6.0.tgz?cache=0&sync_timestamp=1621321922501&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40vant%2Ficons%2Fdownload%2F%40vant%2Ficons-1.6.0.tgz",
+      "integrity": "sha1-Pbfrf5Y/UaKghnZyDVr5xMNRL+s="
+    },
+    "@vant/popperjs": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npm.taobao.org/@vant/popperjs/download/@vant/popperjs-1.1.0.tgz?cache=0&sync_timestamp=1617714772966&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vant%2Fpopperjs%2Fdownload%2F%40vant%2Fpopperjs-1.1.0.tgz",
+      "integrity": "sha1-tO3uW7+m+xhwWYbjE9T9XxeUKg8=",
+      "requires": {
+        "@popperjs/core": "^2.9.2"
+      }
+    },
     "@vue/babel-helper-vue-jsx-merge-props": {
       "version": "1.2.1",
       "resolved": "https://registry.npm.taobao.org/@vue/babel-helper-vue-jsx-merge-props/download/@vue/babel-helper-vue-jsx-merge-props-1.2.1.tgz",
-      "integrity": "sha1-MWJKelBfsU2h1YAjclpMXycOaoE=",
-      "dev": true
+      "integrity": "sha1-MWJKelBfsU2h1YAjclpMXycOaoE="
     },
     "@vue/babel-helper-vue-transform-on": {
       "version": "1.0.2",
@@ -2330,6 +2355,14 @@
       "integrity": "sha1-3TeelPDbgxCwgpH51kwyCXZmF/0=",
       "dev": true
     },
+    "async-validator": {
+      "version": "1.8.5",
+      "resolved": "https://registry.nlark.com/async-validator/download/async-validator-1.8.5.tgz?cache=0&sync_timestamp=1619755974429&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fasync-validator%2Fdownload%2Fasync-validator-1.8.5.tgz",
+      "integrity": "sha1-3D4I7B/Q3dtn5ghC8CwM0c7G1/A=",
+      "requires": {
+        "babel-runtime": "6.x"
+      }
+    },
     "asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npm.taobao.org/asynckit/download/asynckit-0.4.0.tgz",
@@ -2369,6 +2402,14 @@
       "integrity": "sha1-1h9G2DslGSUOJ4Ta9bCUeai0HFk=",
       "dev": true
     },
+    "axios": {
+      "version": "0.21.1",
+      "resolved": "https://registry.nlark.com/axios/download/axios-0.21.1.tgz",
+      "integrity": "sha1-IlY0gZYvTWvemnbVFu8OXTwJsrg=",
+      "requires": {
+        "follow-redirects": "^1.10.0"
+      }
+    },
     "babel-eslint": {
       "version": "10.1.0",
       "resolved": "https://registry.nlark.com/babel-eslint/download/babel-eslint-10.1.0.tgz",
@@ -2383,6 +2424,11 @@
         "resolve": "^1.12.0"
       }
     },
+    "babel-helper-vue-jsx-merge-props": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npm.taobao.org/babel-helper-vue-jsx-merge-props/download/babel-helper-vue-jsx-merge-props-2.0.3.tgz",
+      "integrity": "sha1-Iq69OzOQIyjlEyk6jkmSs4T58bY="
+    },
     "babel-loader": {
       "version": "8.2.2",
       "resolved": "https://registry.npm.taobao.org/babel-loader/download/babel-loader-8.2.2.tgz",
@@ -2434,6 +2480,27 @@
         "@babel/helper-define-polyfill-provider": "^0.2.2"
       }
     },
+    "babel-runtime": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npm.taobao.org/babel-runtime/download/babel-runtime-6.26.0.tgz",
+      "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+      "requires": {
+        "core-js": "^2.4.0",
+        "regenerator-runtime": "^0.11.0"
+      },
+      "dependencies": {
+        "core-js": {
+          "version": "2.6.12",
+          "resolved": "https://registry.nlark.com/core-js/download/core-js-2.6.12.tgz?cache=0&sync_timestamp=1624966012065&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fcore-js%2Fdownload%2Fcore-js-2.6.12.tgz",
+          "integrity": "sha1-2TM9+nsGXjR8xWgiGdb2kIWcwuw="
+        },
+        "regenerator-runtime": {
+          "version": "0.11.1",
+          "resolved": "https://registry.npm.taobao.org/regenerator-runtime/download/regenerator-runtime-0.11.1.tgz",
+          "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk="
+        }
+      }
+    },
     "balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npm.taobao.org/balanced-match/download/balanced-match-1.0.2.tgz?cache=0&sync_timestamp=1617714298273&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbalanced-match%2Fdownload%2Fbalanced-match-1.0.2.tgz",
@@ -2793,6 +2860,11 @@
         "isarray": "^1.0.0"
       }
     },
+    "buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npm.taobao.org/buffer-equal-constant-time/download/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
+    },
     "buffer-from": {
       "version": "1.1.1",
       "resolved": "https://registry.npm.taobao.org/buffer-from/download/buffer-from-1.1.1.tgz",
@@ -3414,6 +3486,38 @@
         }
       }
     },
+    "compression-webpack-plugin": {
+      "version": "8.0.1",
+      "resolved": "https://registry.nlark.com/compression-webpack-plugin/download/compression-webpack-plugin-8.0.1.tgz?cache=0&sync_timestamp=1624626650884&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fcompression-webpack-plugin%2Fdownload%2Fcompression-webpack-plugin-8.0.1.tgz",
+      "integrity": "sha1-E7NEAwKXYOZgB/C6yM+JKiAJo/Q=",
+      "dev": true,
+      "requires": {
+        "schema-utils": "^3.0.0",
+        "serialize-javascript": "^6.0.0"
+      },
+      "dependencies": {
+        "schema-utils": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npm.taobao.org/schema-utils/download/schema-utils-3.0.0.tgz",
+          "integrity": "sha1-Z1AvaqK2ai1AMrQnmilEl4oJE+8=",
+          "dev": true,
+          "requires": {
+            "@types/json-schema": "^7.0.6",
+            "ajv": "^6.12.5",
+            "ajv-keywords": "^3.5.2"
+          }
+        },
+        "serialize-javascript": {
+          "version": "6.0.0",
+          "resolved": "https://registry.nlark.com/serialize-javascript/download/serialize-javascript-6.0.0.tgz",
+          "integrity": "sha1-765diPRdeSQUHai1w6en5mP+/rg=",
+          "dev": true,
+          "requires": {
+            "randombytes": "^2.1.0"
+          }
+        }
+      }
+    },
     "concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npm.taobao.org/concat-map/download/concat-map-0.0.1.tgz",
@@ -3705,6 +3809,11 @@
         }
       }
     },
+    "core-js-pure": {
+      "version": "3.15.2",
+      "resolved": "https://registry.nlark.com/core-js-pure/download/core-js-pure-3.15.2.tgz",
+      "integrity": "sha1-yOCHSCJwXzOF0xl6+TSPfJri484="
+    },
     "core-util-is": {
       "version": "1.0.2",
       "resolved": "https://registry.npm.taobao.org/core-util-is/download/core-util-is-1.0.2.tgz",
@@ -4088,8 +4197,7 @@
     "deepmerge": {
       "version": "1.5.2",
       "resolved": "https://registry.npm.taobao.org/deepmerge/download/deepmerge-1.5.2.tgz?cache=0&sync_timestamp=1606805746825&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdeepmerge%2Fdownload%2Fdeepmerge-1.5.2.tgz",
-      "integrity": "sha1-EEmdhohEza1P7ghC34x/bwyVp1M=",
-      "dev": true
+      "integrity": "sha1-EEmdhohEza1P7ghC34x/bwyVp1M="
     },
     "default-gateway": {
       "version": "5.0.5",
@@ -4520,6 +4628,14 @@
         "safer-buffer": "^2.1.0"
       }
     },
+    "ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npm.taobao.org/ecdsa-sig-formatter/download/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha1-rg8PothQRe8UqBfao86azQSJ5b8=",
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
     "ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npm.taobao.org/ee-first/download/ee-first-1.1.1.tgz",
@@ -4538,6 +4654,19 @@
       "integrity": "sha1-P6TjvL2lObUOOqIwQWJwY6XP/mE=",
       "dev": true
     },
+    "element-ui": {
+      "version": "2.15.3",
+      "resolved": "https://registry.nlark.com/element-ui/download/element-ui-2.15.3.tgz?cache=0&sync_timestamp=1624954513817&other_urls=https%3A%2F%2Fregistry.nlark.com%2Felement-ui%2Fdownload%2Felement-ui-2.15.3.tgz",
+      "integrity": "sha1-VRCKuCo7zGRuewVwhxxIupYwBlI=",
+      "requires": {
+        "async-validator": "~1.8.1",
+        "babel-helper-vue-jsx-merge-props": "^2.0.0",
+        "deepmerge": "^1.2.0",
+        "normalize-wheel": "^1.0.1",
+        "resize-observer-polyfill": "^1.5.0",
+        "throttle-debounce": "^1.0.1"
+      }
+    },
     "elliptic": {
       "version": "6.5.4",
       "resolved": "https://registry.npm.taobao.org/elliptic/download/elliptic-6.5.4.tgz?cache=0&sync_timestamp=1612290836352&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Felliptic%2Fdownload%2Felliptic-6.5.4.tgz",
@@ -5452,8 +5581,7 @@
     "follow-redirects": {
       "version": "1.14.1",
       "resolved": "https://registry.nlark.com/follow-redirects/download/follow-redirects-1.14.1.tgz?cache=0&sync_timestamp=1620555246888&other_urls=https%3A%2F%2Fregistry.nlark.com%2Ffollow-redirects%2Fdownload%2Ffollow-redirects-1.14.1.tgz",
-      "integrity": "sha1-2RFN7Qoc/dM04WTmZirQK/2R/0M=",
-      "dev": true
+      "integrity": "sha1-2RFN7Qoc/dM04WTmZirQK/2R/0M="
     },
     "for-in": {
       "version": "1.0.2",
@@ -6899,6 +7027,30 @@
         "graceful-fs": "^4.1.6"
       }
     },
+    "jsonwebtoken": {
+      "version": "8.5.1",
+      "resolved": "https://registry.npm.taobao.org/jsonwebtoken/download/jsonwebtoken-8.5.1.tgz",
+      "integrity": "sha1-AOceC431TCEhofJhN98igGc7zA0=",
+      "requires": {
+        "jws": "^3.2.2",
+        "lodash.includes": "^4.3.0",
+        "lodash.isboolean": "^3.0.3",
+        "lodash.isinteger": "^4.0.4",
+        "lodash.isnumber": "^3.0.3",
+        "lodash.isplainobject": "^4.0.6",
+        "lodash.isstring": "^4.0.1",
+        "lodash.once": "^4.0.0",
+        "ms": "^2.1.1",
+        "semver": "^5.6.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.7.1",
+          "resolved": "https://registry.nlark.com/semver/download/semver-5.7.1.tgz?cache=0&sync_timestamp=1618847017123&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsemver%2Fdownload%2Fsemver-5.7.1.tgz",
+          "integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc="
+        }
+      }
+    },
     "jsprim": {
       "version": "1.4.1",
       "resolved": "https://registry.npm.taobao.org/jsprim/download/jsprim-1.4.1.tgz",
@@ -6911,6 +7063,25 @@
         "verror": "1.10.0"
       }
     },
+    "jwa": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npm.taobao.org/jwa/download/jwa-1.4.1.tgz",
+      "integrity": "sha1-dDwymFy56YZVUw1TZBtmyGRbA5o=",
+      "requires": {
+        "buffer-equal-constant-time": "1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "jws": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npm.taobao.org/jws/download/jws-3.2.2.tgz?cache=0&sync_timestamp=1610085129426&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fjws%2Fdownload%2Fjws-3.2.2.tgz",
+      "integrity": "sha1-ABCZ82OUaMlBQADpmZX6UvtHgwQ=",
+      "requires": {
+        "jwa": "^1.4.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
     "killable": {
       "version": "1.0.1",
       "resolved": "https://registry.npm.taobao.org/killable/download/killable-1.0.1.tgz",
@@ -7119,8 +7290,7 @@
     "lodash": {
       "version": "4.17.21",
       "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.21.tgz",
-      "integrity": "sha1-Z5WRxWTDv/quhFTPCz3zcMPWkRw=",
-      "dev": true
+      "integrity": "sha1-Z5WRxWTDv/quhFTPCz3zcMPWkRw="
     },
     "lodash.debounce": {
       "version": "4.0.8",
@@ -7134,6 +7304,36 @@
       "integrity": "sha1-US6b1yHSctlOPTpjZT+hdRZ0HKY=",
       "dev": true
     },
+    "lodash.includes": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npm.taobao.org/lodash.includes/download/lodash.includes-4.3.0.tgz",
+      "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
+    },
+    "lodash.isboolean": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npm.taobao.org/lodash.isboolean/download/lodash.isboolean-3.0.3.tgz",
+      "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
+    },
+    "lodash.isinteger": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npm.taobao.org/lodash.isinteger/download/lodash.isinteger-4.0.4.tgz",
+      "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
+    },
+    "lodash.isnumber": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npm.taobao.org/lodash.isnumber/download/lodash.isnumber-3.0.3.tgz",
+      "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
+    },
+    "lodash.isplainobject": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npm.taobao.org/lodash.isplainobject/download/lodash.isplainobject-4.0.6.tgz",
+      "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
+    },
+    "lodash.isstring": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npm.taobao.org/lodash.isstring/download/lodash.isstring-4.0.1.tgz",
+      "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
+    },
     "lodash.kebabcase": {
       "version": "4.1.1",
       "resolved": "https://registry.npm.taobao.org/lodash.kebabcase/download/lodash.kebabcase-4.1.1.tgz",
@@ -7152,6 +7352,11 @@
       "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
       "dev": true
     },
+    "lodash.once": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npm.taobao.org/lodash.once/download/lodash.once-4.1.1.tgz",
+      "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
+    },
     "lodash.transform": {
       "version": "4.6.0",
       "resolved": "https://registry.npm.taobao.org/lodash.transform/download/lodash.transform-4.6.0.tgz",
@@ -7487,6 +7692,11 @@
         "minimist": "^1.2.5"
       }
     },
+    "moment": {
+      "version": "2.29.1",
+      "resolved": "https://registry.npm.taobao.org/moment/download/moment-2.29.1.tgz",
+      "integrity": "sha1-sr52n6MZQL6e7qZGnAdeNQBvo9M="
+    },
     "move-concurrently": {
       "version": "1.0.1",
       "resolved": "https://registry.npm.taobao.org/move-concurrently/download/move-concurrently-1.0.1.tgz",
@@ -7504,8 +7714,7 @@
     "ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.1.2.tgz?cache=0&sync_timestamp=1607433856030&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.1.2.tgz",
-      "integrity": "sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk=",
-      "dev": true
+      "integrity": "sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk="
     },
     "multicast-dns": {
       "version": "6.2.3",
@@ -7540,6 +7749,14 @@
         "thenify-all": "^1.0.0"
       }
     },
+    "naf-core": {
+      "version": "0.1.2",
+      "resolved": "https://registry.nlark.com/naf-core/download/naf-core-0.1.2.tgz",
+      "integrity": "sha1-0UetT3+BTsnSvYGPWCOVHgWAsJU=",
+      "requires": {
+        "lodash": "^4.17.11"
+      }
+    },
     "nan": {
       "version": "2.14.2",
       "resolved": "https://registry.npm.taobao.org/nan/download/nan-2.14.2.tgz",
@@ -7706,6 +7923,11 @@
       "integrity": "sha1-suHE3E98bVd0PfczpPWXjRhlBVk=",
       "dev": true
     },
+    "normalize-wheel": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npm.taobao.org/normalize-wheel/download/normalize-wheel-1.0.1.tgz",
+      "integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU="
+    },
     "npm-run-path": {
       "version": "2.0.2",
       "resolved": "https://registry.npm.taobao.org/npm-run-path/download/npm-run-path-2.0.2.tgz",
@@ -9145,8 +9367,7 @@
     "regenerator-runtime": {
       "version": "0.13.7",
       "resolved": "https://registry.npm.taobao.org/regenerator-runtime/download/regenerator-runtime-0.13.7.tgz",
-      "integrity": "sha1-ysLazIoepnX+qrrriugziYrkb1U=",
-      "dev": true
+      "integrity": "sha1-ysLazIoepnX+qrrriugziYrkb1U="
     },
     "regenerator-transform": {
       "version": "0.14.5",
@@ -9376,6 +9597,11 @@
       "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
       "dev": true
     },
+    "resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npm.taobao.org/resize-observer-polyfill/download/resize-observer-polyfill-1.5.1.tgz",
+      "integrity": "sha1-DpAg3T0hAkRY1OvSfiPkAmmBBGQ="
+    },
     "resolve": {
       "version": "1.20.0",
       "resolved": "https://registry.npm.taobao.org/resolve/download/resolve-1.20.0.tgz",
@@ -9487,8 +9713,7 @@
     "safe-buffer": {
       "version": "5.1.2",
       "resolved": "https://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.1.2.tgz",
-      "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=",
-      "dev": true
+      "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0="
     },
     "safe-regex": {
       "version": "1.1.0",
@@ -10566,6 +10791,11 @@
         "neo-async": "^2.6.0"
       }
     },
+    "throttle-debounce": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npm.taobao.org/throttle-debounce/download/throttle-debounce-1.1.0.tgz",
+      "integrity": "sha1-UYU9o3vmihVctugns1FKPEIuic0="
+    },
     "through": {
       "version": "2.3.8",
       "resolved": "https://registry.npm.taobao.org/through/download/through-2.3.8.tgz",
@@ -11063,6 +11293,18 @@
         "spdx-expression-parse": "^3.0.0"
       }
     },
+    "vant": {
+      "version": "2.12.22",
+      "resolved": "https://registry.nlark.com/vant/download/vant-2.12.22.tgz?cache=0&sync_timestamp=1624795564949&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fvant%2Fdownload%2Fvant-2.12.22.tgz",
+      "integrity": "sha1-ZbhMivD1mY8xM99KNkxXJhUotQ0=",
+      "requires": {
+        "@babel/runtime": "7.x",
+        "@vant/icons": "^1.5.3",
+        "@vant/popperjs": "^1.0.0",
+        "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
+        "vue-lazyload": "1.2.3"
+      }
+    },
     "vary": {
       "version": "1.1.2",
       "resolved": "https://registry.npm.taobao.org/vary/download/vary-1.1.2.tgz",
@@ -11129,6 +11371,11 @@
       "integrity": "sha1-UylVzB6yCKPZkLOp+acFdGV+CPI=",
       "dev": true
     },
+    "vue-lazyload": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npm.taobao.org/vue-lazyload/download/vue-lazyload-1.2.3.tgz",
+      "integrity": "sha1-kB+ewVx+bKeHgaK65KNDaGve2yw="
+    },
     "vue-loader": {
       "version": "15.9.7",
       "resolved": "https://registry.nlark.com/vue-loader/download/vue-loader-15.9.7.tgz?cache=0&sync_timestamp=1624996813170&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fvue-loader%2Fdownload%2Fvue-loader-15.9.7.tgz",
@@ -11231,6 +11478,21 @@
         }
       }
     },
+    "vue-meta": {
+      "version": "2.4.0",
+      "resolved": "https://registry.nlark.com/vue-meta/download/vue-meta-2.4.0.tgz?cache=0&sync_timestamp=1623026709800&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fvue-meta%2Fdownload%2Fvue-meta-2.4.0.tgz",
+      "integrity": "sha1-pBn7S0E1zpZdqzLsZB0ZicLuSEU=",
+      "requires": {
+        "deepmerge": "^4.2.2"
+      },
+      "dependencies": {
+        "deepmerge": {
+          "version": "4.2.2",
+          "resolved": "https://registry.npm.taobao.org/deepmerge/download/deepmerge-4.2.2.tgz?cache=0&sync_timestamp=1606805746825&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdeepmerge%2Fdownload%2Fdeepmerge-4.2.2.tgz",
+          "integrity": "sha1-RNLqNnm49NT/ujPwPYZfwee/SVU="
+        }
+      }
+    },
     "vue-router": {
       "version": "3.5.2",
       "resolved": "https://registry.nlark.com/vue-router/download/vue-router-3.5.2.tgz?cache=0&sync_timestamp=1624286995690&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fvue-router%2Fdownload%2Fvue-router-3.5.2.tgz",
@@ -11275,6 +11537,23 @@
       "resolved": "https://registry.nlark.com/vuex/download/vuex-3.6.2.tgz",
       "integrity": "sha1-I2vAhqhww655lG8QfxbeWdWJXnE="
     },
+    "wangeditor": {
+      "version": "4.7.4",
+      "resolved": "https://registry.nlark.com/wangeditor/download/wangeditor-4.7.4.tgz",
+      "integrity": "sha1-gq7IRPUg0tYewi2avpgiwS+UDxY=",
+      "requires": {
+        "@babel/runtime": "^7.11.2",
+        "@babel/runtime-corejs3": "^7.11.2",
+        "tslib": "^2.1.0"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.3.0",
+          "resolved": "https://registry.nlark.com/tslib/download/tslib-2.3.0.tgz",
+          "integrity": "sha1-gDuM2rPhK6WBpMpByIObuw2ssJ4="
+        }
+      }
+    },
     "watchpack": {
       "version": "1.7.5",
       "resolved": "https://registry.nlark.com/watchpack/download/watchpack-1.7.5.tgz",

+ 11 - 1
package.json

@@ -8,10 +8,19 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
+    "axios": "^0.21.1",
     "core-js": "^3.6.5",
+    "element-ui": "^2.15.2",
+    "jsonwebtoken": "^8.5.1",
+    "lodash": "^4.17.21",
+    "moment": "^2.29.1",
+    "naf-core": "^0.1.2",
+    "vant": "^2.12.22",
     "vue": "^2.6.11",
+    "vue-meta": "^2.4.0",
     "vue-router": "^3.2.0",
-    "vuex": "^3.4.0"
+    "vuex": "^3.4.0",
+    "wangeditor": "^4.7.4"
   },
   "devDependencies": {
     "@vue/cli-plugin-babel": "~4.5.0",
@@ -21,6 +30,7 @@
     "@vue/cli-service": "~4.5.0",
     "@vue/eslint-config-prettier": "^6.0.0",
     "babel-eslint": "^10.1.0",
+    "compression-webpack-plugin": "^8.0.0",
     "eslint": "^6.7.2",
     "eslint-plugin-prettier": "^3.3.1",
     "eslint-plugin-vue": "^6.2.2",

+ 10 - 6
public/index.html

@@ -1,15 +1,19 @@
 <!DOCTYPE html>
 <html lang="">
   <head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width,initial-scale=1.0">
-    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
     <title><%= htmlWebpackPlugin.options.title %></title>
   </head>
-  <body>
+  <body style="padding: 0; margin: 0">
     <noscript>
-      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+      <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 -->

+ 18 - 27
src/App.vue

@@ -1,32 +1,23 @@
 <template>
-  <div id="app">
-    <div id="nav">
-      <router-link to="/">Home</router-link> |
-      <router-link to="/about">About</router-link>
-    </div>
-    <router-view />
-  </div>
+  <router-view />
 </template>
 
-<style lang="less">
-#app {
-  font-family: Avenir, Helvetica, Arial, sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  text-align: center;
-  color: #2c3e50;
-}
+<script>
+import { mapMutations, createNamespacedHelpers } from 'vuex';
 
-#nav {
-  padding: 30px;
+export default {
+  name: 'App',
 
-  a {
-    font-weight: bold;
-    color: #2c3e50;
-
-    &.router-link-exact-active {
-      color: #42b983;
-    }
-  }
-}
-</style>
+  data: () => ({
+    //
+  }),
+  methods: {
+    ...mapMutations(['checkMobile']),
+  },
+  mounted() {
+    window.addEventListener('resize', () => {
+      this.checkMobile();
+    });
+  },
+};
+</script>

+ 0 - 130
src/components/HelloWorld.vue

@@ -1,130 +0,0 @@
-<template>
-  <div class="hello">
-    <h1>{{ msg }}</h1>
-    <p>
-      For a guide and recipes on how to configure / customize this project,<br />
-      check out the
-      <a href="https://cli.vuejs.org" target="_blank" rel="noopener"
-        >vue-cli documentation</a
-      >.
-    </p>
-    <h3>Installed CLI Plugins</h3>
-    <ul>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
-          target="_blank"
-          rel="noopener"
-          >babel</a
-        >
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router"
-          target="_blank"
-          rel="noopener"
-          >router</a
-        >
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex"
-          target="_blank"
-          rel="noopener"
-          >vuex</a
-        >
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
-          target="_blank"
-          rel="noopener"
-          >eslint</a
-        >
-      </li>
-    </ul>
-    <h3>Essential Links</h3>
-    <ul>
-      <li>
-        <a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
-      </li>
-      <li>
-        <a href="https://forum.vuejs.org" target="_blank" rel="noopener"
-          >Forum</a
-        >
-      </li>
-      <li>
-        <a href="https://chat.vuejs.org" target="_blank" rel="noopener"
-          >Community Chat</a
-        >
-      </li>
-      <li>
-        <a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
-          >Twitter</a
-        >
-      </li>
-      <li>
-        <a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
-      </li>
-    </ul>
-    <h3>Ecosystem</h3>
-    <ul>
-      <li>
-        <a href="https://router.vuejs.org" target="_blank" rel="noopener"
-          >vue-router</a
-        >
-      </li>
-      <li>
-        <a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-devtools#vue-devtools"
-          target="_blank"
-          rel="noopener"
-          >vue-devtools</a
-        >
-      </li>
-      <li>
-        <a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
-          >vue-loader</a
-        >
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/awesome-vue"
-          target="_blank"
-          rel="noopener"
-          >awesome-vue</a
-        >
-      </li>
-    </ul>
-  </div>
-</template>
-
-<script>
-export default {
-  name: "HelloWorld",
-  props: {
-    msg: String,
-  },
-};
-</script>
-
-<!-- Add "scoped" attribute to limit CSS to this component only -->
-<style scoped lang="less">
-h3 {
-  margin: 40px 0 0;
-}
-ul {
-  list-style-type: none;
-  padding: 0;
-}
-li {
-  display: inline-block;
-  margin: 0 10px;
-}
-a {
-  color: #42b983;
-}
-</style>

+ 71 - 0
src/components/e-upload.vue

@@ -0,0 +1,71 @@
+<template>
+  <div id="e-upload">
+    <el-upload :action="url" :http-request="upload" list-type="picture-card" :fileList="fileList" :on-remove="onRemove" :limit="limit" :on-exceed="onExceed">
+      <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
+    </el-upload>
+  </div>
+</template>
+
+<script>
+const _ = require('lodash');
+import axios from 'axios';
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'eUpload',
+  props: {
+    url: { type: String, required: true },
+    limit: { type: Number },
+    fileList: { type: Array },
+  },
+  model: {
+    prop: 'fileList',
+    event: 'change',
+  },
+  components: {},
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {
+    async upload({ file }) {
+      let formdata = new FormData();
+      formdata.append('file', file, file.name);
+      const res = await axios.post(this.url, formdata, { headers: { 'Content-Type': 'multipart/form-data' } });
+      if (res.status === 200 && res.data.errcode == 0) {
+        const { id, name, uri } = res.data;
+        const obj = { url: uri };
+        this.fileList.push(obj);
+      }
+    },
+    onRemove(file) {
+      const index = this.fileList.findIndex((f) => f.url === file.url);
+      this.fileList.splice(index, 1);
+    },
+    onExceed() {
+      this.$message.error(`只能上传${this.limit}个`);
+    },
+  },
+  computed: {
+    ...mapState(['user', 'menuParams']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+    list() {
+      let dup = _.cloneDeep(this.fileList);
+      if (_.isString(dup)) {
+        return [{ url: dup }];
+      } else if (_.isArray(dup)) return dup;
+      else return [obj];
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+/deep/.el-upload-list__item.is-ready {
+  display: none;
+}
+</style>

+ 52 - 0
src/components/filter-page-table.md

@@ -0,0 +1,52 @@
+## filter-page-table.vue
+#### prop
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|fields|Array|`-`|是|字段列表(下文会说明如何使用)|
+|data|Array|`-`|是|数据列表|
+|opera|Array|[ ]|否|操作列的列表(下文会说明如何使用)|
+|toFormat|Function|`-`|否|如果fields中的format不是function类型,则会走toFormat的方法,需要自己写过滤规则,多个的情况需要区分|
+|select|Boolean|false|否|需要选择就变成true|
+|total|NUmber|0|否|分页的总数据|
+|usePage|Boolean|true|否|是否使用分页|
+|options|Object|null|否|加些属性,不知道能加啥,反正我把合计加上好使了|
+|useSum|Boolean|false|否|使用合计|
+|sumcol|Array|`[]`|否|计算哪一列,就把哪一列的prop写进去|
+|sumres|String|`total`|否|处理每列结果的要求,默认计算总和(total),平均值(avg),最大值(max),最小值(min)|
+|filter|Array|`[]`|否|额外查询|
+|operaWidth|Number|200|否|操作栏宽度|
+
+>fields
+>>
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|label|String|`-`|是|列名称|
+|prop|String|`-`|是|字段名称|
+|format|Function/String|`-`|否|Function类型:数据需要过滤则将过滤方法写在这;String类型:走toFormat方法,参数位(model=>字段名,value=>值)|
+|custom|Boolean|false|否|自定义输出|
+|options|Object|`-`|否|添加额外属性,比如说样式之类的|
+|filter|String|`-`|否|如果填写,则这个字段会查询,这里只填写类型,input/select,select的选项在options插槽中使用|
+|selected|Array|`-`|false|多选选项的数据|
+|showTip|Boolean|false|否|是否使用tooltip显示过长内容|
+|filterReturn|Boolean|`-`|否|针对这个选项需要在选择后就做些逻辑处理时,改成true,然后再使用filterReturn方法处理,(例如二级联动的情况)|
+|notable|Boolean|false/undefined|否|不需要在表格中显示|
+|selected|Array|`-`|false|多选选项的数据|
+
+>opera
+>>
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|label|String|`-`|是|操作按钮提示文字|
+|icon|String|`-`|否|图标|
+|method|String|`-`|是|此按钮连接的父级方法($emit)|
+|confirm|Boolean|`-`|否|是否需要确认提示|
+|methodZh|String/Function|label|否|确认提示的操作文字,1,Function参数为这条数据,自己随意组合;2,String为纯自定义字符串,需要自己写整个提示语;3,默认,使用label字段提示|
+|display|Function|`-`|否|控制按钮是否显示(目前为简单版,只是根据此条数据中的内容判断,以后要是有需求会修改成toFormat的形式)|
+
+>methods
+>>
+|方法名|参数|说明|
+|:-:|:-:|:-:|
+|handleSelect|Array[object]|返回选择的内容|
+|query|{skip,limit,...info}|分页查询,及条件查询|
+|filterReturn|{data,prop}|查询条件栏过滤条件中filterReturn字段为true的回调方法|

+ 403 - 0
src/components/filter-page-table.vue

@@ -0,0 +1,403 @@
+<template>
+  <div id="data-table">
+    <el-row type="flex" justify="end">
+      <el-col v-if="isMobile" :span="20">
+        <el-form :model="searchInfo" :inline="true" style="padding: 0.9rem 1.875rem" size="mini" v-if="useFilter">
+          <el-form-item v-for="(item, index) in filterList" :key="index">
+            <template v-if="item.filter === 'select'">
+              <el-select
+                v-model="searchInfo[item.model]"
+                size="mini"
+                clearable
+                filterable
+                :placeholder="`请选择${item.label}`"
+                @clear="toClear(item.model)"
+                @change="(data) => filterReturn(data, item)"
+              >
+                <slot name="options" v-bind="{ item }"></slot>
+              </el-select>
+            </template>
+            <template v-else-if="item.filter === 'date'">
+              <el-date-picker
+                v-model="searchInfo[item.model]"
+                value-format="yyyy-MM-dd"
+                format="yyyy-MM-dd"
+                type="daterange"
+                range-separator="-"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                clearable
+              >
+              </el-date-picker>
+            </template>
+            <template v-else>
+              <el-input v-model="searchInfo[item.model]" clearable size="mini" :placeholder="`请输入${item.label}`" @clear="toClear(item.model)"></el-input>
+            </template>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" size="mini" @click="filterSearch">查询</el-button>
+          </el-form-item>
+        </el-form>
+      </el-col>
+      <el-col :span="4" style="text-align: right; padding: 0.9rem 0">
+        <slot name="btn"></slot>
+      </el-col>
+    </el-row>
+
+    <el-table
+      ref="table"
+      row-key="id"
+      :data="data"
+      border
+      stripe
+      size="mini"
+      :max-height="height !== null ? height : ''"
+      @select="handleSelectionChange"
+      @select-all="handleSelectAll"
+      v-bind="options"
+      :show-summary="useSum"
+      @row-click="rowClick"
+      :summary-method="computedSum"
+    >
+      <el-table-column type="selection" width="55" v-if="select" prop="id" :reserve-selection="true"> </el-table-column>
+      <template v-for="(item, index) in fields">
+        <template v-if="!item.notable">
+          <template v-if="item.custom">
+            <el-table-column :key="index" align="center" :label="item.label" v-bind="item.options" :show-overflow-tooltip="item.showTip || true">
+              <template v-slot="{ row }">
+                <slot name="custom" v-bind="{ item, row }"></slot>
+              </template>
+            </el-table-column>
+          </template>
+          <template v-else>
+            <el-table-column
+              :key="index"
+              align="center"
+              :label="item.label"
+              :prop="item.model"
+              :formatter="toFormatter"
+              :sortable="isMobile"
+              v-bind="item.options"
+              :show-overflow-tooltip="item.showTip || true"
+            >
+            </el-table-column>
+          </template>
+        </template>
+      </template>
+      <template v-if="opera.length > 0">
+        <el-table-column label="操作" align="center" :width="operaWidth">
+          <template v-slot="{ row, $index }">
+            <template v-for="(item, index) in opera">
+              <template v-if="display(item, row)">
+                <el-tooltip v-if="item.icon" :key="index" effect="dark" :content="item.label" placement="bottom">
+                  <el-button
+                    :key="index"
+                    type="text"
+                    :icon="item.icon || ''"
+                    size="mini"
+                    @click="handleOpera(row, item.method, item.confirm, item.methodZh, item.label, $index)"
+                  ></el-button>
+                </el-tooltip>
+                <!-- <el-button v-else :key="index" type="text" size="mini" @click="handleOpera(row, item.method, item.confirm, item.methodZh, item.label, $index)">
+                  {{ item.label }}
+                </el-button> -->
+                <el-link
+                  v-else
+                  :key="`${item.model}-column-${index}`"
+                  :type="item.type || 'primary'"
+                  :icon="item.icon || ''"
+                  size="mini"
+                  style="padding-right: 10px"
+                  :underline="false"
+                  @click="handleOpera(row, item.method, item.confirm, item.methodZh, item.label, $index)"
+                >
+                  {{ item.label }}
+                </el-link>
+              </template>
+            </template>
+          </template>
+        </el-table-column>
+      </template>
+    </el-table>
+    <el-row type="flex" align="middle" justify="end" style="padding-top: 1rem" v-if="usePage">
+      <el-col :span="24" style="text-align: right">
+        <el-pagination
+          background
+          layout="total, prev, pager, next"
+          :page-sizes="[10, 15, 20, 50, 100]"
+          :total="total"
+          :page-size="limit"
+          :current-page.sync="currentPage"
+          @current-change="changePage"
+          @size-change="sizeChange"
+        >
+        </el-pagination>
+        <!-- sizes -->
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+export default {
+  name: 'data-table',
+  props: {
+    fields: { type: Array, required: true },
+    data: { type: Array, required: true },
+    opera: { type: Array, default: () => [] },
+    toFormat: null,
+    height: null,
+    select: { type: Boolean, default: false },
+    selected: { type: Array, default: () => [] },
+    usePage: { type: Boolean, default: true },
+    total: { type: Number, default: 0 },
+    options: null,
+    useSum: { type: Boolean, default: false },
+    sumcol: { type: Array, default: () => [] },
+    sumres: { type: String, default: 'total' },
+    filter: { type: Array, default: () => [] },
+    operaWidth: { type: String, default: '200' },
+    limit: { type: Number, default: _.get(this, `$limit`, undefined) !== undefined ? this.$limit : process.env.VUE_APP_LIMIT * 1 || 10 },
+  },
+  components: {},
+  data: () => ({
+    pageSelected: [],
+    currentPage: 1,
+    // limit: _.get(this, `$limit`, undefined) !== undefined ? this.$limit : process.env.VUE_APP_LIMIT * 1 || 10,
+    searchInfo: {},
+    useFilter: true,
+    filterList: [],
+  }),
+  created() {},
+  computed: {},
+  methods: {
+    toFormatter(row, column, cellValue, index) {
+      let this_fields = this.fields.filter((fil) => fil.model === column.property);
+      if (this_fields.length > 0) {
+        let format = _.get(this_fields[0], `format`, false);
+        if (format) {
+          let res;
+          if (_.isFunction(format)) {
+            res = format(cellValue);
+          } else {
+            res = this.toFormat({
+              model: this_fields[0].model,
+              value: cellValue,
+            });
+          }
+          return res;
+        } else return cellValue;
+      }
+    },
+    handleOpera(data, method, confirm = false, methodZh, label, index) {
+      let self = true;
+      if (_.isFunction(methodZh)) {
+        methodZh = methodZh(data);
+      } else if (!_.isString(methodZh)) {
+        methodZh = label;
+        self = false;
+      }
+      if (confirm) {
+        this.$confirm(self ? methodZh : `您确认${methodZh}该数据?`, '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        })
+          .then(() => {
+            this.$emit(method, { data, index });
+          })
+          .catch(() => {});
+      } else {
+        this.$emit(method, { data, index });
+      }
+    },
+    handleSelectionChange(selection, row) {
+      // console.log(selection);
+      // console.log(row);
+      //根据row是否再pageSelected中,判断是添加还是删除
+      let res = [];
+      if (this.pageSelected.find((i) => i.id === row.id)) {
+        res = this.pageSelected.filter((f) => f.id !== row.id);
+      } else {
+        this.pageSelected.push(row);
+        res = this.pageSelected;
+      }
+      this.$set(this, `pageSelected`, res);
+      this.$emit(`handleSelect`, _.uniqBy(res, 'id'));
+    },
+    handleSelectAll(selection) {
+      //处于没全选状态,选择之后一定是全选,只有处于全选状态时,才会反选(全取消)
+      // console.log(selection);
+      let res = [];
+      if (selection.length > 0) {
+        //全选
+        res = _.uniqBy(this.pageSelected.concat(selection), 'id');
+      } else {
+        //全取消
+        res = _.differenceBy(this.pageSelected, this.data, 'id');
+      }
+      this.$set(this, `pageSelected`, res);
+      this.$emit(`handleSelect`, res);
+    },
+    initSelection() {
+      this.$nextTick(() => {
+        this.$refs.table.clearSelection();
+        this.selected.forEach((info) => {
+          let d = this.data.filter((p) => p.id === info.id);
+          if (d.length > 0) this.$refs.table.toggleRowSelection(d[0]);
+        });
+      });
+    },
+    selectReset() {
+      this.$refs.table.clearSelection();
+    },
+    display(item, row) {
+      let display = _.get(item, `display`, true);
+      if (display === true) return true;
+      else {
+        let res = display(row);
+        return res;
+      }
+    },
+    //
+    changePage(page = this.currentPage) {
+      this.$emit('query', { skip: (page - 1) * this.limit, limit: this.limit, ...this.searchInfo });
+    },
+    sizeChange(limit) {
+      this.limit = limit;
+      this.currentPage = 1;
+      this.$emit('query', { skip: 0, limit: this.limit, ...this.searchInfo });
+    },
+    getFilterList() {
+      let res = this.fields.filter((f) => _.get(f, 'filter', false));
+      this.$set(this, `useFilter`, res.length > 0);
+      res.map((i) => {
+        if (i.filter === 'date' && this.searchInfo[i.porp] === undefined) this.$set(this.searchInfo, i.model, []);
+      });
+      res = [...res, ...this.filter];
+      this.$set(this, `filterList`, res);
+    },
+    filterSearch() {
+      this.currentPage = 1;
+      const query = _.cloneDeep(this.searchInfo);
+      const dateType = this.fields.filter((f) => f.filter === 'date');
+      if (dateType.length > 0) {
+        for (const i of dateType) {
+          const { model } = i;
+          const data = query[model];
+          if (data && _.isArray(data) && data.length == 2) {
+            query[`${model}@start`] = data[0];
+            query[`${model}@end`] = data[1];
+          }
+          delete query[model];
+        }
+      }
+      this.$emit('query', { skip: 0, limit: this.limit, ...query });
+    },
+    rowClick(row, column, event) {
+      this.$emit(`rowClick`, row);
+    },
+    toClear(model) {
+      delete this.searchInfo[model];
+    },
+    filterReturn(data, item) {
+      let { model, filterReturn } = item;
+      if (filterReturn) this.$emit('filterReturn', { data, model });
+    },
+    // 计算合计
+    computedSum({ columns, data }) {
+      if (columns.length <= 0 || data.length <= 0) return '';
+      const result = [];
+      const reg = new RegExp(/^\d+$/);
+      for (const column of columns) {
+        // 判断有没有prop属性
+        const model = _.get(column, 'property');
+        if (!model) {
+          result.push('');
+          continue;
+        }
+        // 判断是否需要计算
+        const inlist = this.sumcol.find((f) => f == model);
+        if (!inlist) {
+          result.push('');
+          continue;
+        }
+        let res = 0;
+        // 整理出要计算的属性(只取出数字或者可以为数字的值)
+        const resetList = data.map((i) => {
+          const d = _.get(i, model);
+          const res = reg.test(d);
+          if (res) return d * 1;
+          else return 0;
+        });
+        if (this.sumres === 'total') {
+          res = this.totalComputed(resetList);
+        } else if (this.sumres === 'avg') {
+          res = this.avgComputed(resetList);
+        } else if (this.sumres === 'max') {
+          res = this.maxComputed(resetList);
+        } else if (this.sumres === 'min') {
+          res = this.minComputed(resetList);
+        }
+        result.push(res);
+      }
+      result[0] = '合计';
+      return result;
+    },
+    // 合计计算
+    totalComputed(data) {
+      const total = data.reduce((p, n) => p + n, 0);
+      return total;
+    },
+    // 平均值计算
+    avgComputed(data) {
+      const total = this.totalComputed(data);
+      return _.round(_.divide(total, data.length), 2);
+    },
+    // 最大值计算
+    maxComputed(data) {
+      return _.max(data);
+    },
+    // 最小值计算
+    minComputed(data) {
+      return _.min(data);
+    },
+  },
+  watch: {
+    selected: {
+      handler(val) {
+        if (val.length > 0) {
+          this.pageSelected = val;
+          this.initSelection();
+        }
+      },
+      immediate: true,
+    },
+    data: {
+      handler(val, oval) {
+        if (this.select) {
+          this.initSelection();
+        }
+      },
+    },
+    fields: {
+      handler(val, oval) {
+        if (val) this.getFilterList();
+      },
+      immediate: true,
+    },
+  },
+  computed: {
+    isMobile() {
+      // 手机不排序
+      let flag = navigator.userAgent.match(
+        /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
+      );
+      return !Boolean(flag);
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 85 - 0
src/components/form.md

@@ -0,0 +1,85 @@
+# 组件说明文档
+### form.vue
+### props
+
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|fields|Array|`-`|是|字段相关都在这里,用来自动输出,详情见下面|
+|submitText|String|`保存`|否|默认保存按钮的文字|
+|rules|Object|`-`|否|校验规则,不会找el-form的例子,不过使用的async-validator这个依赖为基础,会写这个也可以~~(那就厉害了,反正我是不行)~~|
+|isNew|Boolean|`-`|是|修改还是添加的提示|
+|data|Object|`-`|否|修改传来的数据|
+|needSave|Boolean|false|否|是否禁用保存按钮|
+|useEnter|Boolean|true|否|使用回车提交|
+|reset|Boolean|true|否|提交后是否重置表单|
+|filterReturn|Function|`-`|否|fields中,filterReturn为true的情况从这个函数走,参数:({data,model})|
+
+### fields
+>Array类型 必填
+>>
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|label|String|`-`|是|显示的字段中文|
+|type|String|input|否|这个字段要用什么类型来输出 input的基本类型可选值:date,datetime,radio,checkbox,select,text(只显示值),editor(富文本编辑器),password|
+|required|Boolean|`-`|否|是否必须输入|
+|model|String|`-`|是|字段名|
+|placeholder|String|`-`|否|占位,正常用,只是个透传|
+|options|object|`-`|否|标签的属性设置,例如:textarea 需要显示剩余字数,或者input限制长度,都往这里写,key-value形式(键值对,json的基本了解,不知道百度,具体属性看你具体用那个组件,那个组件有什么属性,瞎写不一定好使)|
+|custom|Boolean|`-`|否|是否使用自定义插槽|
+|tip|String|`-`|否|提示语,例如:请输入11位电话号码|
+|labelWidth|String|`120px`|否|表单label宽度,element的,默认120px|
+|format|Function|`-`|否|当type = text 时需要将该字段内容转换,可以使用format|
+|filterReturn|Boolean|`-`|否|是否返回这个字段,返回到filterReturn方法中|
+
+### slot
+>已经改成了动态名称组件,select,radio,checkbox,custom直接放到组件名为自己 model 的插槽下即可
+
+
+
+
+
+
+### slot
+>
+|插槽名|说明|
+|:-:|:-:|
+|options|fields中type为select的,选项都写在这个插槽中,多个select则需要区分options所属问题|
+|radios|fields中type为radio的,选项都写在这个插槽中,多个radio则需要区分radios所属问题|
+|checkbox|fields中type为checkbox的,选项都写在这个插槽中,多个checkbox则需要区分checkboxs所属问题|
+|custom|自定义插槽,完全自己去写|
+|submit|提交按钮部分,当needSave为false时才可以使用|
+>>关于自定义的用法:
+>>在fields中,custom:true的情况即需要自定义,写法如下
+
+>>`<template #custom="{ item, form, fieldChange }"> ... </template>`
+>>
+|参数名|说明|
+|:-:|:-:|
+|item|fields循环出来的每一项|
+|form|组件内部的表单|
+|fieldChange|组件内部的修改方法,此方法不一定必须使用,看情况来;参数:{model:xxx,value:XXX}(model:字段名,value:值)|
+>>在使用时,此插槽内的v-model可以写成form[item.model],也可以写成form.字段名
+
+>>例如`<el-input v-model="form[item.model]">`或者`<el-input v-model="form.xxx">`
+
+>> **如果有多处需要自定义,请区分开去写**
+
+
+***
+### upload
+|参数|类型|默认值|是否必填|说明|
+|:-:|:-:|:-:|:-:|:-:|
+|url|String|`-`|是|上传地址|
+|limit|Number|`-`|是|限制上传数量|
+|data|any|`-`|否|上传数据|
+|type|String|`-`|否|上传返回的字段|
+|isBtn|Boolean|false|否|是否只显示按钮|
+|showList|Boolean|true|否|是否显示上传列表|
+|accept|String|`-`|否|可以上传的文件类型,不写就没限制|
+|tip|String|`-`|否|提示信息|
+|listType|String|picture-card|否|上传文件列表显示类型|
+
+>### method
+>|方法名|返回参数|说明|
+|:-:|:-:|:-:|
+|upload|{type,data}|上传成功返回

+ 225 - 0
src/components/form.vue

@@ -0,0 +1,225 @@
+<template>
+  <div id="add">
+    <el-form
+      ref="form"
+      :model="form"
+      :rules="rules"
+      :label-width="labelWidth"
+      class="form"
+      size="small"
+      @submit.native.prevent
+      :style="styles"
+      :inline="inline"
+    >
+      <el-form-item v-if="back">
+        <el-row>
+          <el-col :span="24" style="text-align: right">
+            <el-button type="primary" size="mini" @click="toReturn">返回</el-button>
+          </el-col>
+        </el-row>
+      </el-form-item>
+      <template v-for="(item, index) in fields">
+        <template v-if="!loading">
+          <el-form-item v-if="display(item)" :key="'form-field-' + index" :label="getField('label', item)" :prop="item.model" :required="item.required">
+            <template v-if="!item.custom">
+              <template v-if="item.type !== 'text'">
+                <el-tooltip class="item" effect="dark" :content="item.tip" placement="top-start" :disabled="!item.tip">
+                  <template v-if="item.type === `date` || item.type === `datetime`">
+                    <el-date-picker
+                      v-model="form[item.model]"
+                      :type="item.type"
+                      placeholder="选择择"
+                      format="yyyy-MM-dd"
+                      value-format="yyyy-MM-dd"
+                      v-bind="item.options"
+                    >
+                    </el-date-picker>
+                  </template>
+                  <template v-else-if="item.type === `year` || item.type === `week` || item.type === `day`">
+                    <el-date-picker
+                      v-model="form[item.model]"
+                      :type="item.type"
+                      placeholder="选择择"
+                      :format="`${item.type === 'year' ? 'yyyy' : item.type === 'week' ? 'MM' : 'dd'}`"
+                      :value-format="`${item.type === 'year' ? 'yyyy' : item.type === 'week' ? 'MM' : 'dd'}`"
+                      v-bind="item.options"
+                    >
+                    </el-date-picker>
+                  </template>
+                  <template v-else-if="item.type === 'time'">
+                    <el-time-picker v-model="form[item.model]" placeholder="请选择时间" format="HH:mm" value-format="HH:mm"></el-time-picker>
+                  </template>
+                  <template v-else-if="item.type === 'radio'">
+                    <el-radio-group v-model="form[item.model]" size="mini" v-bind="item.options">
+                      <slot :name="item.model" v-bind="{ item, form, fieldChange }"></slot>
+                    </el-radio-group>
+                  </template>
+                  <template v-else-if="item.type === 'checkbox'">
+                    <el-checkbox-group v-model="form[item.model]" v-bind="item.options">
+                      <slot :name="item.model" v-bind="{ item, form, fieldChange }"></slot>
+                    </el-checkbox-group>
+                  </template>
+                  <template v-else-if="item.type === 'select'">
+                    <el-select v-model="form[item.model]" v-bind="item.options" filterable clearable @change="(data) => filterReturn(data, item)">
+                      <slot :name="item.model" v-bind="{ item, form, fieldChange }"></slot>
+                    </el-select>
+                  </template>
+                  <template v-else-if="item.type === 'textarea'">
+                    <el-input clearable v-model="form[item.model]" type="textarea" :autosize="{ minRows: 3, maxRows: 5 }"></el-input>
+                  </template>
+                  <template v-else-if="item.type === 'editor'">
+                    <wang-editor v-model="form[item.model]"></wang-editor>
+                  </template>
+                  <template v-else>
+                    <el-input
+                      clearable
+                      v-model="form[item.model]"
+                      :type="getField('type', item)"
+                      :placeholder="getField('placeholder', item)"
+                      :show-password="getField('type', item) === 'password'"
+                      v-bind="item.options"
+                    ></el-input>
+                  </template>
+                </el-tooltip>
+              </template>
+              <template v-else>
+                <template v-if="item.format">
+                  {{ item.format(form[item.model]) }}
+                </template>
+                <template v-else>
+                  {{ form[item.model] }}
+                </template>
+              </template>
+            </template>
+            <template v-else>
+              <slot :name="item.model" v-bind="{ item, form, fieldChange }"></slot>
+            </template>
+          </el-form-item>
+        </template>
+      </template>
+      <div style="padding-top: 10px">
+        <template v-if="needSave" class="btn">
+          <el-row type="flex" align="middle" justify="space-around">
+            <el-col :span="8">
+              <el-button type="primary" @click="save" style="width: 100%">{{ submitText }}</el-button>
+            </el-col>
+          </el-row>
+        </template>
+        <template v-else>
+          <slot name="submit"></slot>
+        </template>
+      </div>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+import wangEditor from './wangEditor.vue';
+export default {
+  name: 'add',
+  props: {
+    fields: { type: Array, default: () => [] },
+    rules: { type: Object, default: () => {} },
+    isNew: { type: Boolean, default: true },
+    form: null,
+    styles: { type: Object, default: () => {} },
+    needSave: { type: Boolean, default: true },
+    labelWidth: { type: String, default: '120px' },
+    useEnter: { type: Boolean, default: true },
+    submitText: { type: String, default: '保存' },
+    inline: { type: Boolean, default: false },
+    reset: { type: Boolean, default: true },
+    back: null,
+  },
+  components: {
+    wangEditor,
+  },
+  model: {
+    prop: 'form',
+    event: 'change',
+  },
+  data: () => ({
+    show: false,
+    dateShow: false,
+    loading: false,
+  }),
+  created() {
+    if (this.useEnter) {
+      document.onkeydown = () => {
+        let key = window.event.keyCode;
+        if (key == 13) {
+          this.save();
+        }
+      };
+    }
+  },
+  computed: {},
+  mounted() {},
+  watch: {
+    fields: {
+      handler(val) {
+        this.checkType();
+      },
+      immediate: true,
+    },
+  },
+  methods: {
+    getField(item, data) {
+      let res = _.get(data, item, null);
+      if (item === 'type') res = res === null ? `text` : res;
+      if (item === 'placeholder') res = res === null ? `请输入${data.label}` : res;
+      if (item === 'required') res = res === null ? false : res;
+      if (item === `error`) res = res === null ? `${data.label}错误` : res;
+      return res;
+    },
+    save() {
+      this.$refs['form'].validate((valid) => {
+        if (valid) {
+          this.$emit(`save`, { isNew: this.isNew, data: JSON.parse(JSON.stringify(this.form)) });
+          if (this.reset) this.$refs.form.resetFields();
+        } else {
+          console.warn('form validate error!!!');
+        }
+      });
+    },
+    fieldChange({ model, value }) {
+      this.$set(this.form, model, value);
+    },
+    checkType() {
+      let arr = this.fields.filter((fil) => fil.type === 'checkbox');
+      if (arr.length > 0 && this.isNew) {
+        for (const item of arr) {
+          this.$set(this.form, `${item.model}`, []);
+        }
+      }
+    },
+    display(field) {
+      let noForm = _.get(field, `noForm`);
+      if (_.isBoolean(noForm) && noForm) return false;
+      let dis = _.get(field, `display`);
+      if (!_.isFunction(dis)) return true;
+      else return dis(field, this.form);
+    },
+    filterReturn(data, item) {
+      let { model, filterReturn } = item;
+      if (filterReturn) this.$emit('filterReturn', { data, model });
+    },
+    toReturn() {
+      if (_.isFunction(this.back)) eval(this.back)();
+      else this.$router.push(this.back);
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.form {
+  padding: 2rem 1rem;
+  background: #fff;
+  border-radius: 20px;
+}
+// /deep/.btn .el-form-item__content {
+//   margin-left: 0 !important;
+// }
+</style>

+ 141 - 0
src/components/wangEditor.vue

@@ -0,0 +1,141 @@
+<template>
+  <div id="wangEditor">
+    <div :id="this.id" style="text-align: left"></div>
+  </div>
+</template>
+
+<script>
+import E from 'wangeditor';
+import axios from 'axios';
+export default {
+  name: 'wangEditor',
+  props: {
+    value: String,
+    height: { type: Number, default: 300 }, //编辑区域高度
+    zIndex: { type: Number, default: 200 }, //显示层级优先度
+    placeholder: { type: String }, //未输入时显示提示
+    isFocus: { type: Boolean, default: false }, //是否自动焦点(自动切换到富文本上)
+    url: { type: String }, //上传地址,如果没有下面的两个,或者用的时候缺对应项,也用这个,如果都没有,就提示有问题
+    imgSize: { type: Number, default: 5 }, //图片大小,单位MB,默认5MB
+    imgAccept: { type: Array, default: () => ['jpg', 'jpeg', 'png', 'gif', 'bmp'] }, //图片类型
+    imgOnceNumber: { type: Number, default: 5 }, //一次最多上传几个图片
+    imgUrl: { type: String }, //图片上传地址
+    videoSize: { type: Number, default: 1 }, //视频大小限制,单位为G,默认1G
+    videoUrl: { type: String }, //视频上传地址
+    imgLimit: { type: Array }, //img标签限制[width,height]
+  },
+  model: {
+    prop: 'value',
+    event: 'input',
+  },
+  data: function () {
+    const getId = () => {
+      let id = 'weditor';
+      let i = 0;
+      while (i < 20) {
+        const num = Math.ceil(Math.random() * 10);
+        id = `${id}${num}`;
+        i++;
+      }
+      return id;
+    };
+    return {
+      id: getId(),
+      editor: undefined,
+      imgObject: {},
+    };
+  },
+  created() {},
+  mounted() {
+    this.init();
+  },
+  methods: {
+    init() {
+      const editor = new E(`#${this.id}`);
+      // 使用设置
+      editor.config.height = this.height;
+      editor.config.zIndex = this.zIndex;
+      editor.config.focus = this.isFocus;
+      if (this.placeholder) editor.config.placeholder = this.placeholder;
+      // 图片设置
+      editor.config.uploadImgMaxSize = this.imgSize * 1024 * 1024;
+      editor.config.uploadImgAccept = this.imgAccept;
+      editor.config.uploadImgMaxLength = this.imgOnceNumber;
+      // 视频
+      editor.config.uploadVideoMaxSize = this.videoSize * 1024 * 1024 * 1024;
+      // 默认设置
+      // 图片-完全自定义上传
+      editor.config.customUploadImg = async (resultFiles, insertImgFn) => {
+        let url = this.imgUrl || this.url;
+        if (!url) {
+          this.$message.error('未配置上传路径!');
+          return false;
+        }
+        for (const file of resultFiles) {
+          const res = await this.upload(file);
+          insertImgFn(res);
+        }
+      };
+      // 视频-完全自定义上传
+      editor.config.customUploadVideo = async (resultFiles, insertVideoFn) => {
+        let url = this.videoUrl || this.url;
+        if (!url) {
+          this.$message.error('未配置上传路径!');
+          return false;
+        }
+        for (const file of resultFiles) {
+          const res = await this.upload(file);
+          insertVideoFn(res);
+        }
+      };
+      editor.config.onchange = (html) => {
+        this.$emit('input', html);
+      };
+      // hooks
+      editor.txt.eventHooks.clickEvents.push(this.toClick);
+      editor.create();
+      this.$set(this, `editor`, editor);
+    },
+    toClick(e) {
+      // const target = e.target;
+      // if (target.tagName == 'IMG') {
+      //   target.style.width = '150px';
+      //   target.style.height = '150px';
+      // }
+    },
+
+    async upload(file) {
+      let formdata = new FormData();
+      formdata.append('file', file, file.name);
+      const res = await axios.post(this.url, formdata, { headers: { 'Content-Type': 'multipart/form-data' } });
+      if (res.status === 200 && res.data.errcode == 0) {
+        const { id, name, uri } = res.data;
+        return uri;
+      } else {
+        this.$message.error('上传失败');
+      }
+    },
+  },
+  watch: {
+    value: {
+      handler(val, oval) {
+        this.$nextTick(() => {
+          // 必须在nextTick下才可以获取this.editor,因为下面用了immediate,在mounted周期之前,但是init是在mounted中进行的.所以必须延迟到下个更新队列中
+          // 且这个watch只是初始化用,因为上面用的v-model,有自己更新的方式;
+          if (val && !oval) {
+            this.editor.txt.html(val);
+          }
+        });
+      },
+      immediate: true,
+    },
+  },
+  beforeDestroy() {
+    // 销毁编辑器
+    this.editor.destroy();
+    this.editor = null;
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 40 - 0
src/layout/admin.vue

@@ -0,0 +1,40 @@
+<template>
+  <div id="admin">
+    <el-container>
+      <side />
+      <el-container>
+        <el-header style="height: 5vh"><topHead /></el-header>
+        <el-main style="height: 93vh; margin-top: 2vh; background: #f1f1f1">
+          <router-view />
+        </el-main>
+      </el-container>
+    </el-container>
+  </div>
+</template>
+
+<script>
+import side from './admin/side.vue';
+import topHead from './admin/head.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'admin',
+  props: {},
+  components: { side, topHead },
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {},
+  computed: {
+    ...mapState(['user', 'isMobile']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 48 - 0
src/layout/admin/head.vue

@@ -0,0 +1,48 @@
+<template>
+  <el-row type="flex" align="middle" justify="space-between" style="padding-top: 5px">
+    <el-col :span="2" v-if="drawer">
+      <el-tooltip :disabled="isMobile" effect="dark" content="收起菜单" placement="bottom">
+        <i class="el-icon-s-fold" style="font-size: 24px; color: #409eff" @click="drawerOpera"></i>
+      </el-tooltip>
+    </el-col>
+    <el-col :span="2" v-if="!drawer">
+      <el-tooltip :disabled="isMobile" effect="dark" content="展开菜单" placement="bottom">
+        <i class="el-icon-s-unfold" style="font-size: 24px; color: #409eff" @click="drawerOpera"></i>
+      </el-tooltip>
+    </el-col>
+
+    <el-col :span="20"> </el-col>
+
+    <el-col :span="2" style="text-align: right">
+      <avatar />
+    </el-col>
+  </el-row>
+</template>
+
+<script>
+import avatar from './head/avatar.vue';
+import { mapState, mapMutations, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'topHead',
+  props: {},
+  components: { avatar },
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {
+    ...mapMutations(['drawerOpera']),
+  },
+  computed: {
+    ...mapState(['user', 'drawer', 'isMobile']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 46 - 0
src/layout/admin/head/avatar.vue

@@ -0,0 +1,46 @@
+<template>
+  <div id="avatar">
+    <el-dropdown @command="evalCommand">
+      <el-avatar :size="isMobile ? 32 : 48"> {{ user.name || '未登录' }} </el-avatar>
+      <template #dropdown>
+        <el-dropdown-menu>
+          <el-dropdown-item divided command="logout">注销</el-dropdown-item>
+        </el-dropdown-menu>
+      </template>
+    </el-dropdown>
+  </div>
+</template>
+
+<script>
+import { mapState, mapMutations, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'avatar',
+  props: {},
+  components: {},
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {
+    ...mapMutations(['deleteUser']),
+    evalCommand(data) {
+      eval(`this.${data}`)();
+    },
+    logout() {
+      this.deleteUser();
+      this.$router.push('/login');
+    },
+  },
+  computed: {
+    ...mapState(['user', 'isMobile']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 70 - 0
src/layout/admin/head/position.vue

@@ -0,0 +1,70 @@
+<template>
+  <div id="position">
+    <el-tooltip :disabled="isMobile" effect="dark" content="切换分站" placement="bottom" v-if="user && user._tenant === 'master'">
+      <i class="el-icon-guide" style="font-size: 24px; color: #409eff" @click="changeTenant"></i>
+    </el-tooltip>
+    <el-dialog title="切换分站" :visible.sync="dialog" :width="width">
+      <el-select v-model="tenant" placeholder="请选择分站">
+        <el-option v-for="(i, index) in list" :key="`item-${index}`" :label="i.name" :value="i._tenant"></el-option>
+      </el-select>
+      <template #footer>
+        <el-button type="primary" @click="toChange" size="mini">切换</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: tenant } = createNamespacedHelpers('tenant');
+export default {
+  name: 'position',
+  props: {},
+  components: {},
+  data: function () {
+    return {
+      list: [],
+      dialog: false,
+      tenant: '',
+    };
+  },
+  created() {
+    const tenant = localStorage.getItem('tenant');
+    if (tenant) this.$set(this, `tenant`, tenant);
+  },
+  methods: {
+    ...tenant(['query']),
+    changeTenant() {
+      this.dialog = true;
+      this.search();
+    },
+    async search() {
+      if (this.list.length > 0) return;
+      const res = await this.query();
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, res.data);
+      }
+    },
+    toChange() {
+      localStorage.setItem('tenant', this.tenant);
+      this.$message.success('切换成功');
+      this.dialog = false;
+      this.$router.go(0);
+    },
+  },
+  computed: {
+    ...mapState(['user', 'isMobile']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+    width() {
+      return this.isMobile ? '70%' : '30%';
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 40 - 0
src/layout/admin/side.vue

@@ -0,0 +1,40 @@
+<template>
+  <div id="side">
+    <el-aside v-if="!isMobile" style="max-width: 300px; width: auto; overflow-x: hidden">
+      <menus />
+    </el-aside>
+    <template v-if="isMobile">
+      <el-drawer :visible.sync="drawer" direction="ltr" :show-close="false" :withHeader="false" :before-close="drawerOpera" style="width: 100vh">
+        <menus />
+      </el-drawer>
+    </template>
+  </div>
+</template>
+
+<script>
+import menus from './side/menu.vue';
+import { mapState, mapMutations, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'side',
+  props: {},
+  components: { menus },
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {
+    ...mapMutations(['drawerOpera']),
+  },
+  computed: {
+    ...mapState(['user', 'isMobile', 'drawer']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 74 - 0
src/layout/admin/side/menu.vue

@@ -0,0 +1,74 @@
+<template>
+  <div id="menu" :style="getWidth()">
+    <el-menu :default-active="route" @open="handleOpen" :collapse="isCollapse" :unique-opened="true" :collapse-transition="isMobile ? false : true" router>
+      <el-menu-item index="/">
+        <i class="el-icon-s-grid"></i>
+        <span slot="title">首页</span>
+      </el-menu-item>
+      <el-menu-item index="/setting">
+        <i class="el-icon-setting"></i>
+        <span slot="title">本管设置</span>
+      </el-menu-item>
+      <el-menu-item index="/space">
+        <i class="el-icon-s-order"></i>
+        <span slot="title">场地管理</span>
+      </el-menu-item>
+      <el-menu-item index="/arrange">
+        <i class="el-icon-date"></i>
+        <span slot="title">场地安排</span>
+      </el-menu-item>
+      <el-menu-item index="/vip">
+        <i class="el-icon-tickets"></i>
+        <span slot="title">会员管理 及 会员充值记录</span>
+      </el-menu-item>
+      <el-menu-item index="/order">
+        <i class="el-icon-link"></i>
+        <span slot="title">订单管理</span>
+      </el-menu-item>
+    </el-menu>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'menus',
+  props: {},
+  components: {},
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {
+    getWidth() {
+      let style = { width: 'auto' };
+      if (this.drawer) style.minWidth = '200px';
+      return style;
+    },
+    handleOpen(key, keyPath) {
+      console.log(key, keyPath);
+    },
+    handleClose(key, keyPath) {
+      console.log(key, keyPath);
+    },
+  },
+
+  computed: {
+    ...mapState(['user', 'drawer', 'isMobile']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+    isCollapse() {
+      return !this.drawer;
+    },
+    route() {
+      return this.$route.path;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 10 - 5
src/main.js

@@ -1,7 +1,12 @@
-import Vue from "vue";
-import App from "./App.vue";
-import router from "./router";
-import store from "./store";
+import Vue from 'vue';
+import App from './App.vue';
+import router from './router';
+import store from './store';
+import '@/plugins/element';
+import '@/plugins/components';
+import '@/plugins/axios';
+import '@/plugins/meta';
+import '@/plugins/check-res';
 
 Vue.config.productionTip = false;
 
@@ -9,4 +14,4 @@ new Vue({
   router,
   store,
   render: (h) => h(App),
-}).$mount("#app");
+}).$mount('#app');

+ 19 - 0
src/plugins/axios.js

@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import AxiosWrapper from '@/util/axios-wrapper';
+
+const Plugin = {
+  install(vue, options) {
+    // 3. 注入组件
+    vue.mixin({
+      created() {
+        if (this.$store && !this.$store.$axios) {
+          this.$store.$axios = this.$axios;
+        }
+      },
+    });
+    // 4. 添加实例方法
+    vue.prototype.$axios = new AxiosWrapper(options);
+  },
+};
+
+Vue.use(Plugin, { baseUrl: process.env.VUE_APP_AXIOS_BASE_URL });

+ 38 - 0
src/plugins/check-res.js

@@ -0,0 +1,38 @@
+/* eslint-disable no-underscore-dangle */
+/* eslint-disable no-param-reassign */
+/* eslint-disable no-unused-vars */
+/* eslint-disable no-shadow */
+import Vue from 'vue';
+import _ from 'lodash';
+import { Message } from 'element-ui';
+
+const vm = new Vue({});
+const Plugin = {
+  install(Vue, options) {
+    // 4. 添加实例方法
+    Vue.prototype.$checkRes = (res, okText, errText) => {
+      let _okText = okText;
+      let _errText = errText;
+      if (!_.isFunction(okText) && _.isObject(okText) && okText != null) {
+        ({ okText: _okText, errText: _errText } = okText);
+      }
+      const { errcode = 0, errmsg } = res || {};
+      if (errcode === 0) {
+        if (_.isFunction(_okText)) {
+          return _okText();
+        }
+        if (_okText) {
+          Message({ message: _okText, type: 'success' });
+        }
+        return true;
+      }
+      if (_.isFunction(_errText)) {
+        return _errText();
+      }
+      Message({ message: _errText || errmsg, type: 'error' });
+      return false;
+    };
+  },
+};
+
+Vue.use(Plugin);

+ 12 - 0
src/plugins/components.js

@@ -0,0 +1,12 @@
+import Vue from 'vue';
+import dataTable from '@/components/filter-page-table.vue';
+import dataForm from '@/components/form.vue';
+import eUpload from '@/components/e-upload.vue';
+const Plugin = (vue) => {
+  vue.prototype.$dev_mode = process.env.NODE_ENV === 'development';
+  vue.component('data-table', dataTable);
+  vue.component('data-form', dataForm);
+  vue.component('eUpload', eUpload);
+};
+
+Vue.use(Plugin);

+ 5 - 0
src/plugins/element.js

@@ -0,0 +1,5 @@
+import Vue from 'vue';
+import Element from 'element-ui';
+import 'element-ui/lib/theme-chalk/index.css';
+
+Vue.use(Element);

+ 4 - 0
src/plugins/meta.js

@@ -0,0 +1,4 @@
+import Vue from 'vue';
+import Meta from 'vue-meta';
+
+Vue.use(Meta);

+ 21 - 0
src/plugins/setting.js

@@ -0,0 +1,21 @@
+import Vue from 'vue';
+
+Vue.config.weixin = {
+  // baseUrl: process.env.BASE_URL + 'weixin',
+  baseUrl: `http://${location.host}`,
+};
+
+Vue.config.stomp = {
+  // brokerURL: 'ws://192.168.1.118/ws',
+  brokerURL: '/ws', // ws://${location.host}/ws
+  // brokerURL: 'ws://127.0.0.1:8000/ws',
+  connectHeaders: {
+    host: 'platform',
+    login: 'visit', //visit
+    passcode: 'visit', //visit123
+  },
+  // debug: true,
+  reconnectDelay: 5000,
+  heartbeatIncoming: 4000,
+  heartbeatOutgoing: 4000,
+};

+ 65 - 0
src/plugins/stomp.js

@@ -0,0 +1,65 @@
+/**
+ * 基于WebStomp的消息处理插件
+ */
+
+import Vue from 'vue';
+import _ from 'lodash';
+import assert from 'assert';
+import { Client } from '@stomp/stompjs/esm5/client';
+
+const Plugin = {
+  install(Vue, options) {
+    assert(_.isObject(options));
+    if (options.debug && !_.isFunction(options.debug)) {
+      options.debug = (str) => {
+        console.log(str);
+      };
+    }
+    assert(_.isString(options.brokerURL));
+    if (!options.brokerURL.startsWith('ws://')) {
+      options.brokerURL = `ws://${location.host}${options.brokerURL}`;
+    }
+
+    // 3. 注入组件
+    Vue.mixin({
+      beforeDestroy: function () {
+        if (this.$stompClient) {
+          this.$stompClient.deactivate();
+          delete this.$stompClient;
+        }
+      },
+    });
+
+    // 4. 添加实例方法
+    Vue.prototype.$stomp = function (subscribes = {}) {
+      // connect to mq
+      const client = new Client(options);
+      client.onConnect = (frame) => {
+        // Do something, all subscribes must be done is this callback
+        // This is needed because this will be executed after a (re)connect
+        console.log('[stomp] connected');
+        Object.keys(subscribes)
+          .filter((p) => _.isFunction(subscribes[p]))
+          .forEach((key) => {
+            client.subscribe(key, subscribes[key]);
+          });
+      };
+
+      client.onStompError = (frame) => {
+        // Will be invoked in case of error encountered at Broker
+        // Bad login/passcode typically will cause an error
+        // Complaint brokers will set `message` header with a brief message. Body may contain details.
+        // Compliant brokers will terminate the connection after any error
+        console.log('Broker reported error: ' + frame.headers['message']);
+        console.log('Additional details: ' + frame.body);
+      };
+
+      client.activate();
+
+      this.$stompClient = client;
+    };
+  },
+};
+export default () => {
+  Vue.use(Plugin, Vue.config.stomp);
+};

+ 66 - 14
src/router/index.js

@@ -1,30 +1,82 @@
-import Vue from "vue";
-import VueRouter from "vue-router";
-import Home from "../views/Home.vue";
+import Vue from 'vue';
+import store from '@/store';
+import VueRouter from 'vue-router';
+const jwt = require('jsonwebtoken');
 
 Vue.use(VueRouter);
 
 const routes = [
   {
-    path: "/",
-    name: "Home",
-    component: Home,
+    path: '/login',
+    name: 'login',
+    component: () => import(/* webpackChunkName: "login" */ '../views/login.vue'),
   },
   {
-    path: "/about",
-    name: "About",
-    // route level code-splitting
-    // 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"),
+    path: '/',
+    name: 'admin',
+    meta: { title: '管理端' },
+    component: () => import(/* webpackChunkName: "login" */ '../layout/admin.vue'),
+    children: [
+      {
+        path: '/',
+        name: 'index',
+        component: () => import(/* webpackChunkName: "index" */ '../views/index/index.vue'),
+      },
+      {
+        path: '/setting',
+        name: 'setting',
+        component: () => import(/* webpackChunkName: "setting" */ '../views/setting/index.vue'),
+      },
+      {
+        path: '/space',
+        name: 'space',
+        component: () => import(/* webpackChunkName: "space" */ '../views/space/index.vue'),
+      },
+      {
+        path: '/arrange',
+        name: 'arrange',
+        component: () => import(/* webpackChunkName: "arrange" */ '../views/arrange/index.vue'),
+      },
+      {
+        path: '/vip',
+        name: 'vip',
+        component: () => import(/* webpackChunkName: "vip" */ '../views/vip/index.vue'),
+      },
+      {
+        path: '/order',
+        name: 'order',
+        component: () => import(/* webpackChunkName: "order" */ '../views/order/index.vue'),
+      },
+    ],
   },
 ];
 
 const router = new VueRouter({
-  mode: "history",
+  mode: 'history',
   base: process.env.BASE_URL,
   routes,
 });
 
+const isLogin = () => {
+  // vuex中是否有用户信息
+  let user = _.get(store, 'state.user');
+  // 有就回去
+  if (user) return true;
+  // 浏览器缓存中有没有token
+  const token = localStorage.getItem('token');
+  // 没有也回去
+  if (!token) return false;
+  // 将token转换成用户信息
+  user = jwt.decode(token);
+  // 放到vuex中
+  store.commit('setUser', user, { root: true });
+  return true;
+};
+
+router.beforeEach((to, from, next) => {
+  const res = isLogin();
+  if (!res && to.path !== 'login') next('login');
+  next();
+});
+
 export default router;

+ 59 - 0
src/store/api/system/admin.js

@@ -0,0 +1,59 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import _ from 'lodash';
+import { Message } from 'element-ui';
+const jwt = require('jsonwebtoken');
+Vue.use(Vuex);
+const api = {
+  interface: `/api/qiuguan/system/admin`,
+};
+const state = () => ({});
+const mutations = {};
+
+const actions = {
+  async query({ commit }, { skip = 0, limit, ...info } = {}) {
+    const res = await this.$axios.$get(`${api.interface}`, {
+      skip,
+      limit,
+      ...info,
+    });
+    return res;
+  },
+  async create({ commit }, payload) {
+    const res = await this.$axios.$post(`${api.interface}`, payload);
+    return res;
+  },
+  async fetch({ commit }, payload) {
+    const res = await this.$axios.$get(`${api.interface}/${payload}`);
+    return res;
+  },
+  async update({ commit }, { id, ...data }) {
+    const res = await this.$axios.$post(`${api.interface}/update/${id}`, data);
+    return res;
+  },
+  async delete({ commit }, payload) {
+    const res = await this.$axios.$delete(`${api.interface}/${payload}`);
+    return res;
+  },
+  async login({ commit }, user) {
+    const res = await this.$axios.$post(`${api.interface}/login`, user);
+    if (res.errcode === 0) {
+      localStorage.setItem('token', res.data);
+      user = jwt.decode(res.data);
+      commit('setUser', user, { root: true });
+      return res;
+    } else {
+      Message.error(res.errmsg);
+    }
+  },
+  async updatePwd({ commit }, { id, ...data }) {
+    const res = await this.$axios.$post(`${api.interface}/password/${id}`, data);
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 21 - 0
src/store/frame.js

@@ -0,0 +1,21 @@
+const isMobile = () => {
+  let flag = navigator.userAgent.match(
+    /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
+  );
+  return Boolean(flag);
+};
+
+export const fstate = {
+  isMobile: isMobile(),
+  drawer: !isMobile(),
+};
+
+export const opera = {
+  drawerOpera(state) {
+    state.drawer = !state.drawer;
+  },
+  checkMobile(state) {
+    state.isMobile = isMobile();
+    state.drawer = !isMobile();
+  },
+};

+ 17 - 5
src/store/index.js

@@ -1,11 +1,23 @@
-import Vue from "vue";
-import Vuex from "vuex";
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { fstate, opera } from './frame';
 
+import admin from './api/system/admin';
 Vue.use(Vuex);
 
 export default new Vuex.Store({
-  state: {},
-  mutations: {},
+  state: { user: undefined, ...fstate },
+  mutations: {
+    ...opera,
+    setUser(state, payload) {
+      state.user = payload;
+      localStorage.setItem('user', JSON.stringify(payload));
+    },
+    deleteUser(state, payload) {
+      state.user = {};
+      localStorage.removeItem('user');
+    },
+  },
   actions: {},
-  modules: {},
+  modules: { admin },
 });

+ 119 - 0
src/util/axios-wrapper.js

@@ -0,0 +1,119 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import _ from 'lodash';
+import Axios from 'axios';
+import { Util, Error } from 'naf-core';
+// import { Indicator } from 'mint-ui';
+import util from './user-util';
+
+const { trimData, isNullOrUndefined } = Util;
+const { ErrorCode } = Error;
+
+let currentRequests = 0;
+
+export default class AxiosWrapper {
+  constructor({ baseUrl = '', unwrap = true } = {}) {
+    this.baseUrl = baseUrl;
+    this.unwrap = unwrap;
+  }
+
+  // 替换uri中的参数变量
+  static merge(uri, query = {}) {
+    if (!uri.includes(':')) {
+      return uri;
+    }
+    const keys = [];
+    const regexp = /\/:([a-z0-9_]+)/gi;
+    let res;
+    // eslint-disable-next-line no-cond-assign
+    while ((res = regexp.exec(uri)) != null) {
+      keys.push(res[1]);
+    }
+    keys.forEach((key) => {
+      if (!isNullOrUndefined(query[key])) {
+        uri = uri.replace(`:${key}`, query[key]);
+      }
+    });
+    return uri;
+  }
+
+  $get(uri, query, options) {
+    return this.$request(uri, null, query, options);
+  }
+
+  $post(uri, data = {}, query, options) {
+    return this.$request(uri, data, query, options);
+  }
+  $delete(uri, data = {}, router, query, options = {}) {
+    options = { ...options, method: 'delete' };
+    return this.$request(uri, data, query, options, router);
+  }
+  async $request(uri, data, query, options) {
+    // TODO: 合并query和options
+    if (_.isObject(query) && _.isObject(options)) {
+      options = { ...options, params: query, method: 'get' };
+    } else if (_.isObject(query) && !query.params) {
+      options = { params: query };
+    } else if (_.isObject(query) && query.params) {
+      options = query;
+    }
+    if (!options) options = {};
+    if (options.params) options.params = trimData(options.params);
+    const url = AxiosWrapper.merge(uri, options.params);
+    currentRequests += 1;
+    // Indicator.open({
+    //   spinnerType: 'fading-circle',
+    // });
+
+    try {
+      const axios = Axios.create({
+        baseURL: this.baseUrl,
+      });
+      axios.defaults.headers.common.Authorization = util.token;
+      const tenant = localStorage.getItem('tenant');
+      if (tenant) axios.defaults.headers.common['x-tenant'] = tenant;
+      let res = await axios.request({
+        method: isNullOrUndefined(data) ? 'get' : 'post',
+        url,
+        data,
+        responseType: 'json',
+        ...options,
+      });
+      res = res.data;
+      const { errcode, errmsg, details } = res;
+      if (errcode) {
+        console.warn(`[${uri}] fail: ${errcode}-${errmsg} ${details}`);
+        return res;
+      }
+      // unwrap data
+      if (this.unwrap) {
+        res = _.omit(res, ['errmsg', 'details']);
+        const keys = Object.keys(res);
+        if (keys.length === 1 && keys.includes('data')) {
+          res = res.data;
+        }
+      }
+      return res;
+    } catch (err) {
+      let errmsg = '接口请求失败,请稍后重试';
+      if (err.response) {
+        const { status } = err.response;
+        if (status === 401) errmsg = '用户认证失败,请重新登录';
+        if (status === 403) errmsg = '当前用户不允许执行该操作';
+      }
+      console.error(
+        `[AxiosWrapper] 接口请求失败: ${err.config && err.config.url} - 
+        ${err.message}`
+      );
+      return { errcode: ErrorCode.SERVICE_FAULT, errmsg, details: err.message };
+    } finally {
+      /* eslint-disable */
+      currentRequests -= 1;
+      if (currentRequests <= 0) {
+        currentRequests = 0;
+        // Indicator.close();
+      }
+    }
+  }
+}

+ 69 - 0
src/util/user-util.js

@@ -0,0 +1,69 @@
+/* eslint-disable no-console */
+export default {
+  get user() {
+    const val = sessionStorage.getItem('user');
+    try {
+      if (val) return JSON.parse(val);
+    } catch (err) {
+      console.error(err);
+    }
+    return null;
+  },
+  set user(userinfo) {
+    sessionStorage.setItem('user', JSON.stringify(userinfo));
+  },
+  get token() {
+    return sessionStorage.getItem('token');
+  },
+  set token(token) {
+    sessionStorage.setItem('token', token);
+  },
+  get openid() {
+    return sessionStorage.getItem('openid');
+  },
+  set openid(openid) {
+    sessionStorage.setItem('openid', openid);
+  },
+  get isGuest() {
+    return !this.user || this.user.role === 'guest';
+  },
+  save({ userinfo, token }) {
+    sessionStorage.setItem('user', JSON.stringify(userinfo));
+    sessionStorage.setItem('token', token);
+  },
+
+  get corpInfo() {
+    const val = sessionStorage.getItem('corpInfo');
+    if (val) return JSON.parse(val);
+    return null;
+  },
+  set corpInfo(corpInfo) {
+    sessionStorage.setItem('corpInfo', JSON.stringify(corpInfo));
+  },
+  saveCorpInfo(corpInfo) {
+    sessionStorage.setItem('corpInfo', JSON.stringify(corpInfo));
+  },
+
+  get unit() {
+    const val = sessionStorage.getItem('unit');
+    if (val) return JSON.parse(val);
+    return null;
+  },
+  set unit(unitList) {
+    sessionStorage.setItem('unit', JSON.stringify(unitList));
+  },
+  saveUnit(unitList) {
+    sessionStorage.setItem('unit', JSON.stringify(unitList));
+  },
+  get userInfo() {
+    const val = sessionStorage.getItem('userInfo');
+    if (val) return JSON.parse(val);
+    return null;
+  },
+  set userInfo(userInfo) {
+    sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
+  },
+  saveUserInfo(userInfo) {
+    sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
+  },
+};

+ 0 - 5
src/views/About.vue

@@ -1,5 +0,0 @@
-<template>
-  <div class="about">
-    <h1>This is an about page</h1>
-  </div>
-</template>

+ 0 - 18
src/views/Home.vue

@@ -1,18 +0,0 @@
-<template>
-  <div class="home">
-    <img alt="Vue logo" src="../assets/logo.png" />
-    <HelloWorld msg="Welcome to Your Vue.js App" />
-  </div>
-</template>
-
-<script>
-// @ is an alias to /src
-import HelloWorld from "@/components/HelloWorld.vue";
-
-export default {
-  name: "Home",
-  components: {
-    HelloWorld,
-  },
-};
-</script>

+ 30 - 0
src/views/arrange/index.vue

@@ -0,0 +1,30 @@
+<template>
+  <div id="index">
+    <p>index</p>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'index',
+  props: {},
+  components: {},
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {},
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 30 - 0
src/views/index/index.vue

@@ -0,0 +1,30 @@
+<template>
+  <div id="index">
+    <p>index</p>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'index',
+  props: {},
+  components: {},
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {},
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 65 - 0
src/views/login.vue

@@ -0,0 +1,65 @@
+<template>
+  <div id="login" style="height: 100vh">
+    <el-row style="padding-top: 25vh; text-align: center">
+      <el-col :span="24">
+        <h2>管理员登陆</h2>
+      </el-col>
+    </el-row>
+    <el-row type="flex" justify="space-around">
+      <el-col :span="span">
+        <sForm v-model="form" :fields="fields" :rules="rules" labelWidth="100px" @save="toLogin">
+          <template #tenant>
+            <el-option v-for="(i, index) in siteList" :key="`site-${index}`" :label="i.name" :value="i._tenant"></el-option>
+          </template>
+        </sForm>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import sForm from '@/components/form.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: admin } = createNamespacedHelpers('admin');
+export default {
+  name: 'login',
+  props: {},
+  components: { sForm },
+  data: function () {
+    return {
+      form: {},
+      siteList: [],
+      fields: [
+        { label: '登陆用户名', model: 'login_id', required: true },
+        { label: '密码', model: 'password', type: 'password', required: true },
+      ],
+      rules: {
+        login_id: [{ required: true, message: '请输入登录用户名', trigger: 'blur' }],
+        password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+      },
+    };
+  },
+  created() {},
+  methods: {
+    ...admin(['login']),
+    async toLogin({ data }) {
+      const res = await this.login(data);
+      if (res) this.$router.push('/');
+    },
+  },
+  computed: {
+    ...mapState(['user', 'isMobile']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+    span() {
+      return this.isMobile ? 24 : 8;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 30 - 0
src/views/order/index.vue

@@ -0,0 +1,30 @@
+<template>
+  <div id="index">
+    <p>index</p>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'index',
+  props: {},
+  components: {},
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {},
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 30 - 0
src/views/setting/index.vue

@@ -0,0 +1,30 @@
+<template>
+  <div id="index">
+    <p>index</p>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'index',
+  props: {},
+  components: {},
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {},
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 30 - 0
src/views/space/index.vue

@@ -0,0 +1,30 @@
+<template>
+  <div id="index">
+    <p>index</p>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'index',
+  props: {},
+  components: {},
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {},
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 30 - 0
src/views/vip/index.vue

@@ -0,0 +1,30 @@
+<template>
+  <div id="index">
+    <p>index</p>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'index',
+  props: {},
+  components: {},
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {},
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 74 - 0
vue.config.js

@@ -0,0 +1,74 @@
+const path = require('path');
+const CompressionPlugin = require('compression-webpack-plugin');
+
+module.exports = {
+  publicPath: `/${process.env.VUE_APP_ROUTER}`,
+  // 打包文件
+  outputDir: `${process.env.VUE_APP_ROUTER}`,
+  productionSourceMap: false,
+  configureWebpack: (config) => {
+    Object.assign(config, {
+      // 开发生产共同配置
+      resolve: {
+        extensions: ['.js', '.json', '.vue'],
+        alias: {
+          '@': path.resolve(__dirname, './src'),
+          '@c': path.resolve(__dirname, './src/components'),
+          '@a': path.resolve(__dirname, './src/assets'),
+        },
+      },
+    });
+    if (process.env.NODE_ENV === 'production') {
+      //生产环境
+      config.plugins.push(
+        new CompressionPlugin({
+          /* [file]被替换为原始资产文件名。
+                 [path]替换为原始资产的路径。
+                 [dir]替换为原始资产的目录。
+                 [name]被替换为原始资产的文件名。
+                 [ext]替换为原始资产的扩展名。
+                 [query]被查询替换。*/
+          filename: '[path].gz[query]',
+          //压缩算法
+          algorithm: 'gzip',
+          //匹配文件
+          test: /\.js$|\.css$|\.html$/,
+          //压缩超过此大小的文件,以字节为单位
+          threshold: 10240,
+          minRatio: 0.8,
+          //删除原始文件只保留压缩后的文件
+          //deleteOriginalAssets: false
+        })
+      );
+      config.externals = {
+        vue: 'Vue',
+        'vue-router': 'VueRouter',
+        vuex: 'Vuex',
+        axios: 'axios',
+        moment: 'moment',
+        lodash: '_',
+        vant: 'vant',
+      };
+    }
+  },
+
+  devServer: {
+    port: '9801',
+    //api地址前缀
+    proxy: {
+      '/files': {
+        target: 'http://broadcast.waityou24.cn',
+      },
+      '/wxgateway': {
+        target: 'http://broadcast.waityou24.cn', //http://192.168.1.19:9101
+        changeOrigin: true,
+        ws: false,
+      },
+      '/api': {
+        target: 'http://192.168.1.19:11001', //http://192.168.1.19:9101
+        changeOrigin: true,
+        ws: false,
+      },
+    },
+  },
+};