lrf402788946 3 years ago
parent
commit
3a66598582
50 changed files with 3272 additions and 293 deletions
  1. 3 0
      .env
  2. 33 0
      .eslintrc.js
  3. 1 0
      .gitignore
  4. 330 88
      package-lock.json
  5. 10 1
      package.json
  6. 15 5
      public/index.html
  7. 18 27
      src/App.vue
  8. 1 0
      src/assets/logo.svg
  9. 0 130
      src/components/HelloWorld.vue
  10. 67 0
      src/components/e-upload.vue
  11. 52 0
      src/components/filter-page-table.md
  12. 390 0
      src/components/filter-page-table.vue
  13. 85 0
      src/components/form.md
  14. 213 0
      src/components/form.vue
  15. 141 0
      src/components/wangEditor.vue
  16. 40 0
      src/layout/admin.vue
  17. 51 0
      src/layout/admin/head.vue
  18. 46 0
      src/layout/admin/head/avatar.vue
  19. 70 0
      src/layout/admin/head/position.vue
  20. 40 0
      src/layout/admin/side.vue
  21. 66 0
      src/layout/admin/side/menu.vue
  22. 10 6
      src/main.js
  23. 19 0
      src/plugins/axios.js
  24. 39 0
      src/plugins/check-res.js
  25. 12 0
      src/plugins/components.js
  26. 4 0
      src/plugins/element.js
  27. 4 0
      src/plugins/meta.js
  28. 21 0
      src/plugins/setting.js
  29. 65 0
      src/plugins/stomp.js
  30. 49 16
      src/router/index.js
  31. 105 0
      src/router/meta-check.js
  32. 49 0
      src/store/api/dining/arrange.js
  33. 44 0
      src/store/api/dining/menu.js
  34. 59 0
      src/store/api/system/admin.js
  35. 44 0
      src/store/api/system/tenant.js
  36. 12 6
      src/store/index.js
  37. 28 0
      src/store/setting/frame.js
  38. 17 0
      src/store/setting/user.js
  39. 119 0
      src/util/axios-wrapper.js
  40. 69 0
      src/util/user-util.js
  41. 0 5
      src/views/About.vue
  42. 27 9
      src/views/Home.vue
  43. 41 0
      src/views/arrange/index.vue
  44. 82 0
      src/views/arrange/mobile.vue
  45. 100 0
      src/views/arrange/pc.vue
  46. 105 0
      src/views/arrange/pc/meal-table.vue
  47. 79 0
      src/views/login.vue
  48. 217 0
      src/views/menu/index.vue
  49. 144 0
      src/views/site/index.vue
  50. 36 0
      vue.config.js

+ 3 - 0
.env

@@ -0,0 +1,3 @@
+VUE_APP_AXIOS_BASE_URL = ''
+VUE_APP_ROUTER="shitang"
+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 - 0
.gitignore

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

+ 330 - 88
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.6",
+      "resolved": "https://registry.nlark.com/@babel/runtime-corejs3/download/@babel/runtime-corejs3-7.14.6.tgz?cache=0&sync_timestamp=1623708222156&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40babel%2Fruntime-corejs3%2Fdownload%2F%40babel%2Fruntime-corejs3-7.14.6.tgz",
+      "integrity": "sha1-BmuWbtpASBdAGAyzyquGGj8gjNM=",
+      "requires": {
+        "core-js-pure": "^3.14.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",
@@ -1752,6 +1760,63 @@
           "integrity": "sha1-/q7SVZc9LndVW4PbwIhRpsY1IPo=",
           "dev": true
         },
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.nlark.com/ansi-styles/download/ansi-styles-4.3.0.tgz?cache=0&sync_timestamp=1618995547052&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fansi-styles%2Fdownload%2Fansi-styles-4.3.0.tgz",
+          "integrity": "sha1-7dgDYornHATIWuegkG7a00tkiTc=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.1",
+          "resolved": "https://registry.nlark.com/chalk/download/chalk-4.1.1.tgz?cache=0&sync_timestamp=1618995355917&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fchalk%2Fdownload%2Fchalk-4.1.1.tgz",
+          "integrity": "sha1-yAs/qyi/Y3HmhjMl7uZ+YYt35q0=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-2.0.1.tgz",
+          "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.4.tgz",
+          "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=",
+          "dev": true,
+          "optional": true
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npm.taobao.org/has-flag/download/has-flag-4.0.0.tgz?cache=0&sync_timestamp=1618559744568&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhas-flag%2Fdownload%2Fhas-flag-4.0.0.tgz",
+          "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=",
+          "dev": true,
+          "optional": true
+        },
+        "loader-utils": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npm.taobao.org/loader-utils/download/loader-utils-2.0.0.tgz",
+          "integrity": "sha1-5MrOW4FtQloWa18JfhDNErNgZLA=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "big.js": "^5.2.2",
+            "emojis-list": "^3.0.0",
+            "json5": "^2.1.2"
+          }
+        },
         "ssri": {
           "version": "8.0.1",
           "resolved": "https://registry.nlark.com/ssri/download/ssri-8.0.1.tgz?cache=0&sync_timestamp=1621364918494&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fssri%2Fdownload%2Fssri-8.0.1.tgz",
@@ -1760,6 +1825,28 @@
           "requires": {
             "minipass": "^3.1.1"
           }
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.nlark.com/supports-color/download/supports-color-7.2.0.tgz?cache=0&sync_timestamp=1622293630895&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsupports-color%2Fdownload%2Fsupports-color-7.2.0.tgz",
+          "integrity": "sha1-G33NyzK4E4gBs+R4umpRyqiWSNo=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        },
+        "vue-loader-v16": {
+          "version": "npm:vue-loader@16.2.0",
+          "resolved": "https://registry.nlark.com/vue-loader/download/vue-loader-16.2.0.tgz?cache=0&sync_timestamp=1620717743226&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fvue-loader%2Fdownload%2Fvue-loader-16.2.0.tgz",
+          "integrity": "sha1-BGpTMI3Ufljv4g3ewe3sAnzjtG4=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "chalk": "^4.1.0",
+            "hash-sum": "^2.0.0",
+            "loader-utils": "^2.0.0"
+          }
         }
       }
     },
@@ -2330,6 +2417,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 +2464,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 +2486,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 +2542,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",
+          "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 +2922,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",
@@ -3705,6 +3839,11 @@
         }
       }
     },
+    "core-js-pure": {
+      "version": "3.14.0",
+      "resolved": "https://registry.nlark.com/core-js-pure/download/core-js-pure-3.14.0.tgz",
+      "integrity": "sha1-crz6y6dKZf/OBL+UrpHZZugO5VM="
+    },
     "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 +4227,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 +4658,30 @@
         "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"
+      }
+    },
+    "echarts": {
+      "version": "5.1.2",
+      "resolved": "https://registry.nlark.com/echarts/download/echarts-5.1.2.tgz",
+      "integrity": "sha1-qhqwzvW3T6L3xiAmGl8oaJPTD9E=",
+      "requires": {
+        "tslib": "2.0.3",
+        "zrender": "5.1.1"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.0.3",
+          "resolved": "https://registry.nlark.com/tslib/download/tslib-2.0.3.tgz",
+          "integrity": "sha1-jgdBrEX8DCJuWKF7/D5kubxsphw="
+        }
+      }
+    },
     "ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npm.taobao.org/ee-first/download/ee-first-1.1.1.tgz",
@@ -4538,6 +4700,19 @@
       "integrity": "sha1-ByhYfxublw7J/62TJJZCmu91DQk=",
       "dev": true
     },
+    "element-ui": {
+      "version": "2.15.2",
+      "resolved": "https://registry.nlark.com/element-ui/download/element-ui-2.15.2.tgz?cache=0&sync_timestamp=1622187007420&other_urls=https%3A%2F%2Fregistry.nlark.com%2Felement-ui%2Fdownload%2Felement-ui-2.15.2.tgz",
+      "integrity": "sha1-G0xK9YKjcGHefYFGBHo08AmbUsw=",
+      "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 +5627,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 +7073,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 +7109,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 +7336,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 +7350,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 +7398,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 +7738,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 +7760,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 +7795,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 +7969,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 +9413,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 +9643,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 +9759,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 +10837,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",
@@ -11150,84 +11426,18 @@
         }
       }
     },
-    "vue-loader-v16": {
-      "version": "npm:vue-loader@16.2.0",
-      "resolved": "https://registry.nlark.com/vue-loader/download/vue-loader-16.2.0.tgz?cache=0&sync_timestamp=1620717743226&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fvue-loader%2Fdownload%2Fvue-loader-16.2.0.tgz",
-      "integrity": "sha1-BGpTMI3Ufljv4g3ewe3sAnzjtG4=",
-      "dev": true,
-      "optional": true,
+    "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": {
-        "chalk": "^4.1.0",
-        "hash-sum": "^2.0.0",
-        "loader-utils": "^2.0.0"
+        "deepmerge": "^4.2.2"
       },
       "dependencies": {
-        "ansi-styles": {
-          "version": "4.3.0",
-          "resolved": "https://registry.nlark.com/ansi-styles/download/ansi-styles-4.3.0.tgz?cache=0&sync_timestamp=1618995547052&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fansi-styles%2Fdownload%2Fansi-styles-4.3.0.tgz",
-          "integrity": "sha1-7dgDYornHATIWuegkG7a00tkiTc=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-convert": "^2.0.1"
-          }
-        },
-        "chalk": {
-          "version": "4.1.1",
-          "resolved": "https://registry.nlark.com/chalk/download/chalk-4.1.1.tgz?cache=0&sync_timestamp=1618995355917&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fchalk%2Fdownload%2Fchalk-4.1.1.tgz",
-          "integrity": "sha1-yAs/qyi/Y3HmhjMl7uZ+YYt35q0=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "ansi-styles": "^4.1.0",
-            "supports-color": "^7.1.0"
-          }
-        },
-        "color-convert": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-2.0.1.tgz",
-          "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-name": "~1.1.4"
-          }
-        },
-        "color-name": {
-          "version": "1.1.4",
-          "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.4.tgz",
-          "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=",
-          "dev": true,
-          "optional": true
-        },
-        "has-flag": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npm.taobao.org/has-flag/download/has-flag-4.0.0.tgz?cache=0&sync_timestamp=1618559744568&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhas-flag%2Fdownload%2Fhas-flag-4.0.0.tgz",
-          "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=",
-          "dev": true,
-          "optional": true
-        },
-        "loader-utils": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npm.taobao.org/loader-utils/download/loader-utils-2.0.0.tgz",
-          "integrity": "sha1-5MrOW4FtQloWa18JfhDNErNgZLA=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "big.js": "^5.2.2",
-            "emojis-list": "^3.0.0",
-            "json5": "^2.1.2"
-          }
-        },
-        "supports-color": {
-          "version": "7.2.0",
-          "resolved": "https://registry.nlark.com/supports-color/download/supports-color-7.2.0.tgz?cache=0&sync_timestamp=1622293630895&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsupports-color%2Fdownload%2Fsupports-color-7.2.0.tgz",
-          "integrity": "sha1-G33NyzK4E4gBs+R4umpRyqiWSNo=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "has-flag": "^4.0.0"
-          }
+        "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="
         }
       }
     },
@@ -11275,6 +11485,23 @@
       "resolved": "https://registry.nlark.com/vuex/download/vuex-3.6.2.tgz",
       "integrity": "sha1-I2vAhqhww655lG8QfxbeWdWJXnE="
     },
+    "wangeditor": {
+      "version": "4.7.3",
+      "resolved": "https://registry.nlark.com/wangeditor/download/wangeditor-4.7.3.tgz?cache=0&sync_timestamp=1623331686029&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fwangeditor%2Fdownload%2Fwangeditor-4.7.3.tgz",
+      "integrity": "sha1-E1QHlgy3KMsQyFDmhVVwSegH2H4=",
+      "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",
@@ -12162,6 +12389,21 @@
           "dev": true
         }
       }
+    },
+    "zrender": {
+      "version": "5.1.1",
+      "resolved": "https://registry.nlark.com/zrender/download/zrender-5.1.1.tgz",
+      "integrity": "sha1-BRX0+MwPR0LwKmuIGVUKbRPWTFw=",
+      "requires": {
+        "tslib": "2.0.3"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.0.3",
+          "resolved": "https://registry.nlark.com/tslib/download/tslib-2.0.3.tgz",
+          "integrity": "sha1-jgdBrEX8DCJuWKF7/D5kubxsphw="
+        }
+      }
     }
   }
 }

+ 10 - 1
package.json

@@ -8,10 +8,19 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
+    "axios": "^0.21.1",
     "core-js": "^3.6.5",
+    "echarts": "^5.1.2",
+    "element-ui": "^2.15.2",
+    "jsonwebtoken": "^8.5.1",
+    "lodash": "^4.17.21",
+    "moment": "^2.29.1",
+    "naf-core": "^0.1.2",
     "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.3"
   },
   "devDependencies": {
     "@vue/cli-plugin-babel": "~4.5.0",

+ 15 - 5
public/index.html

@@ -1,15 +1,25 @@
 <!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>
+  <style>
+    body {
+      margin: 0;
+      padding: 0;
+    }
+  </style>
   <body>
     <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>

File diff suppressed because it is too large
+ 1 - 0
src/assets/logo.svg


+ 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>

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

@@ -0,0 +1,67 @@
+<template>
+  <div id="e-upload">
+    <el-upload :action="url" :http-request="upload" list-type="picture-card" :fileList="fileList" :on-remove="onRemove">
+      <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 },
+    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);
+    },
+  },
+  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的回调方法|

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

@@ -0,0 +1,390 @@
+<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;
+      this.$emit('query', { skip: 0, limit: this.limit, ...this.searchInfo });
+    },
+    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}|上传成功返回

+ 213 - 0
src/components/form.vue

@@ -0,0 +1,213 @@
+<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"
+    >
+      <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="radios" 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="checkboxs" 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 },
+  },
+  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 });
+    },
+  },
+};
+</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: 10000 }, //显示层级优先度
+    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>

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

@@ -0,0 +1,51 @@
+<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">
+      <bPosition />
+    </el-col>
+
+    <el-col :span="2" style="text-align: right">
+      <avatar />
+    </el-col>
+  </el-row>
+</template>
+
+<script>
+import bPosition from './head/position.vue';
+import avatar from './head/avatar.vue';
+import { mapState, mapMutations, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'topHead',
+  props: {},
+  components: { bPosition, 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>

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

@@ -0,0 +1,66 @@
+<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="/home">
+        <i class="el-icon-s-grid"></i>
+        <span slot="title">首页</span>
+      </el-menu-item>
+      <el-menu-item index="/site">
+        <i class="el-icon-setting"></i>
+        <span slot="title">站点设置</span>
+      </el-menu-item>
+      <el-menu-item index="/menu">
+        <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>
+  </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 - 6
src/main.js

@@ -1,12 +1,16 @@
-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/axios';
+import './plugins/check-res';
+import './plugins/meta';
+import './plugins/element';
+import './plugins/components';
 Vue.config.productionTip = false;
 
 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 });

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

@@ -0,0 +1,39 @@
+/* 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.success(_okText);
+        }
+        return true;
+      }
+      if (_.isFunction(_errText)) {
+        return _errText();
+      }
+      Message.error(_errText || errmsg);
+      // Message({ message: _errText || errmsg, duration: 60000 });
+      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);

+ 4 - 0
src/plugins/element.js

@@ -0,0 +1,4 @@
+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);
+};

+ 49 - 16
src/router/index.js

@@ -1,30 +1,63 @@
-import Vue from "vue";
-import VueRouter from "vue-router";
-import Home from "../views/Home.vue";
+import { _ } from 'core-js';
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import home from '../views/home.vue';
+import meatCheck from './meta-check';
 
 Vue.use(VueRouter);
-
+// check:需要检查的函数名
+// parent:是否需要带上父级的检查结果;只要不是false,就带着检查
 const routes = [
   {
-    path: "/",
-    name: "Home",
-    component: Home,
+    path: '/',
+    name: 'admin',
+    meta: { title: '管理端', check: 'isLogin' },
+    component: () => import(/* webpackChunkName: "login" */ '../layout/admin.vue'),
+    children: [
+      {
+        path: '/home',
+        name: 'home',
+        meta: { title: '首页', parent: true },
+        component: () => import(/* webpackChunkName: "home" */ '../views/home.vue'),
+      },
+      {
+        path: '/site',
+        name: 'site',
+        meta: { title: '站点管理', parent: true },
+        component: () => import(/* webpackChunkName: "home" */ '../views/site/index.vue'),
+      },
+      {
+        path: '/menu',
+        name: 'menu',
+        meta: { title: '菜单管理', parent: true },
+        component: () => import(/* webpackChunkName: "home" */ '../views/menu/index.vue'),
+      },
+      {
+        path: '/arrange',
+        name: 'arrange',
+        meta: { title: '安排管理', parent: true },
+        component: () => import(/* webpackChunkName: "home" */ '../views/arrange/index.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: '/login',
+    name: 'login',
+    meta: { title: '登陆', check: false },
+    component: () => import(/* webpackChunkName: "login" */ '../views/login.vue'),
   },
 ];
 
 const router = new VueRouter({
-  mode: "history",
-  base: process.env.BASE_URL,
+  mode: 'history',
+  base: process.env.VUE_APP_ROUTER,
   routes,
 });
 
+router.beforeEach((to, from, next) => {
+  const result = meatCheck(to, from, routes);
+  if (_.isString(result)) next(result);
+  next();
+});
+
 export default router;

+ 105 - 0
src/router/meta-check.js

@@ -0,0 +1,105 @@
+//检查meta中的check,应对各种情况是否允许通过
+import store from '@/store';
+const _ = require('lodash');
+const jwt = require('jsonwebtoken');
+
+/**
+ * 检查是否登录
+ */
+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;
+};
+
+/**
+ * 查询是否是子路由,及子路由是否需要继承父级的检查结果
+ * @param {Object} to 前往的路由信息
+ * @param {Array} routes 所有路由集合
+ */
+const checkChild = (to, routes) => {
+  let result = [];
+  for (const route of routes) {
+    // 先判断是否是该路由 && _.get(route.path.children, 'length', 0) > 0
+    if (route.path === to.path) {
+      const check = getCheck(route);
+      if (useCheck(check)) {
+        result = false;
+        break;
+      } else {
+        result.push(route);
+        break;
+      }
+    } else if (_.isArray(route.children) && _.get(route.children, 'length', 0) > 0) {
+      // 如果父级有需要检验的,就推进result中
+      const res = checkChild(to, route.children);
+      if (res) {
+        // 这个返回值就是if部分:
+        // 1,false:不需要检查;
+        // 2,是route,需要检查;推进result
+        // 继续检查是否要父级检查,该路由的parent字段,只要不是false,就带父级检查
+        result.push(...res); //返回的是数组,解开都扔里
+        if (to.parent !== false) {
+          const pcheck = getCheck(route);
+          if (pcheck) result.push(route);
+        }
+      }
+    }
+  }
+  return result;
+};
+/**
+ * 获取路由的检查字段内容
+ * @param {Object} route 路由
+ */
+const getCheck = (route) => {
+  return _.get(route, 'meta.check');
+};
+
+/**
+ * 抽出检查check方法
+ * @param {Any} check 路由中的check字段
+ * @returns 如果不是false,那就是需要检查
+ */
+const useCheck = (check) => {
+  return _.isBoolean(check) && !check;
+};
+/**
+ * 错误集合解释及处理
+ */
+const errorList = [{ function: 'isLogin', to: 'login', message: '未检测到用户登陆' }];
+
+export default (to, from, routes) => {
+  const rs = checkChild(to, routes);
+  if (_.isBoolean(rs)) return rs;
+  let result = true;
+  for (const route of rs) {
+    const check = _.get(route, 'meta.check');
+    if (useCheck(check)) continue;
+    else if (_.isString(check)) {
+      // 字符串为函数名,是上面某个函数的结果,执行就完事了
+      result = eval(check)();
+      if (!result) {
+        const deal = errorList.find((f) => f.function === check);
+        if (!deal) console.warn(`meta -> ${check} 情况没有对应处理的方式`);
+        else {
+          const { to, message } = deal;
+          console.error(message);
+          result = to;
+        }
+        break;
+      }
+    }
+  }
+  return result;
+};

+ 49 - 0
src/store/api/dining/arrange.js

@@ -0,0 +1,49 @@
+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/st/dining/arrange`,
+};
+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 getByDate({ commit }, date) {
+    const res = await this.$axios.$get(`${api.interface}/getByDate`, { date });
+    return res;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 44 - 0
src/store/api/dining/menu.js

@@ -0,0 +1,44 @@
+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/st/dining/menu`,
+};
+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;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 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/st/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,
+};

+ 44 - 0
src/store/api/system/tenant.js

@@ -0,0 +1,44 @@
+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/st/system/tenant`,
+};
+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;
+  },
+};
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 12 - 6
src/store/index.js

@@ -1,11 +1,17 @@
-import Vue from "vue";
-import Vuex from "vuex";
-
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { fstate, opera, fgetter } from './setting/frame';
+import { ustate, uopera } from './setting/user';
+import admin from './api/system/admin';
+import tenant from './api/system/tenant';
+import menu from './api/dining/menu';
+import arrange from './api/dining/arrange';
 Vue.use(Vuex);
 
 export default new Vuex.Store({
-  state: {},
-  mutations: {},
+  state: { ...fstate, ...ustate },
+  mutations: { ...opera, ...uopera },
   actions: {},
-  modules: {},
+  getters: { ...fgetter },
+  modules: { admin, tenant, menu, arrange },
 });

+ 28 - 0
src/store/setting/frame.js

@@ -0,0 +1,28 @@
+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();
+  },
+};
+
+export const fgetter = {
+  // 这个tenant是当前用户选择的tenant
+  tenant() {
+    const tenant = localStorage.getItem('tenant');
+    return tenant;
+  },
+};

+ 17 - 0
src/store/setting/user.js

@@ -0,0 +1,17 @@
+// 用户部分
+export const ustate = {
+  user: undefined,
+  menuList: [],
+};
+
+export const uopera = {
+  setUser: (state, payload) => {
+    state.user = payload;
+  },
+  deleteUser: (state, payload) => {
+    state.user = undefined;
+    state.menuList = [];
+    localStorage.removeItem('token');
+    localStorage.removeItem('tenant');
+  },
+};

+ 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>

+ 27 - 9
src/views/Home.vue

@@ -1,18 +1,36 @@
 <template>
-  <div class="home">
-    <img alt="Vue logo" src="../assets/logo.png" />
-    <HelloWorld msg="Welcome to Your Vue.js App" />
+  <div id="home">
+    <p>home</p>
   </div>
 </template>
 
 <script>
-// @ is an alias to /src
-import HelloWorld from "@/components/HelloWorld.vue";
-
+import { mapState, createNamespacedHelpers } from 'vuex';
 export default {
-  name: "Home",
-  components: {
-    HelloWorld,
+  name: 'home',
+  props: {},
+  components: {},
+  data: function () {
+    return {
+      list: [],
+    };
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    async search() {},
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
   },
 };
 </script>
+
+<style lang="less" scoped></style>

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

@@ -0,0 +1,41 @@
+<template>
+  <div id="index">
+    <el-card>
+      <component :is="view"></component>
+    </el-card>
+  </div>
+</template>
+
+<script>
+const moment = require('moment');
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'index',
+  props: {},
+  components: {
+    pcView: () => import('./pc.vue'),
+    mobileView: () => import('./mobile.vue'),
+  },
+  data: function () {
+    return {
+      info: {},
+    };
+  },
+  created() {},
+  methods: {},
+  computed: {
+    ...mapState(['user', 'isMobile']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+    view() {
+      return 'pcView'; //this.isMobile ? 'mobileView' : 'pcView'
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 82 - 0
src/views/arrange/mobile.vue

@@ -0,0 +1,82 @@
+<template>
+  <div id="mobile">
+    <!-- <el-row type="flex" justify="space-between" align="middle">
+      <el-col :span="12">
+        <h4>{{ toFormat('title', data.title) }}</h4>
+      </el-col>
+      <el-col :span="13" style="text-align: right">
+        <el-button-group>
+          <el-button size="mini" type="primary" icon="el-icon-arrow-left" @click="toLast"></el-button>
+          <el-button size="mini" type="primary" @click="toToday">今天</el-button>
+          <el-button size="mini" type="primary" icon="el-icon-arrow-right" @click="toNext"></el-button>
+        </el-button-group>
+      </el-col>
+    </el-row>
+    <el-divider></el-divider>
+    <el-table :data="list" empty-text=" ">
+      <el-table-column v-for="(wd, index) in weekDay" :key="`wd-${index}`" align="center" :label="wd.label" :prop="wd.key"></el-table-column>
+    </el-table> -->
+  </div>
+</template>
+
+<script>
+const moment = require('moment');
+moment.locale('zh-cn');
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'mobile',
+  props: {},
+  components: {},
+  data: function () {
+    return {
+      weekDay: [
+        { label: '一', key: 'monday' },
+        { label: '二', key: 'tuesday' },
+        { label: '三', key: 'wednesday' },
+        { label: '四', key: 'thursday' },
+        { label: '五', key: 'friday' },
+        { label: '六', key: 'saturday' },
+        { label: '日', key: 'sunday' },
+      ],
+    };
+  },
+  created() {},
+  methods: {
+    toFormat(type, value) {
+      if (type === 'title') return moment(value).format('YYYY年MM月');
+    },
+    toLast() {},
+    toToday() {},
+    toNext() {},
+    test() {
+      const dup = _.cloneDeep(this.data);
+      let start = `${moment(dup.view).format('YYYY-MM')}-01`;
+      let end = moment(start).add(1, 'month').format('YYYY-MM-DD');
+      const arr = [];
+      arr.push({ [this.getWeekDay(start)]: start });
+      while (!moment(start).isSameOrAfter(end)) {
+        start = moment(start).add(1, 'days').format('YYYY-MM-DD');
+        arr.push({ [this.getWeekDay(start)]: start });
+      }
+      console.log(arr);
+      return arr;
+    },
+    getWeekDay(date) {
+      const weekday = moment(date).weekday();
+      const key = _.get(this.weekDay[weekday], 'key');
+      return key;
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 100 - 0
src/views/arrange/pc.vue

@@ -0,0 +1,100 @@
+<template>
+  <div id="pc">
+    <el-row :gutter="10">
+      <el-col :span="8">
+        <el-calendar v-model="date" />
+      </el-col>
+      <el-col :span="16">
+        <el-tabs v-model="active" type="card">
+          <el-tab-pane label="早餐" name="breakfast">
+            <mealTable v-model="form.arrange.breakfast" @save="toSave" />
+          </el-tab-pane>
+          <el-tab-pane label="午餐" name="lunch">
+            <mealTable v-model="form.arrange.lunch" @save="toSave" />
+          </el-tab-pane>
+          <el-tab-pane label="晚餐" name="dinner">
+            <mealTable v-model="form.arrange.dinner" @save="toSave" />
+          </el-tab-pane>
+        </el-tabs>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import mealTable from './pc/meal-table.vue';
+const _ = require('lodash');
+const moment = require('moment');
+moment.locale('zh-cn');
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: arrange } = createNamespacedHelpers('arrange');
+export default {
+  name: 'pc',
+  props: {},
+  components: { mealTable },
+  data: function () {
+    return {
+      date: new Date(),
+      active: 'breakfast',
+      form: {
+        arrange: {
+          breakfast: [],
+          lunch: [],
+          dinner: [],
+        },
+      },
+    };
+  },
+  created() {},
+  methods: {
+    ...arrange(['getByDate', 'create', 'update']),
+    async getArrange(val) {
+      const date = moment(val).format('YYYY-MM-DD');
+      const res = await this.getByDate(date);
+      if (this.$checkRes(res)) {
+        if (res.data) this.$set(this, `form`, res.data);
+        else this.formInit();
+      }
+    },
+    async toSave() {
+      const dup = _.cloneDeep(this.form);
+      if (!dup.date) dup.date = moment(this.date).format('YYYY-MM-DD');
+      let res;
+      if (dup._id) res = await this.update(dup);
+      else res = await this.create(dup);
+      if (this.$checkRes(res, '操作成功', res.errmsg || '操作失败')) {
+        this.getArrange(this.date);
+      }
+    },
+    formInit() {
+      const form = {
+        arrange: {
+          breakfast: [],
+          lunch: [],
+          dinner: [],
+        },
+      };
+      this.$set(this, `form`, form);
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  watch: {
+    date: {
+      immediate: true,
+      handler(val) {
+        this.getArrange(val);
+      },
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 105 - 0
src/views/arrange/pc/meal-table.vue

@@ -0,0 +1,105 @@
+<template>
+  <div id="meal-table">
+    <el-row>
+      <el-col :span="24" style="text-align: right">
+        <el-button type="primary" size="mini" @click="dialog = true">添加</el-button>
+      </el-col>
+      <el-col :span="24">
+        <data-table :fields="fields" :opera="opera" :data="value" :usePage="false" operaWidth="auto" @delete="toDelete" />
+      </el-col>
+    </el-row>
+    <el-dialog title="参数编辑" :visible.sync="dialog" :width="width">
+      <data-table :fields="fields" :opera="operaDialog" :data="menuList" :usePage="false" operaWidth="auto" @select="toSelect" :height="height" />
+      <template #footer>
+        <el-row>
+          <el-col :span="24" style="text-align: center">
+            <el-button type="danger" size="mini" @click="dialog = false">关闭</el-button>
+          </el-col>
+        </el-row>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: menu } = createNamespacedHelpers('menu');
+export default {
+  name: 'meal-table',
+  props: {
+    value: { type: Array },
+  },
+  model: {
+    prop: 'value',
+    event: 'change',
+  },
+  components: {},
+  data: function () {
+    return {
+      dialog: false,
+      fields: [
+        { label: '菜品', model: 'name' },
+        { label: '卡路里(大卡)', model: 'reserve' },
+      ],
+      opera: [
+        {
+          label: '删除',
+          type: 'danger',
+          method: 'delete',
+        },
+      ],
+      operaDialog: [
+        {
+          label: '选择',
+          method: 'select',
+          display: (i) => this.isSelect(i),
+        },
+      ],
+      menuList: [],
+    };
+  },
+  created() {
+    this.getMenuList();
+  },
+  methods: {
+    ...menu(['query', 'update', 'create']),
+    toDelete({ index }) {
+      this.value.splice(index, 1);
+      this.$emit('save');
+    },
+    toSelect({ data }) {
+      this.value.push(data);
+      this.$emit('save');
+    },
+    async getMenuList() {
+      const res = await this.query({ is_use: true });
+      if (this.$checkRes(res)) {
+        res.data = [...res.data];
+        this.$set(this, `menuList`, res.data);
+      }
+    },
+    isSelect(i) {
+      const res = this.value.find((f) => f._id === i._id);
+      if (res) return false;
+      return true;
+    },
+  },
+  computed: {
+    ...mapState(['user', 'isMobile']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+    width() {
+      return this.isMobile ? '90%' : '30%';
+    },
+    height() {
+      return this.isMobile ? '300px' : '500px';
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 79 - 0
src/views/login.vue

@@ -0,0 +1,79 @@
+<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');
+const { mapActions: tenant } = createNamespacedHelpers('tenant');
+export default {
+  name: 'login',
+  props: {},
+  components: { sForm },
+  data: function () {
+    return {
+      form: {},
+      siteList: [],
+      fields: [
+        { label: '站点选择', model: 'tenant', type: 'select', required: true },
+        { label: '登陆用户名', model: 'login_id', required: true },
+        { label: '密码', model: 'password', type: 'password', required: true },
+      ],
+      rules: {
+        tenant: [{ required: true, message: '请选择站点', trigger: 'blur' }],
+        login_id: [{ required: true, message: '请输入登录用户名', trigger: 'blur' }],
+        password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+      },
+    };
+  },
+  created() {
+    this.init();
+  },
+  methods: {
+    ...admin(['login']),
+    ...tenant(['query']),
+    async toLogin({ data }) {
+      const { tenant, ...info } = data;
+      localStorage.setItem('tenant', tenant);
+      const res = await this.login(info);
+      if (res) this.$router.push('/home');
+    },
+    async init() {
+      const res = await this.query();
+      if (this.$checkRes(res)) {
+        this.$set(this, `siteList`, res.data);
+      }
+    },
+  },
+  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>

+ 217 - 0
src/views/menu/index.vue

@@ -0,0 +1,217 @@
+<template>
+  <div id="index">
+    <data-table
+      v-if="view === 'list'"
+      :fields="fields"
+      :opera="opera"
+      :data="list"
+      :total="total"
+      @query="search"
+      operaWidth="auto"
+      @edit="toEdit"
+      @use="toUse"
+      @delete="toDelete"
+    >
+      <template #btn>
+        <el-button type="primary" size="mini" @click="toAdd">添加</el-button>
+      </template>
+    </data-table>
+
+    <div v-else>
+      <el-row type="flex" align="middle" justify="end">
+        <el-col :span="4">
+          <el-button type="primary" @click="view = 'list'" size="mini">返回</el-button>
+        </el-col>
+      </el-row>
+      <el-row type="flex" align="middle" justify="space-around">
+        <el-col :span="span">
+          <data-form :fields="formFields" v-model="form" @save="evalEdit" labelWidth="70px">
+            <template #params>
+              <el-row>
+                <el-col :span="24" style="text-align: right">
+                  <el-button type="primary" size="mini" @click="dialog = true">添加</el-button>
+                </el-col>
+                <el-col :span="24">
+                  <data-table
+                    :fields="paramsFields"
+                    :data="form.params"
+                    :usePage="false"
+                    :opera="paramsOpera"
+                    @delete="paramsDelete"
+                    operaWidth="auto"
+                  ></data-table>
+                </el-col>
+              </el-row>
+            </template>
+            <template #img>
+              <e-upload v-model="form.img" url="/files/st/test/upload"></e-upload>
+            </template>
+          </data-form>
+        </el-col>
+      </el-row>
+    </div>
+
+    <el-dialog title="参数编辑" :visible.sync="dialog" :width="width">
+      <el-input style="padding: 5px 0" v-model="dialogForm.label" placeholder="请填写参数名" size="mini"></el-input>
+      <el-input style="padding: 5px 0" v-model="dialogForm.value" placeholder="请填写参数值" size="mini"></el-input>
+      <template #footer>
+        <el-button type="primary" @click="paramsSave" size="mini">保存参数</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+const _ = require('lodash');
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: menu } = createNamespacedHelpers('menu');
+export default {
+  name: 'index',
+  props: {},
+  components: {},
+  data: function () {
+    return {
+      view: 'list',
+      list: [],
+      total: 0,
+      fields: [
+        { label: '菜品', model: 'name', filter: true },
+        { label: '卡路里(大卡)', model: 'reserve' },
+        { label: '是否启用', model: 'is_use', format: (i) => (i ? '启用' : '禁用'), noForm: true },
+      ],
+      opera: [
+        {
+          label: '编辑',
+          method: 'edit',
+          display: (i) => !i.is_use,
+        },
+        {
+          label: '启用',
+          method: 'use',
+          display: (i) => !i.is_use,
+        },
+        {
+          label: '禁用',
+          type: 'warning',
+          method: 'use',
+          display: (i) => i.is_use,
+        },
+        {
+          label: '删除',
+          type: 'danger',
+          method: 'delete',
+          display: (i) => !i.is_use,
+        },
+      ],
+      formFields: [
+        { label: '菜品', model: 'name' },
+        { label: '卡路里(大卡)', model: 'reserve' },
+        { label: '参数列表', model: 'params', custom: true },
+        { label: '图片', model: 'img', custom: true },
+        { label: '菜品介绍', model: 'content', type: 'textarea' },
+      ],
+      form: {
+        params: [],
+        img: [],
+      },
+      paramsFields: [
+        { label: '参数名', model: 'label' },
+        { label: '参数值', model: 'value' },
+      ],
+      paramsOpera: [
+        {
+          label: '删除',
+          type: 'danger',
+          method: 'delete',
+        },
+      ],
+      dialog: false,
+      dialogForm: {},
+    };
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    ...menu(['query', 'update', 'create']),
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      const res = await this.query({ skip, limit, ...info });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, res.data);
+        this.$set(this, `total`, res.total);
+      }
+    },
+    toAdd() {
+      this.view = 'detail';
+      this.formInit();
+    },
+    toEdit({ data }) {
+      let dup = _.cloneDeep(data);
+      this.$set(this, `form`, dup);
+      this.view = 'detail';
+    },
+    async toUse({ data }) {
+      let dup = _.cloneDeep(data);
+      dup.is_use = !dup.is_use;
+      this.evalEdit({ data: dup });
+    },
+    async evalEdit({ data }) {
+      let dup = _.cloneDeep(data);
+      let res;
+      if (dup._id) res = await this.update(dup);
+      else res = await this.create(dup);
+      if (this.$checkRes(res, '操作成功', res.errmsg || '操作失败')) {
+        this.search();
+        this.view = 'list';
+      }
+    },
+    async toDelete({ data }) {
+      this.$confirm('此操作将删除该数据, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        center: true,
+      })
+        .then(async () => {
+          const res = await this.delete(data);
+          if (this.$checkRes(res, '删除成功', res.errmsg || '删除失败')) {
+            this.search();
+          }
+        })
+        .catch(() => {});
+    },
+    paramsDelete({ index }) {
+      this.form.params.splice(index, 1);
+    },
+    paramsSave() {
+      let dup = _.cloneDeep(this.dialogForm);
+      this.form.params.push(dup);
+      this.dialogForm = {};
+      this.dialog = false;
+    },
+    formInit() {
+      this.form = {
+        params: [],
+        img: [],
+      };
+    },
+  },
+  computed: {
+    ...mapState(['user', 'isMobile']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+    span() {
+      return this.isMobile ? 24 : 12;
+    },
+    width() {
+      return this.isMobile ? '70%' : '30%';
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 144 - 0
src/views/site/index.vue

@@ -0,0 +1,144 @@
+<template>
+  <div id="index">
+    <data-table
+      v-if="view === 'list'"
+      :fields="fields"
+      :opera="opera"
+      :data="list"
+      :total="total"
+      @query="search"
+      operaWidth="auto"
+      @edit="toEdit"
+      @use="toUse"
+      @delete="toDelete"
+    >
+      <template #btn>
+        <el-button type="primary" size="mini" @click="toAdd">添加</el-button>
+      </template>
+    </data-table>
+    <div v-else>
+      <el-row type="flex" align="middle" justify="end">
+        <el-col :span="4">
+          <el-button type="primary" @click="view = 'list'" size="mini">返回</el-button>
+        </el-col>
+      </el-row>
+      <el-row type="flex" align="middle" justify="space-around">
+        <el-col :span="span">
+          <data-form :fields="fields" v-model="form" @save="evalEdit" labelWidth="80px"></data-form>
+        </el-col>
+      </el-row>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: tenant } = createNamespacedHelpers('tenant');
+export default {
+  name: 'index',
+  props: {},
+  components: {},
+  data: function () {
+    return {
+      view: 'list',
+      list: [],
+      total: 0,
+      fields: [
+        { label: '站点名称', model: 'name', filter: true },
+        { label: '站点标识', model: '_tenant', filter: true },
+        { label: '是否启用', model: 'is_use', format: (i) => (i ? '启用' : '禁用'), noForm: true },
+      ],
+      opera: [
+        {
+          label: '启用',
+          method: 'use',
+          display: (i) => !i.is_use && i._tenant !== 'master',
+        },
+        {
+          label: '编辑',
+          method: 'edit',
+          display: (i) => !i.is_use && i._tenant !== 'master',
+        },
+        {
+          label: '删除',
+          type: 'danger',
+          method: 'delete',
+          display: (i) => !i.is_use && i._tenant !== 'master',
+        },
+        {
+          label: '禁用',
+          method: 'use',
+          display: (i) => i.is_use && i._tenant !== 'master',
+        },
+      ],
+      form: {},
+    };
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    ...tenant(['query', 'update', 'create']),
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      const res = await this.query({ skip, limit, ...info });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, res.data);
+        this.$set(this, `total`, res.total);
+      }
+    },
+    toAdd() {
+      this.view = 'detail';
+      this.form = {};
+    },
+    toEdit({ data }) {
+      let dup = _.cloneDeep(data);
+      this.$set(this, `form`, dup);
+      this.view = 'detail';
+    },
+    async toUse({ data }) {
+      let dup = _.cloneDeep(data);
+      dup.is_use = !dup.is_use;
+      this.evalEdit({ data: dup });
+    },
+    async evalEdit({ data }) {
+      let dup = _.cloneDeep(data);
+      let res;
+      if (dup._id) res = await this.update(dup);
+      else res = await this.create(dup);
+      if (this.$checkRes(res, '操作成功', res.errmsg || '操作失败')) {
+        this.search();
+        this.view = 'list';
+      }
+    },
+    async toDelete({ data }) {
+      this.$confirm('此操作将删除该数据, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        center: true,
+      })
+        .then(async () => {
+          const res = await this.delete(data);
+          if (this.$checkRes(res, '删除成功', res.errmsg || '删除失败')) {
+            this.search();
+          }
+        })
+        .catch(() => {});
+    },
+  },
+  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>

+ 36 - 0
vue.config.js

@@ -0,0 +1,36 @@
+const path = require('path');
+
+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'),
+        },
+      },
+    });
+  },
+  devServer: {
+    port: '9900',
+    //api地址前缀
+    proxy: {
+      '/files': {
+        target: 'http://broadcast.waityou24.cn',
+      },
+      '/api': {
+        target: 'http://192.168.1.19:9901', //http://192.168.1.19:9101
+        changeOrigin: true,
+        ws: false,
+      },
+    },
+  },
+  transpileDependencies: ['vuetify'],
+};