jaikuai 3 anni fa
commit
ce5673e029
100 ha cambiato i file con 5681 aggiunte e 0 eliminazioni
  1. 16 0
      .gitignore
  2. 4 0
      Dockerfile
  3. 21 0
      LICENSE
  4. 102 0
      README-zh.md
  5. 90 0
      README.md
  6. 14 0
      babel.config.js
  7. 35 0
      build/index.js
  8. 29 0
      default.conf
  9. 24 0
      jest.config.js
  10. 9 0
      jsconfig.json
  11. 57 0
      mock/index.js
  12. 81 0
      mock/mock-server.js
  13. 29 0
      mock/table.js
  14. 84 0
      mock/user.js
  15. 25 0
      mock/utils.js
  16. 62 0
      package.json
  17. 8 0
      postcss.config.js
  18. BIN
      public/favicon.ico
  19. 17 0
      public/index.html
  20. 11 0
      src/App.vue
  21. 36 0
      src/api/page.js
  22. 9 0
      src/api/table.js
  23. 23 0
      src/api/user.js
  24. BIN
      src/assets/404_images/404.png
  25. BIN
      src/assets/404_images/404_cloud.png
  26. BIN
      src/assets/head.jpg
  27. 78 0
      src/components/Breadcrumb/index.vue
  28. 54 0
      src/components/GithubCorner/index.vue
  29. 44 0
      src/components/Hamburger/index.vue
  30. 101 0
      src/components/Pagination/index.vue
  31. 62 0
      src/components/SvgIcon/index.vue
  32. 9 0
      src/icons/index.js
  33. 0 0
      src/icons/svg/dashboard.svg
  34. 1 0
      src/icons/svg/example.svg
  35. 1 0
      src/icons/svg/eye-open.svg
  36. 1 0
      src/icons/svg/eye.svg
  37. 0 0
      src/icons/svg/form.svg
  38. 1 0
      src/icons/svg/link.svg
  39. 1 0
      src/icons/svg/nested.svg
  40. 1 0
      src/icons/svg/password.svg
  41. 1 0
      src/icons/svg/table.svg
  42. 1 0
      src/icons/svg/tree.svg
  43. 1 0
      src/icons/svg/user.svg
  44. 22 0
      src/icons/svgo.yml
  45. 40 0
      src/layout/components/AppMain.vue
  46. 128 0
      src/layout/components/Navbar.vue
  47. 26 0
      src/layout/components/Sidebar/FixiOSBug.js
  48. 41 0
      src/layout/components/Sidebar/Item.vue
  49. 43 0
      src/layout/components/Sidebar/Link.vue
  50. 82 0
      src/layout/components/Sidebar/Logo.vue
  51. 95 0
      src/layout/components/Sidebar/SidebarItem.vue
  52. 56 0
      src/layout/components/Sidebar/index.vue
  53. 3 0
      src/layout/components/index.js
  54. 93 0
      src/layout/index.vue
  55. 45 0
      src/layout/mixin/ResizeHandler.js
  56. 43 0
      src/main.js
  57. 64 0
      src/permission.js
  58. 176 0
      src/router/index.js
  59. 16 0
      src/settings.js
  60. 8 0
      src/store/getters.js
  61. 19 0
      src/store/index.js
  62. 48 0
      src/store/modules/app.js
  63. 32 0
      src/store/modules/settings.js
  64. 97 0
      src/store/modules/user.js
  65. 49 0
      src/styles/element-ui.scss
  66. 65 0
      src/styles/index.scss
  67. 28 0
      src/styles/mixin.scss
  68. 226 0
      src/styles/sidebar.scss
  69. 48 0
      src/styles/transition.scss
  70. 25 0
      src/styles/variables.scss
  71. 15 0
      src/utils/auth.js
  72. 10 0
      src/utils/get-page-title.js
  73. 117 0
      src/utils/index.js
  74. 85 0
      src/utils/request.js
  75. 50 0
      src/utils/scrollTo.js
  76. 20 0
      src/utils/validate.js
  77. 228 0
      src/views/404.vue
  78. 154 0
      src/views/car/carinfo/add.vue
  79. 188 0
      src/views/car/carinfo/index.vue
  80. 246 0
      src/views/car/driver/index.vue
  81. 160 0
      src/views/car/event/vlt.vue
  82. 102 0
      src/views/dashboard/components/BarChart.vue
  83. 154 0
      src/views/dashboard/components/LineChart.vue
  84. 178 0
      src/views/dashboard/components/PanelGroup.vue
  85. 79 0
      src/views/dashboard/components/PieChart.vue
  86. 116 0
      src/views/dashboard/components/RaddarChart.vue
  87. 55 0
      src/views/dashboard/components/mixins/resize.js
  88. 31 0
      src/views/dashboard/index.vue
  89. 85 0
      src/views/form/index.vue
  90. 237 0
      src/views/login/index.vue
  91. 7 0
      src/views/nested/menu1/index.vue
  92. 7 0
      src/views/nested/menu1/menu1-1/index.vue
  93. 7 0
      src/views/nested/menu1/menu1-2/index.vue
  94. 5 0
      src/views/nested/menu1/menu1-2/menu1-2-1/index.vue
  95. 5 0
      src/views/nested/menu1/menu1-2/menu1-2-2/index.vue
  96. 5 0
      src/views/nested/menu1/menu1-3/index.vue
  97. 5 0
      src/views/nested/menu2/index.vue
  98. 306 0
      src/views/sys/sys_depart/index.vue
  99. 90 0
      src/views/sys/sys_dict/add.vue
  100. 173 0
      src/views/sys/sys_dict/index.vue

+ 16 - 0
.gitignore

@@ -0,0 +1,16 @@
+.DS_Store
+node_modules/
+/dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+package-lock.json
+

+ 4 - 0
Dockerfile

@@ -0,0 +1,4 @@
+FROM rpi.it5000.com:5000/nginx:alpine
+
+ADD default.conf /etc/nginx/conf.d/
+ADD dist /usr/share/nginx/html

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017-present PanJiaChen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 102 - 0
README-zh.md

@@ -0,0 +1,102 @@
+# vue-admin-template
+
+> 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。
+
+[线上地址](http://panjiachen.github.io/vue-admin-template)
+
+[国内访问](https://panjiachen.gitee.io/vue-admin-template)
+
+目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。
+
+## Extra
+
+如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
+
+## 相关项目
+
+- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
+
+- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
+
+- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
+
+- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
+
+写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目:
+
+- [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2)
+- [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac)
+- [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35)
+- [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56)
+- [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836)
+
+## Build Setup
+
+```bash
+# 克隆项目
+git clone https://github.com/PanJiaChen/vue-admin-template.git
+
+# 进入项目目录
+cd vue-admin-template
+
+# 安装依赖
+npm install
+
+# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
+npm install --registry=https://registry.npm.taobao.org
+
+# 启动服务
+npm run dev
+```
+
+浏览器访问 [http://localhost:9528](http://localhost:9528)
+
+## 发布
+
+```bash
+# 构建测试环境
+npm run build:stage
+
+# 构建生产环境
+npm run build:prod
+```
+
+## 其它
+
+```bash
+# 预览发布环境效果
+npm run preview
+
+# 预览发布环境效果 + 静态资源分析
+npm run preview -- --report
+
+# 代码格式检查
+npm run lint
+
+# 代码格式检查并自动修复
+npm run lint -- --fix
+```
+
+更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/)
+
+## 购买贴纸
+
+你也可以通过 购买[官方授权的贴纸](https://smallsticker.com/product/vue-element-admin) 的方式来支持 vue-element-admin - 每售出一张贴纸,我们将获得 2 元的捐赠。
+
+## Demo
+
+![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
+
+## Browsers support
+
+Modern browsers and Internet Explorer 10+.
+
+| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
+| --------- | --------- | --------- | --------- |
+| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
+
+## License
+
+[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
+
+Copyright (c) 2017-present PanJiaChen

+ 90 - 0
README.md

@@ -0,0 +1,90 @@
+# vue-admin-template
+
+English | [简体中文](./README-zh.md)
+
+> A minimal vue admin template with Element UI & axios & iconfont & permission control & lint
+
+**Live demo:** http://panjiachen.github.io/vue-admin-template
+
+
+**The current version is `v4.0+` build on `vue-cli`. If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0), it does not rely on `vue-cli`**
+
+## Build Setup
+
+```bash
+# clone the project
+git clone https://github.com/PanJiaChen/vue-admin-template.git
+
+# enter the project directory
+cd vue-admin-template
+
+# install dependency
+npm install
+
+# develop
+npm run dev
+```
+
+This will automatically open http://localhost:9528
+
+## Build
+
+```bash
+# build for test environment
+npm run build:stage
+
+# build for production environment
+npm run build:prod
+```
+
+## Advanced
+
+```bash
+# preview the release environment effect
+npm run preview
+
+# preview the release environment effect + static resource analysis
+npm run preview -- --report
+
+# code format check
+npm run lint
+
+# code format check and auto fix
+npm run lint -- --fix
+```
+
+Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information
+
+## Demo
+
+![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
+
+## Extra
+
+If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
+
+For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour))
+
+## Related Project
+
+- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
+
+- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
+
+- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
+
+- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
+
+## Browsers support
+
+Modern browsers and Internet Explorer 10+.
+
+| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
+| --------- | --------- | --------- | --------- |
+| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
+
+## License
+
+[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
+
+Copyright (c) 2017-present PanJiaChen

+ 14 - 0
babel.config.js

@@ -0,0 +1,14 @@
+module.exports = {
+  presets: [
+    // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
+    '@vue/cli-plugin-babel/preset'
+  ],
+  'env': {
+    'development': {
+      // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
+      // This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
+      // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html
+      'plugins': ['dynamic-import-node']
+    }
+  }
+}

+ 35 - 0
build/index.js

@@ -0,0 +1,35 @@
+const { run } = require('runjs')
+const chalk = require('chalk')
+const config = require('../vue.config.js')
+const rawArgv = process.argv.slice(2)
+const args = rawArgv.join(' ')
+
+if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
+  const report = rawArgv.includes('--report')
+
+  run(`vue-cli-service build ${args}`)
+
+  const port = 9526
+  const publicPath = config.publicPath
+
+  var connect = require('connect')
+  var serveStatic = require('serve-static')
+  const app = connect()
+
+  app.use(
+    publicPath,
+    serveStatic('./dist', {
+      index: ['index.html', '/']
+    })
+  )
+
+  app.listen(port, function () {
+    console.log(chalk.green(`> Preview at  http://localhost:${port}${publicPath}`))
+    if (report) {
+      console.log(chalk.green(`> Report at  http://localhost:${port}${publicPath}report.html`))
+    }
+
+  })
+} else {
+  run(`vue-cli-service build ${args}`)
+}

+ 29 - 0
default.conf

@@ -0,0 +1,29 @@
+server {
+    listen       80;
+    server_name  localhost;
+
+    #charset koi8-r;
+    access_log  /var/log/nginx/host.access.log  main;
+
+    gzip on;
+    gzip_min_length 1k;
+    gzip_buffers 4 16k;
+    gzip_comp_level 8;
+    gzip_types text/plain application/javascript application/x-javascript text/css application/json text/javascript application/x-httpd-php image/jpeg image/gif image/png;
+    gzip_vary off;
+    gzip_disable "MSIE [1-6]\.";
+
+    location / {
+        root   /usr/share/nginx/html;
+        index  start.html index.html;
+    }
+
+    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
+    #
+    location /api {
+        proxy_pass   http://server:8084;
+    }
+    location /download {
+        proxy_pass   http://server:8084;
+    }
+}

+ 24 - 0
jest.config.js

@@ -0,0 +1,24 @@
+module.exports = {
+  moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
+  transform: {
+    '^.+\\.vue$': 'vue-jest',
+    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
+      'jest-transform-stub',
+    '^.+\\.jsx?$': 'babel-jest'
+  },
+  moduleNameMapper: {
+    '^@/(.*)$': '<rootDir>/src/$1'
+  },
+  snapshotSerializers: ['jest-serializer-vue'],
+  testMatch: [
+    '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
+  ],
+  collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
+  coverageDirectory: '<rootDir>/tests/unit/coverage',
+  // 'collectCoverage': true,
+  'coverageReporters': [
+    'lcov',
+    'text-summary'
+  ],
+  testURL: 'http://localhost/'
+}

+ 9 - 0
jsconfig.json

@@ -0,0 +1,9 @@
+{
+  "compilerOptions": {
+    "baseUrl": "./",
+    "paths": {
+        "@/*": ["src/*"]
+    }
+  },
+  "exclude": ["node_modules", "dist"]
+}

+ 57 - 0
mock/index.js

@@ -0,0 +1,57 @@
+const Mock = require('mockjs')
+const { param2Obj } = require('./utils')
+
+const user = require('./user')
+const table = require('./table')
+
+const mocks = [
+  ...user,
+  ...table
+]
+
+// for front mock
+// please use it cautiously, it will redefine XMLHttpRequest,
+// which will cause many of your third-party libraries to be invalidated(like progress event).
+function mockXHR() {
+  // mock patch
+  // https://github.com/nuysoft/Mock/issues/300
+  Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
+  Mock.XHR.prototype.send = function() {
+    if (this.custom.xhr) {
+      this.custom.xhr.withCredentials = this.withCredentials || false
+
+      if (this.responseType) {
+        this.custom.xhr.responseType = this.responseType
+      }
+    }
+    this.proxy_send(...arguments)
+  }
+
+  function XHR2ExpressReqWrap(respond) {
+    return function(options) {
+      let result = null
+      if (respond instanceof Function) {
+        const { body, type, url } = options
+        // https://expressjs.com/en/4x/api.html#req
+        result = respond({
+          method: type,
+          body: JSON.parse(body),
+          query: param2Obj(url)
+        })
+      } else {
+        result = respond
+      }
+      return Mock.mock(result)
+    }
+  }
+
+  for (const i of mocks) {
+    Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
+  }
+}
+
+module.exports = {
+  mocks,
+  mockXHR
+}
+

+ 81 - 0
mock/mock-server.js

@@ -0,0 +1,81 @@
+const chokidar = require('chokidar')
+const bodyParser = require('body-parser')
+const chalk = require('chalk')
+const path = require('path')
+const Mock = require('mockjs')
+
+const mockDir = path.join(process.cwd(), 'mock')
+
+function registerRoutes(app) {
+  let mockLastIndex
+  const { mocks } = require('./index.js')
+  const mocksForServer = mocks.map(route => {
+    return responseFake(route.url, route.type, route.response)
+  })
+  for (const mock of mocksForServer) {
+    app[mock.type](mock.url, mock.response)
+    mockLastIndex = app._router.stack.length
+  }
+  const mockRoutesLength = Object.keys(mocksForServer).length
+  return {
+    mockRoutesLength: mockRoutesLength,
+    mockStartIndex: mockLastIndex - mockRoutesLength
+  }
+}
+
+function unregisterRoutes() {
+  Object.keys(require.cache).forEach(i => {
+    if (i.includes(mockDir)) {
+      delete require.cache[require.resolve(i)]
+    }
+  })
+}
+
+// for mock server
+const responseFake = (url, type, respond) => {
+  return {
+    url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`),
+    type: type || 'get',
+    response(req, res) {
+      console.log('request invoke:' + req.path)
+      res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
+    }
+  }
+}
+
+module.exports = app => {
+  // parse app.body
+  // https://expressjs.com/en/4x/api.html#req.body
+  app.use(bodyParser.json())
+  app.use(bodyParser.urlencoded({
+    extended: true
+  }))
+
+  const mockRoutes = registerRoutes(app)
+  var mockRoutesLength = mockRoutes.mockRoutesLength
+  var mockStartIndex = mockRoutes.mockStartIndex
+
+  // watch files, hot reload mock server
+  chokidar.watch(mockDir, {
+    ignored: /mock-server/,
+    ignoreInitial: true
+  }).on('all', (event, path) => {
+    if (event === 'change' || event === 'add') {
+      try {
+        // remove mock routes stack
+        app._router.stack.splice(mockStartIndex, mockRoutesLength)
+
+        // clear routes cache
+        unregisterRoutes()
+
+        const mockRoutes = registerRoutes(app)
+        mockRoutesLength = mockRoutes.mockRoutesLength
+        mockStartIndex = mockRoutes.mockStartIndex
+
+        console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed  ${path}`))
+      } catch (error) {
+        console.log(chalk.redBright(error))
+      }
+    }
+  })
+}

+ 29 - 0
mock/table.js

@@ -0,0 +1,29 @@
+const Mock = require('mockjs')
+
+const data = Mock.mock({
+  'items|30': [{
+    id: '@id',
+    title: '@sentence(10, 20)',
+    'status|1': ['published', 'draft', 'deleted'],
+    author: 'name',
+    display_time: '@datetime',
+    pageviews: '@integer(300, 5000)'
+  }]
+})
+
+module.exports = [
+  {
+    url: '/vue-admin-template/table/list',
+    type: 'get',
+    response: config => {
+      const items = data.items
+      return {
+        code: 20000,
+        data: {
+          total: items.length,
+          items: items
+        }
+      }
+    }
+  }
+]

+ 84 - 0
mock/user.js

@@ -0,0 +1,84 @@
+
+const tokens = {
+  admin: {
+    token: 'admin-token'
+  },
+  editor: {
+    token: 'editor-token'
+  }
+}
+
+const users = {
+  'admin-token': {
+    roles: ['admin'],
+    introduction: 'I am a super administrator',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Super Admin'
+  },
+  'editor-token': {
+    roles: ['editor'],
+    introduction: 'I am an editor',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Normal Editor'
+  }
+}
+
+module.exports = [
+  // user login
+  {
+    url: '/vue-admin-template/user/login',
+    type: 'post',
+    response: config => {
+      const { username } = config.body
+      const token = tokens[username]
+
+      // mock error
+      if (!token) {
+        return {
+          code: 60204,
+          message: 'Account and password are incorrect.'
+        }
+      }
+
+      return {
+        code: 20000,
+        data: token
+      }
+    }
+  },
+
+  // get user info
+  {
+    url: '/vue-admin-template/user/info\.*',
+    type: 'get',
+    response: config => {
+      const { token } = config.query
+      const info = users[token]
+
+      // mock error
+      if (!info) {
+        return {
+          code: 50008,
+          message: 'Login failed, unable to get user details.'
+        }
+      }
+
+      return {
+        code: 20000,
+        data: info
+      }
+    }
+  },
+
+  // user logout
+  {
+    url: '/vue-admin-template/user/logout',
+    type: 'post',
+    response: _ => {
+      return {
+        code: 20000,
+        data: 'success'
+      }
+    }
+  }
+]

+ 25 - 0
mock/utils.js

@@ -0,0 +1,25 @@
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+function param2Obj(url) {
+  const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
+  if (!search) {
+    return {}
+  }
+  const obj = {}
+  const searchArr = search.split('&')
+  searchArr.forEach(v => {
+    const index = v.indexOf('=')
+    if (index !== -1) {
+      const name = v.substring(0, index)
+      const val = v.substring(index + 1, v.length)
+      obj[name] = val
+    }
+  })
+  return obj
+}
+
+module.exports = {
+  param2Obj
+}

+ 62 - 0
package.json

@@ -0,0 +1,62 @@
+{
+  "name": "vue-admin-template",
+  "version": "4.4.0",
+  "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
+  "author": "Pan <panfree23@gmail.com>",
+  "scripts": {
+    "dev": "vue-cli-service serve",
+    "build:prod": "vue-cli-service build",
+    "build:stage": "vue-cli-service build --mode staging",
+    "preview": "node build/index.js --preview",
+    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
+    "lint": "eslint --ext .js,.vue src",
+    "test:unit": "jest --clearCache && vue-cli-service test:unit",
+    "test:ci": "npm run lint && npm run test:unit"
+  },
+  "dependencies": {
+    "axios": "0.18.1",
+    "core-js": "3.6.5",
+    "element-ui": "2.13.2",
+    "js-cookie": "2.2.0",
+    "normalize.css": "7.0.0",
+    "nprogress": "0.2.0",
+    "path-to-regexp": "2.4.0",
+    "vue": "2.6.10",
+    "vue-router": "3.0.6",
+    "vuex": "3.1.0"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "4.4.4",
+    "@vue/cli-plugin-eslint": "4.4.4",
+    "@vue/cli-plugin-unit-jest": "4.4.4",
+    "@vue/cli-service": "4.4.4",
+    "@vue/test-utils": "1.0.0-beta.29",
+    "autoprefixer": "9.5.1",
+    "babel-eslint": "10.1.0",
+    "babel-jest": "23.6.0",
+    "babel-plugin-dynamic-import-node": "2.3.3",
+    "chalk": "2.4.2",
+    "connect": "3.6.6",
+    "eslint": "6.7.2",
+    "eslint-plugin-vue": "6.2.2",
+    "html-webpack-plugin": "3.2.0",
+    "mockjs": "1.0.1-beta3",
+    "runjs": "4.3.2",
+    "sass": "1.26.8",
+    "sass-loader": "8.0.2",
+    "script-ext-html-webpack-plugin": "2.1.3",
+    "serve-static": "1.13.2",
+    "svg-sprite-loader": "4.1.3",
+    "svgo": "1.2.2",
+    "vue-template-compiler": "2.6.10"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions"
+  ],
+  "engines": {
+    "node": ">=8.9",
+    "npm": ">= 3.0.0"
+  },
+  "license": "MIT"
+}

+ 8 - 0
postcss.config.js

@@ -0,0 +1,8 @@
+// https://github.com/michael-ciniawsky/postcss-load-config
+
+module.exports = {
+  'plugins': {
+    // to edit target browsers: use "browserslist" field in package.json
+    'autoprefixer': {}
+  }
+}

BIN
public/favicon.ico


+ 17 - 0
public/index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title><%= webpackConfig.name %></title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 11 - 0
src/App.vue

@@ -0,0 +1,11 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'App'
+}
+</script>

+ 36 - 0
src/api/page.js

@@ -0,0 +1,36 @@
+import request from '@/utils/request'
+import { getToken } from '@/utils/auth'
+import axios from 'axios'
+import qs from 'qs'
+
+
+export function postJson(url, params) {
+  return request({
+    url: url,
+    method: 'post',
+    data: params
+  })
+}
+
+export function postForm(url, params) {
+  const service = axios.create({
+    baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
+    // withCredentials: true, // send cookies when cross-domain requests
+    timeout: 50000 // request timeout
+  })
+  return service.post( url, qs.stringify(params),{
+      headers: {
+        'X-Token': getToken()}
+    });
+}
+
+export function getSort(data) {
+  const { prop, order } = data
+  if (order === 'ascending') {
+    return prop + ' asc'
+  } else if (order === 'descending') {
+    return prop + ' desc'
+  } else {
+    return ''
+  }
+}

+ 9 - 0
src/api/table.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+export function getList(params) {
+  return request({
+    url: '/vue-admin-template/table/list',
+    method: 'get',
+    params
+  })
+}

+ 23 - 0
src/api/user.js

@@ -0,0 +1,23 @@
+import request from '@/utils/request'
+
+export function login(data) {
+  return request({
+    url: '/api/user/login',
+    method: 'post',
+    data
+  })
+}
+
+export function getInfo() {
+  return request({
+    url: '/api/user/info',
+    method: 'post'
+  })
+}
+
+export function logout() {
+  return request({
+    url: '/api/user/logout',
+    method: 'post'
+  })
+}

BIN
src/assets/404_images/404.png


BIN
src/assets/404_images/404_cloud.png


BIN
src/assets/head.jpg


+ 78 - 0
src/components/Breadcrumb/index.vue

@@ -0,0 +1,78 @@
+<template>
+  <el-breadcrumb class="app-breadcrumb" separator="/">
+    <transition-group name="breadcrumb">
+      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
+        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
+        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
+      </el-breadcrumb-item>
+    </transition-group>
+  </el-breadcrumb>
+</template>
+
+<script>
+import pathToRegexp from 'path-to-regexp'
+
+export default {
+  data() {
+    return {
+      levelList: null
+    }
+  },
+  watch: {
+    $route() {
+      this.getBreadcrumb()
+    }
+  },
+  created() {
+    this.getBreadcrumb()
+  },
+  methods: {
+    getBreadcrumb() {
+      // only show routes with meta.title
+      let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
+      const first = matched[0]
+
+      if (!this.isDashboard(first)) {
+        matched = [{ path: '/dashboard', meta: { title: '仪表板' }}].concat(matched)
+      }
+
+      this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
+    },
+    isDashboard(route) {
+      const name = route && route.name
+      if (!name) {
+        return false
+      }
+      return name.trim().toLocaleLowerCase() === '仪表板'.toLocaleLowerCase()
+    },
+    pathCompile(path) {
+      // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
+      const { params } = this.$route
+      var toPath = pathToRegexp.compile(path)
+      return toPath(params)
+    },
+    handleLink(item) {
+      const { redirect, path } = item
+      if (redirect) {
+        this.$router.push(redirect)
+        return
+      }
+      this.$router.push(this.pathCompile(path))
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-breadcrumb.el-breadcrumb {
+  display: inline-block;
+  font-size: 14px;
+  line-height: 50px;
+  margin-left: 8px;
+
+  .no-redirect {
+    color: #97a8be;
+    cursor: text;
+  }
+}
+</style>

+ 54 - 0
src/components/GithubCorner/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <a href="https://github.com/PanJiaChen/vue-element-admin" target="_blank" class="github-corner" aria-label="View source on Github">
+    <svg
+      width="80"
+      height="80"
+      viewBox="0 0 250 250"
+      style="fill:#40c9c6; color:#fff;"
+      aria-hidden="true"
+    >
+      <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" />
+      <path
+        d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
+        fill="currentColor"
+        style="transform-origin: 130px 106px;"
+        class="octo-arm"
+      />
+      <path
+        d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
+        fill="currentColor"
+        class="octo-body"
+      />
+    </svg>
+  </a>
+</template>
+
+<style scoped>
+.github-corner:hover .octo-arm {
+  animation: octocat-wave 560ms ease-in-out
+}
+
+@keyframes octocat-wave {
+  0%,
+  100% {
+    transform: rotate(0)
+  }
+  20%,
+  60% {
+    transform: rotate(-25deg)
+  }
+  40%,
+  80% {
+    transform: rotate(10deg)
+  }
+}
+
+@media (max-width:500px) {
+  .github-corner:hover .octo-arm {
+    animation: none
+  }
+  .github-corner .octo-arm {
+    animation: octocat-wave 560ms ease-in-out
+  }
+}
+</style>

+ 44 - 0
src/components/Hamburger/index.vue

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

+ 101 - 0
src/components/Pagination/index.vue

@@ -0,0 +1,101 @@
+<template>
+  <div :class="{'hidden':hidden}" class="pagination-container">
+    <el-pagination
+      :background="background"
+      :current-page.sync="currentPage"
+      :page-size.sync="pageSize"
+      :layout="layout"
+      :page-sizes="pageSizes"
+      :total="total"
+      v-bind="$attrs"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+    />
+  </div>
+</template>
+
+<script>
+import { scrollTo } from '@/utils/scrollTo'
+
+export default {
+  name: 'Pagination',
+  props: {
+    total: {
+      required: true,
+      type: Number
+    },
+    page: {
+      type: Number,
+      default: 1
+    },
+    limit: {
+      type: Number,
+      default: 20
+    },
+    pageSizes: {
+      type: Array,
+      default() {
+        return [5, 8, 10, 20, 30, 50]
+      }
+    },
+    layout: {
+      type: String,
+      default: 'total, sizes, prev, pager, next, jumper'
+    },
+    background: {
+      type: Boolean,
+      default: true
+    },
+    autoScroll: {
+      type: Boolean,
+      default: true
+    },
+    hidden: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    currentPage: {
+      get() {
+        return this.page
+      },
+      set(val) {
+        this.$emit('update:page', val)
+      }
+    },
+    pageSize: {
+      get() {
+        return this.limit
+      },
+      set(val) {
+        this.$emit('update:limit', val)
+      }
+    }
+  },
+  methods: {
+    handleSizeChange(val) {
+      this.$emit('pagination', { page: this.currentPage, limit: val })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    },
+    handleCurrentChange(val) {
+      this.$emit('pagination', { page: val, limit: this.pageSize })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pagination-container {
+  background: #fff;
+  padding: 32px 16px;
+}
+.pagination-container.hidden {
+  display: none;
+}
+</style>

+ 62 - 0
src/components/SvgIcon/index.vue

@@ -0,0 +1,62 @@
+<template>
+  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
+  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
+    <use :xlink:href="iconName" />
+  </svg>
+</template>
+
+<script>
+// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
+import { isExternal } from '@/utils/validate'
+
+export default {
+  name: 'SvgIcon',
+  props: {
+    iconClass: {
+      type: String,
+      required: true
+    },
+    className: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    isExternal() {
+      return isExternal(this.iconClass)
+    },
+    iconName() {
+      return `#icon-${this.iconClass}`
+    },
+    svgClass() {
+      if (this.className) {
+        return 'svg-icon ' + this.className
+      } else {
+        return 'svg-icon'
+      }
+    },
+    styleExternalIcon() {
+      return {
+        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
+        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.svg-icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+
+.svg-external-icon {
+  background-color: currentColor;
+  mask-size: cover!important;
+  display: inline-block;
+}
+</style>

+ 9 - 0
src/icons/index.js

@@ -0,0 +1,9 @@
+import Vue from 'vue'
+import SvgIcon from '@/components/SvgIcon'// svg component
+
+// register globally
+Vue.component('svg-icon', SvgIcon)
+
+const req = require.context('./svg', false, /\.svg$/)
+const requireAll = requireContext => requireContext.keys().map(requireContext)
+requireAll(req)

File diff suppressed because it is too large
+ 0 - 0
src/icons/svg/dashboard.svg


+ 1 - 0
src/icons/svg/example.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg>

+ 1 - 0
src/icons/svg/eye-open.svg

@@ -0,0 +1 @@
+<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>

+ 1 - 0
src/icons/svg/eye.svg

@@ -0,0 +1 @@
+<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>

File diff suppressed because it is too large
+ 0 - 0
src/icons/svg/form.svg


+ 1 - 0
src/icons/svg/link.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg>

+ 1 - 0
src/icons/svg/nested.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg>

+ 1 - 0
src/icons/svg/password.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>

+ 1 - 0
src/icons/svg/table.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg>

+ 1 - 0
src/icons/svg/tree.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>

+ 1 - 0
src/icons/svg/user.svg

@@ -0,0 +1 @@
+<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>

+ 22 - 0
src/icons/svgo.yml

@@ -0,0 +1,22 @@
+# replace default config
+
+# multipass: true
+# full: true
+
+plugins:
+
+  # - name
+  #
+  # or:
+  # - name: false
+  # - name: true
+  #
+  # or:
+  # - name:
+  #     param1: 1
+  #     param2: 2
+
+- removeAttrs:
+    attrs:
+      - 'fill'
+      - 'fill-rule'

+ 40 - 0
src/layout/components/AppMain.vue

@@ -0,0 +1,40 @@
+<template>
+  <section class="app-main">
+    <transition name="fade-transform" mode="out-in">
+      <router-view :key="key" />
+    </transition>
+  </section>
+</template>
+
+<script>
+export default {
+  name: 'AppMain',
+  computed: {
+    key() {
+      return this.$route.path
+    }
+  }
+}
+</script>
+
+<style scoped>
+.app-main {
+  /*50 = navbar  */
+  min-height: calc(100vh - 50px);
+  width: 100%;
+  position: relative;
+  overflow: hidden;
+}
+.fixed-header+.app-main {
+  padding-top: 50px;
+}
+</style>
+
+<style lang="scss">
+// fix css style bug in open el-dialog
+.el-popup-parent--hidden {
+  .fixed-header {
+    padding-right: 15px;
+  }
+}
+</style>

+ 128 - 0
src/layout/components/Navbar.vue

@@ -0,0 +1,128 @@
+<template>
+  <div class="navbar">
+    <hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
+
+    <breadcrumb class="breadcrumb-container" />
+
+    <div class="right-menu">
+      <el-dropdown class="avatar-container" trigger="click">
+        <div class="avatar-wrapper">
+          <img src="../../assets/head.jpg" class="user-avatar">
+          <i class="el-icon-caret-bottom" />
+        </div>
+        <el-dropdown-menu slot="dropdown" class="user-dropdown">
+          <el-dropdown-item divided @click.native="logout">
+            <span style="display:block;">退出</span>
+          </el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import Breadcrumb from '@/components/Breadcrumb'
+import Hamburger from '@/components/Hamburger'
+
+export default {
+  components: {
+    Breadcrumb,
+    Hamburger
+  },
+  computed: {
+    ...mapGetters([
+      'sidebar',
+      'avatar'
+    ])
+  },
+  methods: {
+    toggleSideBar() {
+      this.$store.dispatch('app/toggleSideBar')
+    },
+    async logout() {
+      await this.$store.dispatch('user/logout')
+      this.$router.push(`/login?redirect=${this.$route.fullPath}`)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.navbar {
+  height: 50px;
+  overflow: hidden;
+  position: relative;
+  background: #fff;
+  box-shadow: 0 1px 4px rgba(0,21,41,.08);
+
+  .hamburger-container {
+    line-height: 46px;
+    height: 100%;
+    float: left;
+    cursor: pointer;
+    transition: background .3s;
+    -webkit-tap-highlight-color:transparent;
+
+    &:hover {
+      background: rgba(0, 0, 0, .025)
+    }
+  }
+
+  .breadcrumb-container {
+    float: left;
+  }
+
+  .right-menu {
+    float: right;
+    height: 100%;
+    line-height: 50px;
+
+    &:focus {
+      outline: none;
+    }
+
+    .right-menu-item {
+      display: inline-block;
+      padding: 0 8px;
+      height: 100%;
+      font-size: 18px;
+      color: #5a5e66;
+      vertical-align: text-bottom;
+
+      &.hover-effect {
+        cursor: pointer;
+        transition: background .3s;
+
+        &:hover {
+          background: rgba(0, 0, 0, .025)
+        }
+      }
+    }
+
+    .avatar-container {
+      margin-right: 30px;
+
+      .avatar-wrapper {
+        margin-top: 5px;
+        position: relative;
+
+        .user-avatar {
+          cursor: pointer;
+          width: 40px;
+          height: 40px;
+          border-radius: 10px;
+        }
+
+        .el-icon-caret-bottom {
+          cursor: pointer;
+          position: absolute;
+          right: -20px;
+          top: 25px;
+          font-size: 12px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 26 - 0
src/layout/components/Sidebar/FixiOSBug.js

@@ -0,0 +1,26 @@
+export default {
+  computed: {
+    device() {
+      return this.$store.state.app.device
+    }
+  },
+  mounted() {
+    // In order to fix the click on menu on the ios device will trigger the mouseleave bug
+    // https://github.com/PanJiaChen/vue-element-admin/issues/1135
+    this.fixBugIniOS()
+  },
+  methods: {
+    fixBugIniOS() {
+      const $subMenu = this.$refs.subMenu
+      if ($subMenu) {
+        const handleMouseleave = $subMenu.handleMouseleave
+        $subMenu.handleMouseleave = (e) => {
+          if (this.device === 'mobile') {
+            return
+          }
+          handleMouseleave(e)
+        }
+      }
+    }
+  }
+}

+ 41 - 0
src/layout/components/Sidebar/Item.vue

@@ -0,0 +1,41 @@
+<script>
+export default {
+  name: 'MenuItem',
+  functional: true,
+  props: {
+    icon: {
+      type: String,
+      default: ''
+    },
+    title: {
+      type: String,
+      default: ''
+    }
+  },
+  render(h, context) {
+    const { icon, title } = context.props
+    const vnodes = []
+
+    if (icon) {
+      if (icon.includes('el-icon')) {
+        vnodes.push(<i class={[icon, 'sub-el-icon']} />)
+      } else {
+        vnodes.push(<svg-icon icon-class={icon}/>)
+      }
+    }
+
+    if (title) {
+      vnodes.push(<span slot='title'>{(title)}</span>)
+    }
+    return vnodes
+  }
+}
+</script>
+
+<style scoped>
+.sub-el-icon {
+  color: currentColor;
+  width: 1em;
+  height: 1em;
+}
+</style>

+ 43 - 0
src/layout/components/Sidebar/Link.vue

@@ -0,0 +1,43 @@
+<template>
+  <component :is="type" v-bind="linkProps(to)">
+    <slot />
+  </component>
+</template>
+
+<script>
+import { isExternal } from '@/utils/validate'
+
+export default {
+  props: {
+    to: {
+      type: String,
+      required: true
+    }
+  },
+  computed: {
+    isExternal() {
+      return isExternal(this.to)
+    },
+    type() {
+      if (this.isExternal) {
+        return 'a'
+      }
+      return 'router-link'
+    }
+  },
+  methods: {
+    linkProps(to) {
+      if (this.isExternal) {
+        return {
+          href: to,
+          target: '_blank',
+          rel: 'noopener'
+        }
+      }
+      return {
+        to: to
+      }
+    }
+  }
+}
+</script>

+ 82 - 0
src/layout/components/Sidebar/Logo.vue

@@ -0,0 +1,82 @@
+<template>
+  <div class="sidebar-logo-container" :class="{'collapse':collapse}">
+    <transition name="sidebarLogoFade">
+      <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
+        <img v-if="logo" :src="logo" class="sidebar-logo">
+        <h1 v-else class="sidebar-title">{{ title }} </h1>
+      </router-link>
+      <router-link v-else key="expand" class="sidebar-logo-link" to="/">
+        <img v-if="logo" :src="logo" class="sidebar-logo">
+        <h1 class="sidebar-title">{{ title }} </h1>
+      </router-link>
+    </transition>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SidebarLogo',
+  props: {
+    collapse: {
+      type: Boolean,
+      required: true
+    }
+  },
+  data() {
+    return {
+      title: 'Vue Admin Template',
+      logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png'
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.sidebarLogoFade-enter-active {
+  transition: opacity 1.5s;
+}
+
+.sidebarLogoFade-enter,
+.sidebarLogoFade-leave-to {
+  opacity: 0;
+}
+
+.sidebar-logo-container {
+  position: relative;
+  width: 100%;
+  height: 50px;
+  line-height: 50px;
+  background: #2b2f3a;
+  text-align: center;
+  overflow: hidden;
+
+  & .sidebar-logo-link {
+    height: 100%;
+    width: 100%;
+
+    & .sidebar-logo {
+      width: 32px;
+      height: 32px;
+      vertical-align: middle;
+      margin-right: 12px;
+    }
+
+    & .sidebar-title {
+      display: inline-block;
+      margin: 0;
+      color: #fff;
+      font-weight: 600;
+      line-height: 50px;
+      font-size: 14px;
+      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
+      vertical-align: middle;
+    }
+  }
+
+  &.collapse {
+    .sidebar-logo {
+      margin-right: 0px;
+    }
+  }
+}
+</style>

+ 95 - 0
src/layout/components/Sidebar/SidebarItem.vue

@@ -0,0 +1,95 @@
+<template>
+  <div v-if="!item.hidden">
+    <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
+      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
+        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
+          <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
+        </el-menu-item>
+      </app-link>
+    </template>
+
+    <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
+      <template slot="title">
+        <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
+      </template>
+      <sidebar-item
+        v-for="child in item.children"
+        :key="child.path"
+        :is-nest="true"
+        :item="child"
+        :base-path="resolvePath(child.path)"
+        class="nest-menu"
+      />
+    </el-submenu>
+  </div>
+</template>
+
+<script>
+import path from 'path'
+import { isExternal } from '@/utils/validate'
+import Item from './Item'
+import AppLink from './Link'
+import FixiOSBug from './FixiOSBug'
+
+export default {
+  name: 'SidebarItem',
+  components: { Item, AppLink },
+  mixins: [FixiOSBug],
+  props: {
+    // route object
+    item: {
+      type: Object,
+      required: true
+    },
+    isNest: {
+      type: Boolean,
+      default: false
+    },
+    basePath: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
+    // TODO: refactor with render function
+    this.onlyOneChild = null
+    return {}
+  },
+  methods: {
+    hasOneShowingChild(children = [], parent) {
+      const showingChildren = children.filter(item => {
+        if (item.hidden) {
+          return false
+        } else {
+          // Temp set(will be used if only has one showing child)
+          this.onlyOneChild = item
+          return true
+        }
+      })
+
+      // When there is only one child router, the child router is displayed by default
+      if (showingChildren.length === 1) {
+        return true
+      }
+
+      // Show parent if there are no child router to display
+      if (showingChildren.length === 0) {
+        this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
+        return true
+      }
+
+      return false
+    },
+    resolvePath(routePath) {
+      if (isExternal(routePath)) {
+        return routePath
+      }
+      if (isExternal(this.basePath)) {
+        return this.basePath
+      }
+      return path.resolve(this.basePath, routePath)
+    }
+  }
+}
+</script>

+ 56 - 0
src/layout/components/Sidebar/index.vue

@@ -0,0 +1,56 @@
+<template>
+  <div :class="{'has-logo':showLogo}">
+    <logo v-if="showLogo" :collapse="isCollapse" />
+    <el-scrollbar wrap-class="scrollbar-wrapper">
+      <el-menu
+        :default-active="activeMenu"
+        :collapse="isCollapse"
+        :background-color="variables.menuBg"
+        :text-color="variables.menuText"
+        :unique-opened="false"
+        :active-text-color="variables.menuActiveText"
+        :collapse-transition="false"
+        mode="vertical"
+      >
+        <sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
+      </el-menu>
+    </el-scrollbar>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import Logo from './Logo'
+import SidebarItem from './SidebarItem'
+import variables from '@/styles/variables.scss'
+
+export default {
+  components: { SidebarItem, Logo },
+  computed: {
+    ...mapGetters([
+      'sidebar'
+    ]),
+    routes() {
+      return this.$router.options.routes
+    },
+    activeMenu() {
+      const route = this.$route
+      const { meta, path } = route
+      // if set path, the sidebar will highlight the path you set
+      if (meta.activeMenu) {
+        return meta.activeMenu
+      }
+      return path
+    },
+    showLogo() {
+      return this.$store.state.settings.sidebarLogo
+    },
+    variables() {
+      return variables
+    },
+    isCollapse() {
+      return !this.sidebar.opened
+    }
+  }
+}
+</script>

+ 3 - 0
src/layout/components/index.js

@@ -0,0 +1,3 @@
+export { default as Navbar } from './Navbar'
+export { default as Sidebar } from './Sidebar'
+export { default as AppMain } from './AppMain'

+ 93 - 0
src/layout/index.vue

@@ -0,0 +1,93 @@
+<template>
+  <div :class="classObj" class="app-wrapper">
+    <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
+    <sidebar class="sidebar-container" />
+    <div class="main-container">
+      <div :class="{'fixed-header':fixedHeader}">
+        <navbar />
+      </div>
+      <app-main />
+    </div>
+  </div>
+</template>
+
+<script>
+import { Navbar, Sidebar, AppMain } from './components'
+import ResizeMixin from './mixin/ResizeHandler'
+
+export default {
+  name: 'Layout',
+  components: {
+    Navbar,
+    Sidebar,
+    AppMain
+  },
+  mixins: [ResizeMixin],
+  computed: {
+    sidebar() {
+      return this.$store.state.app.sidebar
+    },
+    device() {
+      return this.$store.state.app.device
+    },
+    fixedHeader() {
+      return this.$store.state.settings.fixedHeader
+    },
+    classObj() {
+      return {
+        hideSidebar: !this.sidebar.opened,
+        openSidebar: this.sidebar.opened,
+        withoutAnimation: this.sidebar.withoutAnimation,
+        mobile: this.device === 'mobile'
+      }
+    }
+  },
+  methods: {
+    handleClickOutside() {
+      this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  @import "~@/styles/mixin.scss";
+  @import "~@/styles/variables.scss";
+
+  .app-wrapper {
+    @include clearfix;
+    position: relative;
+    height: 100%;
+    width: 100%;
+    &.mobile.openSidebar{
+      position: fixed;
+      top: 0;
+    }
+  }
+  .drawer-bg {
+    background: #000;
+    opacity: 0.3;
+    width: 100%;
+    top: 0;
+    height: 100%;
+    position: absolute;
+    z-index: 999;
+  }
+
+  .fixed-header {
+    position: fixed;
+    top: 0;
+    right: 0;
+    z-index: 9;
+    width: calc(100% - #{$sideBarWidth});
+    transition: width 0.28s;
+  }
+
+  .hideSidebar .fixed-header {
+    width: calc(100% - 54px)
+  }
+
+  .mobile .fixed-header {
+    width: 100%;
+  }
+</style>

+ 45 - 0
src/layout/mixin/ResizeHandler.js

@@ -0,0 +1,45 @@
+import store from '@/store'
+
+const { body } = document
+const WIDTH = 992 // refer to Bootstrap's responsive design
+
+export default {
+  watch: {
+    $route(route) {
+      if (this.device === 'mobile' && this.sidebar.opened) {
+        store.dispatch('app/closeSideBar', { withoutAnimation: false })
+      }
+    }
+  },
+  beforeMount() {
+    window.addEventListener('resize', this.$_resizeHandler)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.$_resizeHandler)
+  },
+  mounted() {
+    const isMobile = this.$_isMobile()
+    if (isMobile) {
+      store.dispatch('app/toggleDevice', 'mobile')
+      store.dispatch('app/closeSideBar', { withoutAnimation: true })
+    }
+  },
+  methods: {
+    // use $_ for mixins properties
+    // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
+    $_isMobile() {
+      const rect = body.getBoundingClientRect()
+      return rect.width - 1 < WIDTH
+    },
+    $_resizeHandler() {
+      if (!document.hidden) {
+        const isMobile = this.$_isMobile()
+        store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
+
+        if (isMobile) {
+          store.dispatch('app/closeSideBar', { withoutAnimation: true })
+        }
+      }
+    }
+  }
+}

+ 43 - 0
src/main.js

@@ -0,0 +1,43 @@
+import Vue from 'vue'
+
+import 'normalize.css/normalize.css' // A modern alternative to CSS resets
+
+import ElementUI from 'element-ui'
+import 'element-ui/lib/theme-chalk/index.css'
+import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18n
+
+import '@/styles/index.scss' // global css
+
+import App from './App'
+import store from './store'
+import router from './router'
+
+import '@/icons' // icon
+import '@/permission' // permission control
+
+/**
+ * If you don't want to use mock-server
+ * you want to use MockJs for mock api
+ * you can execute: mockXHR()
+ *
+ * Currently MockJs will be used in the production environment,
+ * please remove it before going online ! ! !
+ */
+if (process.env.NODE_ENV === 'production') {
+  const { mockXHR } = require('../mock')
+  mockXHR()
+}
+
+// set ElementUI lang to EN
+Vue.use(ElementUI, { locale })
+// 如果想要中文版 element-ui,按如下方式声明
+// Vue.use(ElementUI)
+
+Vue.config.productionTip = false
+
+new Vue({
+  el: '#app',
+  router,
+  store,
+  render: h => h(App)
+})

+ 64 - 0
src/permission.js

@@ -0,0 +1,64 @@
+import router from './router'
+import store from './store'
+import { Message } from 'element-ui'
+import NProgress from 'nprogress' // progress bar
+import 'nprogress/nprogress.css' // progress bar style
+import { getToken } from '@/utils/auth' // get token from cookie
+import getPageTitle from '@/utils/get-page-title'
+
+NProgress.configure({ showSpinner: false }) // NProgress Configuration
+
+const whiteList = ['/login'] // no redirect whitelist
+
+router.beforeEach(async(to, from, next) => {
+  // start progress bar
+  NProgress.start()
+
+  // set page title
+  document.title = getPageTitle(to.meta.title)
+
+  // determine whether the user has logged in
+  const hasToken = getToken()
+
+  if (hasToken) {
+    if (to.path === '/login') {
+      // if is logged in, redirect to the home page
+      next({ path: '/' })
+      NProgress.done()
+    } else {
+      const hasGetUserInfo = store.getters.name
+      if (hasGetUserInfo) {
+        next()
+      } else {
+        try {
+          // get user info
+          await store.dispatch('user/getInfo')
+
+          next()
+        } catch (error) {
+          // remove token and go to login page to re-login
+          await store.dispatch('user/resetToken')
+          Message.error(error || 'Has Error')
+          next(`/login?redirect=${to.path}`)
+          NProgress.done()
+        }
+      }
+    }
+  } else {
+    /* has no token*/
+
+    if (whiteList.indexOf(to.path) !== -1) {
+      // in the free login whitelist, go directly
+      next()
+    } else {
+      // other pages that do not have permission to access are redirected to the login page.
+      next(`/login?redirect=${to.path}`)
+      NProgress.done()
+    }
+  }
+})
+
+router.afterEach(() => {
+  // finish progress bar
+  NProgress.done()
+})

+ 176 - 0
src/router/index.js

@@ -0,0 +1,176 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+
+Vue.use(Router)
+
+/* Layout */
+import Layout from '@/layout'
+
+export const constantRoutes = [
+  {
+    path: '/login',
+    component: () => import('@/views/login/index'),
+    hidden: true
+  },
+
+  {
+    path: '/404',
+    component: () => import('@/views/404'),
+    hidden: true
+  },
+
+  // {
+  //   path: '/',
+  //   component: Layout,
+  //   redirect: '/dashboard',
+  //   children: [{
+  //     path: 'dashboard',
+  //     name: '仪表板',
+  //     component: () => import('@/views/dashboard/index'),
+  //     meta: { title: '仪表板', icon: 'dashboard' }
+  //   }]
+  // },
+
+  {
+    path: '/',
+    component: Layout,
+    redirect: '/dashboard',
+    children: [{
+      path: 'dashboard',
+      name: '违章详细',
+      component: () => import('@/views/car/event/vlt'),
+      meta: { title: '违章详细', icon: 'el-icon-s-data' }
+    }]
+  },
+  {
+    path: '/',
+      component: Layout,
+      redirect: '/times',
+    children: [{
+    path: 'times',
+    name: '违章统计',
+    component: () => import('@/views/car/event/vlt'),
+    meta: { title: '违章统计', icon: 'el-icon-discount' }
+  }]
+  },
+
+  {
+    path: '/car',
+    component: Layout,
+    redirect: '/car/info',
+    name: '基本管理',
+    meta: { title: '基本管理', icon: 'el-icon-notebook-1' },
+    children: [
+      {
+        path: 'info',
+        name: '车辆管理',
+        component: () => import('@/views/car/carinfo/index'),
+        meta: { title: '车辆管理', icon: 'el-icon-document' }
+      },
+      {
+        path: 'info/add',
+        name: '车辆添加',
+        hidden: true,
+        component: () => import('@/views/car/carinfo/add'),
+        meta: { title: '车辆添加'}
+      },
+      {
+        path: 'driver',
+        name: '司机管理',
+        component: () => import('@/views/car/driver/index'),
+        meta: { title: '司机管理', icon: 'el-icon-document' }
+      }
+    ]
+  },
+  // {
+  //   path: '/system',
+  //   component: Layout,
+  //   redirect: '/system/user',
+  //   name: '系统管理',
+  //   meta: { title: '系统管理', icon: 'el-icon-setting' },
+  //   children: [
+  //     {
+  //       path: '/system/dict',
+  //       component: () => import('@/views/sys/sys_dict/index'),
+  //       name: '数据字典',
+  //       meta: { title: '数据字典', icon: 'el-icon-connection' }
+  //     },
+  //     {
+  //       path: '/system/dict/add',
+  //       name: '数据字典',
+  //       hidden: true,
+  //       component: () => import('@/views/sys/sys_dict/add'),
+  //       meta: { title: '字典添加' }
+  //     },
+  //     {
+  //       path: '/system/depart',
+  //       component: () => import('@/views/sys/sys_depart/index'),
+  //       name: '权限管理',
+  //       meta: { title: '权限管理', icon: 'el-icon-s-custom' }
+  //     },
+  //     {
+  //       path: 'user',
+  //       name: '用户管理',
+  //       component: () => import('@/views/sys/sys_user/index'),
+  //       meta: { title: '用户管理', icon: 'el-icon-user' },
+  //       children: [
+  //         {
+  //           path: 'add',
+  //           name: '用户管理',
+  //           hidden: true,
+  //           component: () => import('@/views/sys/sys_user/add'),
+  //           meta: { title: '添加用户' }
+  //         },
+  //       ]
+  //     },
+  //     {
+  //       path: 'role',
+  //       name: '角色管理',
+  //       component: () => import('@/views/sys/sys_role/index'),
+  //       meta: { title: '角色管理', icon: 'el-icon-s-custom' },
+  //       children: [
+  //         {
+  //           path: 'add',
+  //           name: '角色管理',
+  //           hidden: true,
+  //           component: () => import('@/views/sys/sys_role/add'),
+  //           meta: { title: '添加角色' }
+  //         },
+  //       ]
+  //     },
+  //     {
+  //       path: 'department',
+  //       name: '部门管理',
+  //       component: () => import('@/views/tree/index'),
+  //       meta: { title: '部门管理', icon: 'el-icon-school' },
+  //       children: [
+  //         {
+  //           path: 'add',
+  //           name: '添加',
+  //           hidden: true,
+  //           component: () => import('@/views/sys/sys_role/add'),
+  //           meta: { title: '添加角色' }
+  //         },
+  //       ]
+  //     }
+  //   ]
+  // },
+  // 404 page must be placed at the end !!!
+  { path: '*', redirect: '/404', hidden: true }
+]
+
+const createRouter = () => new Router({
+  // mode: 'history', // require service support
+  scrollBehavior: () => ({ y: 0 }),
+  routes: constantRoutes
+})
+
+const router = createRouter()
+
+// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
+export function resetRouter() {
+  const newRouter = createRouter()
+  router.matcher = newRouter.matcher // reset router
+}
+
+export default router

+ 16 - 0
src/settings.js

@@ -0,0 +1,16 @@
+module.exports = {
+
+  title: '通冠物流',
+
+  /**
+   * @type {boolean} true | false
+   * @description Whether fix the header
+   */
+  fixedHeader: true,
+
+  /**
+   * @type {boolean} true | false
+   * @description Whether show the logo in sidebar
+   */
+  sidebarLogo: false
+}

+ 8 - 0
src/store/getters.js

@@ -0,0 +1,8 @@
+const getters = {
+  sidebar: state => state.app.sidebar,
+  device: state => state.app.device,
+  token: state => state.user.token,
+  avatar: state => state.user.avatar,
+  name: state => state.user.name
+}
+export default getters

+ 19 - 0
src/store/index.js

@@ -0,0 +1,19 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+import getters from './getters'
+import app from './modules/app'
+import settings from './modules/settings'
+import user from './modules/user'
+
+Vue.use(Vuex)
+
+const store = new Vuex.Store({
+  modules: {
+    app,
+    settings,
+    user
+  },
+  getters
+})
+
+export default store

+ 48 - 0
src/store/modules/app.js

@@ -0,0 +1,48 @@
+import Cookies from 'js-cookie'
+
+const state = {
+  sidebar: {
+    opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
+    withoutAnimation: false
+  },
+  device: 'desktop'
+}
+
+const mutations = {
+  TOGGLE_SIDEBAR: state => {
+    state.sidebar.opened = !state.sidebar.opened
+    state.sidebar.withoutAnimation = false
+    if (state.sidebar.opened) {
+      Cookies.set('sidebarStatus', 1)
+    } else {
+      Cookies.set('sidebarStatus', 0)
+    }
+  },
+  CLOSE_SIDEBAR: (state, withoutAnimation) => {
+    Cookies.set('sidebarStatus', 0)
+    state.sidebar.opened = false
+    state.sidebar.withoutAnimation = withoutAnimation
+  },
+  TOGGLE_DEVICE: (state, device) => {
+    state.device = device
+  }
+}
+
+const actions = {
+  toggleSideBar({ commit }) {
+    commit('TOGGLE_SIDEBAR')
+  },
+  closeSideBar({ commit }, { withoutAnimation }) {
+    commit('CLOSE_SIDEBAR', withoutAnimation)
+  },
+  toggleDevice({ commit }, device) {
+    commit('TOGGLE_DEVICE', device)
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}

+ 32 - 0
src/store/modules/settings.js

@@ -0,0 +1,32 @@
+import defaultSettings from '@/settings'
+
+const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
+
+const state = {
+  showSettings: showSettings,
+  fixedHeader: fixedHeader,
+  sidebarLogo: sidebarLogo
+}
+
+const mutations = {
+  CHANGE_SETTING: (state, { key, value }) => {
+    // eslint-disable-next-line no-prototype-builtins
+    if (state.hasOwnProperty(key)) {
+      state[key] = value
+    }
+  }
+}
+
+const actions = {
+  changeSetting({ commit }, data) {
+    commit('CHANGE_SETTING', data)
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}
+

+ 97 - 0
src/store/modules/user.js

@@ -0,0 +1,97 @@
+import { login, logout, getInfo } from '@/api/user'
+import { getToken, setToken, removeToken } from '@/utils/auth'
+import { resetRouter } from '@/router'
+
+const getDefaultState = () => {
+  return {
+    token: getToken(),
+    name: '',
+    avatar: ''
+  }
+}
+
+const state = getDefaultState()
+
+const mutations = {
+  RESET_STATE: (state) => {
+    Object.assign(state, getDefaultState())
+  },
+  SET_TOKEN: (state, token) => {
+    state.token = token
+  },
+  SET_NAME: (state, name) => {
+    state.name = name
+  },
+  SET_AVATAR: (state, avatar) => {
+    state.avatar = avatar
+  }
+}
+
+const actions = {
+  // user login
+  login({ commit }, userInfo) {
+    const { username, password } = userInfo
+    return new Promise((resolve, reject) => {
+      login({ username: username.trim(), password: password }).then(response => {
+        const { data } = response
+        commit('SET_TOKEN', data)
+        setToken(data)
+        resolve()
+      }).catch(error => {
+        reject(error)
+      })
+    })
+  },
+
+  // get user info
+  getInfo({ commit, state }) {
+    return new Promise((resolve, reject) => {
+      getInfo().then(response => {
+        const { data } = response
+
+        if (!data) {
+          return reject('Verification failed, please Login again.')
+        }
+
+        const { name, avatar } = data
+
+        commit('SET_NAME', name)
+        commit('SET_AVATAR', avatar)
+        resolve(data)
+      }).catch(error => {
+        reject(error)
+      })
+    })
+  },
+
+  // user logout
+  logout({ commit, state }) {
+    return new Promise((resolve, reject) => {
+      logout(state.token).then(() => {
+        removeToken() // must remove  token  first
+        resetRouter()
+        commit('RESET_STATE')
+        resolve()
+      }).catch(error => {
+        reject(error)
+      })
+    })
+  },
+
+  // remove token
+  resetToken({ commit }) {
+    return new Promise(resolve => {
+      removeToken() // must remove  token  first
+      commit('RESET_STATE')
+      resolve()
+    })
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}
+

+ 49 - 0
src/styles/element-ui.scss

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

+ 65 - 0
src/styles/index.scss

@@ -0,0 +1,65 @@
+@import './variables.scss';
+@import './mixin.scss';
+@import './transition.scss';
+@import './element-ui.scss';
+@import './sidebar.scss';
+
+body {
+  height: 100%;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
+  text-rendering: optimizeLegibility;
+  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+}
+
+label {
+  font-weight: 700;
+}
+
+html {
+  height: 100%;
+  box-sizing: border-box;
+}
+
+#app {
+  height: 100%;
+}
+
+*,
+*:before,
+*:after {
+  box-sizing: inherit;
+}
+
+a:focus,
+a:active {
+  outline: none;
+}
+
+a,
+a:focus,
+a:hover {
+  cursor: pointer;
+  color: inherit;
+  text-decoration: none;
+}
+
+div:focus {
+  outline: none;
+}
+
+.clearfix {
+  &:after {
+    visibility: hidden;
+    display: block;
+    font-size: 0;
+    content: " ";
+    clear: both;
+    height: 0;
+  }
+}
+
+// main-container global css
+.app-container {
+  padding: 20px;
+}

+ 28 - 0
src/styles/mixin.scss

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

+ 226 - 0
src/styles/sidebar.scss

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

+ 48 - 0
src/styles/transition.scss

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

+ 25 - 0
src/styles/variables.scss

@@ -0,0 +1,25 @@
+// sidebar
+$menuText:#383839;
+$menuActiveText:#409EFF;
+$subMenuActiveText:#383839; //https://github.com/ElemeFE/element/issues/12951
+
+$menuBg:#f2f3f4;
+$menuHover:#DDDDDD;
+
+$subMenuBg:#f2f3f4;
+$subMenuHover:#DDDDDD;
+
+$sideBarWidth: 210px;
+
+// the :export directive is the magic sauce for webpack
+// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
+:export {
+  menuText: $menuText;
+  menuActiveText: $menuActiveText;
+  subMenuActiveText: $subMenuActiveText;
+  menuBg: $menuBg;
+  menuHover: $menuHover;
+  subMenuBg: $subMenuBg;
+  subMenuHover: $subMenuHover;
+  sideBarWidth: $sideBarWidth;
+}

+ 15 - 0
src/utils/auth.js

@@ -0,0 +1,15 @@
+import Cookies from 'js-cookie'
+
+const TokenKey = 'vue_admin_template_token'
+
+export function getToken() {
+  return Cookies.get(TokenKey)
+}
+
+export function setToken(token) {
+  return Cookies.set(TokenKey, token)
+}
+
+export function removeToken() {
+  return Cookies.remove(TokenKey)
+}

+ 10 - 0
src/utils/get-page-title.js

@@ -0,0 +1,10 @@
+import defaultSettings from '@/settings'
+
+const title = defaultSettings.title || ''
+
+export default function getPageTitle(pageTitle) {
+  if (pageTitle) {
+    return `${pageTitle} - ${title}`
+  }
+  return `${title}`
+}

+ 117 - 0
src/utils/index.js

@@ -0,0 +1,117 @@
+/**
+ * Created by PanJiaChen on 16/11/18.
+ */
+
+/**
+ * Parse the time to string
+ * @param {(Object|string|number)} time
+ * @param {string} cFormat
+ * @returns {string | null}
+ */
+export function parseTime(time, cFormat) {
+  if (arguments.length === 0 || !time) {
+    return null
+  }
+  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
+  let date
+  if (typeof time === 'object') {
+    date = time
+  } else {
+    if ((typeof time === 'string')) {
+      if ((/^[0-9]+$/.test(time))) {
+        // support "1548221490638"
+        time = parseInt(time)
+      } else {
+        // support safari
+        // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
+        time = time.replace(new RegExp(/-/gm), '/')
+      }
+    }
+
+    if ((typeof time === 'number') && (time.toString().length === 10)) {
+      time = time * 1000
+    }
+    date = new Date(time)
+  }
+  const formatObj = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay()
+  }
+  const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
+    const value = formatObj[key]
+    // Note: getDay() returns 0 on Sunday
+    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
+    return value.toString().padStart(2, '0')
+  })
+  return time_str
+}
+
+/**
+ * @param {number} time
+ * @param {string} option
+ * @returns {string}
+ */
+export function formatTime(time, option) {
+  if (('' + time).length === 10) {
+    time = parseInt(time) * 1000
+  } else {
+    time = +time
+  }
+  const d = new Date(time)
+  const now = Date.now()
+
+  const diff = (now - d) / 1000
+
+  if (diff < 30) {
+    return '刚刚'
+  } else if (diff < 3600) {
+    // less 1 hour
+    return Math.ceil(diff / 60) + '分钟前'
+  } else if (diff < 3600 * 24) {
+    return Math.ceil(diff / 3600) + '小时前'
+  } else if (diff < 3600 * 24 * 2) {
+    return '1天前'
+  }
+  if (option) {
+    return parseTime(time, option)
+  } else {
+    return (
+      d.getMonth() +
+      1 +
+      '月' +
+      d.getDate() +
+      '日' +
+      d.getHours() +
+      '时' +
+      d.getMinutes() +
+      '分'
+    )
+  }
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function param2Obj(url) {
+  const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
+  if (!search) {
+    return {}
+  }
+  const obj = {}
+  const searchArr = search.split('&')
+  searchArr.forEach(v => {
+    const index = v.indexOf('=')
+    if (index !== -1) {
+      const name = v.substring(0, index)
+      const val = v.substring(index + 1, v.length)
+      obj[name] = val
+    }
+  })
+  return obj
+}

+ 85 - 0
src/utils/request.js

@@ -0,0 +1,85 @@
+import axios from 'axios'
+import { MessageBox, Message } from 'element-ui'
+import store from '@/store'
+import { getToken } from '@/utils/auth'
+
+// create an axios instance
+const service = axios.create({
+  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
+  // withCredentials: true, // send cookies when cross-domain requests
+  timeout: 5000 // request timeout
+})
+
+// request interceptor
+service.interceptors.request.use(
+  config => {
+    // do something before request is sent
+
+    if (store.getters.token) {
+      // let each request carry token
+      // ['X-Token'] is a custom headers key
+      // please modify it according to the actual situation
+      config.headers['X-Token'] = getToken()
+      config.headers['Content-Type'] = 'application/json;charset=UTF-8'
+    }
+    return config
+  },
+  error => {
+    // do something with request error
+    console.log(error) // for debug
+    return Promise.reject(error)
+  }
+)
+
+// response interceptor
+service.interceptors.response.use(
+  /**
+   * If you want to get http information such as headers or status
+   * Please return  response => response
+  */
+
+  /**
+   * Determine the request status by custom code
+   * Here is just an example
+   * You can also judge the status by HTTP Status Code
+   */
+  response => {
+    const res = response.data
+
+    // if the custom code is not 20000, it is judged as an error.
+    if (res.code !== 200) {
+      Message({
+        message: res.message || 'Error',
+        type: 'error',
+        duration: 5 * 1000
+      })
+
+      if (res.code === 501) {
+        // to re-login
+        MessageBox.confirm('你的账号已经登出,请重新登录!', '确认', {
+          confirmButtonText: '重新登录',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          store.dispatch('user/resetToken').then(() => {
+            location.reload()
+          })
+        })
+      }
+      return Promise.reject(new Error(res.message || 'Error'))
+    } else {
+      return res
+    }
+  },
+  error => {
+    console.log('err' + error) // for debug
+    Message({
+      message: error.message,
+      type: 'error',
+      duration: 5 * 1000
+    })
+    return Promise.reject(error)
+  }
+)
+
+export default service

+ 50 - 0
src/utils/scrollTo.js

@@ -0,0 +1,50 @@
+Math.easeInOutQuad = function(t, b, c, d) {
+  t /= d / 2
+  if (t < 1) {
+    return c / 2 * t * t + b
+  }
+  t--
+  return -c / 2 * (t * (t - 2) - 1) + b
+}
+
+// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
+var requestAnimFrame = (function() {
+  return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
+})()
+
+// because it's so fucking difficult to detect the scrolling element, just move them all
+function move(amount) {
+  document.documentElement.scrollTop = amount
+  document.body.parentNode.scrollTop = amount
+  document.body.scrollTop = amount
+}
+
+function position() {
+  return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
+}
+
+export function scrollTo(to, duration, callback) {
+  const start = position()
+  const change = to - start
+  const increment = 20
+  let currentTime = 0
+  duration = (typeof (duration) === 'undefined') ? 500 : duration
+  var animateScroll = function() {
+    // increment the time
+    currentTime += increment
+    // find the value with the quadratic in-out easing function
+    var val = Math.easeInOutQuad(currentTime, start, change, duration)
+    // move the document.body
+    move(val)
+    // do the animation unless its over
+    if (currentTime < duration) {
+      requestAnimFrame(animateScroll)
+    } else {
+      if (callback && typeof (callback) === 'function') {
+        // the animation is done so lets callback
+        callback()
+      }
+    }
+  }
+  animateScroll()
+}

+ 20 - 0
src/utils/validate.js

@@ -0,0 +1,20 @@
+/**
+ * Created by PanJiaChen on 16/11/18.
+ */
+
+/**
+ * @param {string} path
+ * @returns {Boolean}
+ */
+export function isExternal(path) {
+  return /^(https?:|mailto:|tel:)/.test(path)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUsername(str) {
+  const valid_map = ['admin', 'editor']
+  return valid_map.indexOf(str.trim()) >= 0
+}

+ 228 - 0
src/views/404.vue

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

+ 154 - 0
src/views/car/carinfo/add.vue

@@ -0,0 +1,154 @@
+<template>
+  <div class="app-container">
+
+    <el-form :model="Form" status-icon ref="ruleForm" label-position="right" size="small"
+             slot="dialog" :rules='rules' label-width="100px">
+
+      <el-form-item prop="image" label="行驶证:">
+      <el-upload
+        class="avatar-uploader"
+        action="http://file.it5000.com/upload/integral"
+        :show-file-list="false"
+        :on-success="handleAvatarSuccess">
+        <img v-if="imageUrl" :src="imageUrl" class="avatar">
+        <i v-else class="el-icon-plus avatar-uploader-icon"></i>
+      </el-upload>
+      </el-form-item>
+
+      <el-form-item prop='plateNumber' label="部门:">
+        <el-select v-model="Form.deptId" placeholder="请选择">
+          <el-option
+            v-for="item in options"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value">
+          </el-option>
+        </el-select>
+      </el-form-item>
+
+      <el-form-item prop='plateNumber' label="车牌:">
+        <el-input v-model="Form.plateNumber" :span="24" placeholder="车牌"></el-input>
+      </el-form-item>
+
+      <el-form-item label="车型:">
+        <el-input v-model="Form.model" :span="24" placeholder="车型"></el-input>
+      </el-form-item>
+
+      <el-form-item prop="VIN" label="VIN:">
+        <el-input v-model="Form.vin" :span="24" placeholder="VIN"></el-input>
+      </el-form-item>
+
+      <el-form-item label="发动机号:">
+        <el-input v-model="Form.engineNo" :span="24" placeholder="发动机号"></el-input>
+      </el-form-item>
+
+      <el-form-item label="注册日期:">
+        <el-input v-model="Form.registerDate" :span="24" placeholder="注册日期"></el-input>
+      </el-form-item>
+
+      <el-form-item label="发证日期:">
+        <el-input v-model="Form.certDate" :span="24" placeholder="发证日期"></el-input>
+      </el-form-item>
+
+      <el-button @click="submit()">提交</el-button>
+    </el-form>
+
+  </div>
+</template>
+
+<script>/* eslint-disable no-unused-vars,no-undef,quotes */
+
+import { postJson, postForm, post } from '@/api/page'
+
+export default {
+  data() {
+    this.rules = {
+      plateNumber: { type: 'string', required: true, trigger: 'blur', message: '车牌号必填!' }
+    }
+    return {
+      imageUrl: '',
+      options: [{
+        value: 'a2',
+        label: '通冠'
+      }, {
+        value: 'a3',
+        label: '棣华'
+      }],
+      Form: {},
+    }
+  },
+  mounted() {
+    this.edit()
+  },
+  methods: {
+    edit() {
+      if (this.$route.query.id !== undefined) {
+        postJson('/api/v1/carinfo/get', { id: this.$route.query.id }).then(response => {
+          if (response.code === 200) {
+            this.Form = response.data
+            this.imageUrl = this.Form.img
+          }
+        })
+      }
+    },
+    submit() {
+      let that = this
+      this.$refs.ruleForm.validate(valid => {
+        if (valid) {
+          postJson('/api/v1/carinfo/save', this.Form).then(response => {
+            if (response.code === 200) {
+              that.$router.push({ path: '/car/info', query: { pageIndex: this.pageIndex, word: this.title }})
+            }
+          })
+        }
+      })
+    },
+    handleAvatarSuccess(res, file) {
+      if(res.code == 200){
+        this.Form.image = "http://file.it5000.com/" + res.data
+        this.imageUrl = URL.createObjectURL(file.raw);
+
+        postForm('/api/app/car/img2text', {imgUrl: this.Form.image}).then(response => {
+          if (response.status == 200) {
+            this.Form = response.data
+          }
+        })
+
+      } else{
+
+      }
+    }
+  }
+}
+</script>
+<style>
+  .avatar-uploader .el-upload {
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
+    width: 90%;
+    max-width: 278px;
+  }
+  .avatar-uploader .el-upload:hover {
+    border-color: #409EFF;
+  }
+  .avatar-uploader-icon {
+    font-size: 28px;
+    color: #8c939d;
+    width: 90%;
+    max-width: 278px;
+    height: 178px;
+    line-height: 178px;
+    text-align: center;
+  }
+  .avatar {
+    width: 90%;
+    max-width: 278px;
+    height: 178px;
+    display: block;
+  }
+
+  .el-input{max-width: 300px;}
+</style>

+ 188 - 0
src/views/car/carinfo/index.vue

@@ -0,0 +1,188 @@
+<template>
+  <div class="app-container">
+
+    <el-row>
+
+      <el-form :inline="true" class="demo-form-inline">
+        <el-form-item label="车牌">
+          <el-input v-model="page.param.plateNumber" placeholder="车牌"></el-input>
+        </el-form-item>
+        <el-form-item label="机构">
+          <el-select v-model="page.param.departId" clearable placeholder="请选择">
+            <el-option
+              v-for="item in options"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value">
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="search">查询</el-button>
+        </el-form-item>
+      </el-form>
+
+    </el-row>
+
+    <el-row>
+      <el-col style="text-align: left;margin-bottom: 5px;">
+        <el-button @click="add()">添加</el-button>
+      </el-col>
+    </el-row>
+
+    <el-table
+      v-loading="page.loading"
+      :data="page.data"
+      element-loading-text="Loading"
+      border
+      fit
+      highlight-current-row
+      @sort-change="sortChange"
+    >
+      <el-table-column label="车牌号" prop="plateNumber" sortable="custom" width="120">
+        <template slot-scope="scope">
+          {{ scope.row.plateNumber }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="机构" prop="deptId" sortable="custom" width="80">
+        <template slot-scope="scope">
+          {{ scope.row.deptId == 'a2' ? '通冠' : '棣华'  }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="VIN" prop="vin" sortable="custom">
+        <template slot-scope="scope">
+          {{ scope.row.vin }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="发动机号" width="120">
+        <template slot-scope="scope">
+          {{ scope.row.engineNo }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="车款" prop="model" sortable="custom">
+        <template slot-scope="scope">
+          {{ scope.row.model }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="上户时间" width="100">
+        <template slot-scope="scope">
+          {{ scope.row.registerDate }}
+        </template>
+      </el-table-column>
+
+      <!--<el-table-column label="认证时间" width="100">-->
+        <!--<template slot-scope="scope">-->
+          <!--{{ scope.row.certDate }}-->
+        <!--</template>-->
+      <!--</el-table-column>-->
+
+      <el-table-column align="center" prop="updateTime" label="更新时间" width="180" sortable="custom">
+        <template slot-scope="scope">
+          <span>{{ scope.row.updateTime }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column align="center" label="状态">
+        <template slot-scope="scope">
+          <el-tag size="mini" type="success" v-if="scope.row.status == '查询成功'">{{scope.row.status}}</el-tag>
+          <el-tag size="mini" type="warning" v-else-if="scope.row.status" >{{scope.row.status}}</el-tag>
+        </template>
+      </el-table-column>
+
+      <el-table-column align="center" label="操作" width="180">
+        <template slot-scope="scope">
+          <el-button size="mini" @click="edit(scope.row.id)" icon="el-icon-edit">编辑</el-button>
+          <el-button size="mini" type="danger" @click="del(scope.row.id)" icon="el-icon-delete">删除</el-button>
+        </template>
+      </el-table-column>
+
+    </el-table>
+
+    <pagination v-show="page.total>0" :total="page.total" :page.sync="page.param.index" :limit.sync="page.param.length" @pagination="fetchData" />
+  </div>
+</template>
+
+<script>
+import { postJson, getSort } from '@/api/page'
+import Pagination from '@/components/Pagination'
+
+export default {
+  components: { Pagination },
+  data() {
+    return {
+      page: {
+        loading: true,
+        data: null,
+        total: 0,
+        param: {
+          index: 1,
+          length: 10,
+          sort: 'updateTime desc',
+          plateNumber: ''
+        }
+      },
+      options: [{
+        value: '2',
+        label: '通冠'
+      }, {
+        value: '3',
+        label: '棣华'
+      }]
+    }
+  },
+  created() {
+    if (this.$route.query.pageIndex !== undefined) {
+      this.page.param.index = this.$route.query.pageIndex
+    }
+    if (this.$route.query.word !== undefined) {
+      this.page.param.title = this.$route.query.word
+    }
+    this.fetchData()
+  },
+  methods: {
+    search() {
+      this.page.param.index = 1
+      this.fetchData()
+    },
+    fetchData() {
+      this.page.loading = true
+      postJson('/api/v1/carinfo/pageQuery', this.page.param).then(response => {
+        if (response.code === 200) {
+          this.page.data = response.data
+          this.page.total = response.total
+          this.page.loading = false
+        }
+      })
+    },
+    sortChange(data) {
+      this.page.param.index = 1
+      this.page.param.sort = getSort(data)
+      this.fetchData()
+    },
+    add() {
+      this.$router.push('/car/info/add')
+    },
+    edit(val) {
+      this.$router.push({ path: '/car/info/add', query: { id: val, pageIndex: this.page.param.index, word: this.page.param.title }})
+    },
+    del(val) {
+      this.$confirm('此操作将删除该数据, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        postJson('/api/v1/carinfo/delete', { id: val }).then(response => {
+          if (response.code === 200) {
+            this.fetchData()
+          }
+        })
+      })
+    }
+  }
+}
+</script>

+ 246 - 0
src/views/car/driver/index.vue

@@ -0,0 +1,246 @@
+<template>
+  <div class="app-container">
+
+    <el-row>
+
+      <el-form :inline="true" class="demo-form-inline">
+        <el-form-item label="车牌">
+          <el-input v-model="page.param.plateNumber" placeholder="车牌"></el-input>
+        </el-form-item>
+        <el-form-item label="车机号">
+          <el-input v-model="page.param.din" placeholder="车机号"></el-input>
+        </el-form-item>
+        <el-form-item label="机构">
+          <el-select v-model="page.param.orgId" filterable placeholder="选择机构">
+            <el-option label="全部" value=""></el-option>
+            <el-option
+              v-for="item in parentOption"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value">
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-select v-model="page.param.status" placeholder="选择状态">
+            <el-option label="全部" value=""></el-option>
+            <el-option label="正常" value="0"></el-option>
+            <el-option label="维修" value="1"></el-option>
+            <el-option label="报废" value="2"></el-option>
+            <el-option label="白名单" value="3"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="search">查询</el-button>
+        </el-form-item>
+      </el-form>
+
+    </el-row>
+
+    <el-row>
+      <el-col style="text-align: left;margin-bottom: 5px;">
+        <el-button @click="add()">添加</el-button>
+      </el-col>
+    </el-row>
+
+    <el-table
+      v-loading="page.loading"
+      :data="page.data"
+      element-loading-text="Loading"
+      border
+      fit
+      highlight-current-row
+      @sort-change="sortChange"
+    >
+      <el-table-column label="车牌号" prop="plateNumber" sortable="custom" width="120">
+        <template slot-scope="scope">
+          {{ scope.row.plateNumber }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="车机" prop="machineId" sortable="custom" width="140">
+        <template slot-scope="scope">
+          {{ scope.row.din }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="OPEN_ID" prop="openCarId" sortable="custom">
+        <template slot-scope="scope">
+          {{ scope.row.openCarId }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="机构" prop="orgId" sortable="custom">
+        <template slot-scope="scope">
+          {{ scope.row.orgName }}
+        </template>
+      </el-table-column>
+
+
+      <el-table-column label="车款" prop="carModelId" sortable="custom"  width="100">
+        <template slot-scope="scope">
+          {{ scope.row.carModelId }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="机构" prop="orgId" sortable="custom">
+        <template slot-scope="scope">
+          {{ scope.row.orgName }}
+        </template>
+      </el-table-column>
+
+      <el-table-column align="center" prop="updateTime" label="更新时间" width="180" sortable="custom">
+        <template slot-scope="scope">
+          <span>{{ scope.row.updateTime }}</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column align="center" prop="status" label="状态" width="80" sortable="custom">
+        <template slot-scope="scope">
+          <span v-if="scope.row.status == 0">正常</span>
+          <span v-if="scope.row.status == 1">维修</span>
+          <span v-if="scope.row.status == 2">报废</span>
+          <span v-if="scope.row.status == 3">白名单</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column align="center" label="操作" width="460">
+        <template slot-scope="scope">
+          <el-button size="mini" type="danger" @click="clean(scope.row.id)" icon="el-icon-delete">清洗</el-button>
+          <el-button size="mini" type="success" v-if="!scope.row.openCarId" @click="genOpenId(scope.row.id)">生成ID</el-button>
+          <el-button size="mini" type="warning" v-if="scope.row.din" @click="unbind(scope.row.id)" icon="el-icon-remove-outline">解绑</el-button>
+          <el-button size="mini" @click="edit(scope.row.id)" icon="el-icon-edit">编辑</el-button>
+          <el-button size="mini" type="danger" @click="del(scope.row.id)" icon="el-icon-delete">删除</el-button>
+        </template>
+      </el-table-column>
+
+    </el-table>
+
+    <pagination v-show="page.total>0" :total="page.total" :page.sync="page.param.index" :limit.sync="page.param.length" @pagination="fetchData" />
+  </div>
+</template>
+
+<script>
+import { postJson, getSort } from '@/api/page'
+import Pagination from '@/components/Pagination'
+
+export default {
+  components: { Pagination },
+  data() {
+    return {
+      page: {
+        loading: true,
+        data: null,
+        total: 0,
+        param: {
+          index: 1,
+          length: 10,
+          sort: 'updateTime desc',
+          plateNumber: '',
+          din: '',
+          orgId: '',
+          status: ''
+        }
+      },
+      parentOption: []
+    }
+  },
+  created() {
+    if (this.$route.query.pageIndex !== undefined) {
+      this.page.param.index = this.$route.query.pageIndex
+    }
+    if (this.$route.query.word !== undefined) {
+      this.page.param.title = this.$route.query.word
+    }
+    this.parentQuery()
+    this.fetchData()
+  },
+  methods: {
+    search() {
+      this.page.param.index = 1
+      this.fetchData()
+    },
+    fetchData() {
+      this.page.loading = true
+      postJson('/api/v1/car/pageQuery', this.page.param).then(response => {
+        if (response.code === 200) {
+          this.page.data = response.data
+          this.page.total = response.total
+          this.page.loading = false
+        }
+      })
+    },
+    sortChange(data) {
+      this.page.param.index = 1
+      this.page.param.sort = getSort(data)
+      this.fetchData()
+    },
+    add() {
+      this.$router.push('/data/car/add')
+    },
+    edit(val) {
+      this.$router.push({ path: '/data/car/add', query: { id: val, pageIndex: this.page.param.index, word: this.page.param.title }})
+    },
+    genOpenId(val) {
+      this.$confirm('确定要生成OPENID吗', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        postJson('/api/v1/car/genOpenID', { id: val }).then(response => {
+          if (response.code === 200) {
+            this.fetchData()
+          }
+        })
+      })
+    },
+    unbind(val) {
+      this.$confirm('此操作将解绑车机, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        postJson('/api/v1/car/unbind', { id: val }).then(response => {
+          if (response.code === 200) {
+            this.fetchData()
+          }
+        })
+      })
+    },
+    del(val) {
+      this.$confirm('此操作将删除该数据, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        postJson('/api/v1/car/delete', { id: val }).then(response => {
+          if (response.code === 200) {
+            this.fetchData()
+          }
+        })
+      })
+    },
+    clean(val) {
+      this.$confirm('此操作将删除该设备非当前车辆下的历史数据, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        postJson('/api/v1/car/clean', { id: val }).then(response => {
+          if (response.code === 200) {
+            this.fetchData()
+          }
+        })
+      })
+    },
+    parentQuery() {
+      this.page.loading = true
+      postJson('/api/v1/org/parentQuery').then(response => {
+        if (response.code === 200) {
+          this.parentOption = response.data
+        }
+      })
+    }
+  }
+}
+</script>

+ 160 - 0
src/views/car/event/vlt.vue

@@ -0,0 +1,160 @@
+<template>
+  <div class="app-container">
+
+    <el-row>
+
+      <el-form :inline="true" class="demo-form-inline">
+        <el-form-item label="车牌">
+          <el-input v-model="page.param.plateNumber" placeholder="车牌"></el-input>
+        </el-form-item>
+        <el-form-item label="违章时间">
+          <el-date-picker
+            v-model="page.param.day"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+          >
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" @click="search">查询</el-button>
+          <el-button type="warning" icon="el-icon-refresh" @click="sync()">手动同步</el-button>
+        </el-form-item>
+      </el-form>
+
+    </el-row>
+
+    <el-table
+      v-loading="page.loading"
+      :data="page.data"
+      element-loading-text="Loading"
+      border
+      fit
+      highlight-current-row
+      @sort-change="sortChange"
+    >
+      <el-table-column label="车牌号" width="120">
+        <template slot-scope="scope">
+          {{ scope.row.carInfo !== undefined ? scope.row.carInfo.plateNumber : '' }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="机构" width="80">
+        <template slot-scope="scope">
+          {{ (scope.row.carInfo !== undefined  && scope.row.carInfo.deptId == 'a2') ? '通冠' : '棣华' }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="状态" width="90">
+        <template slot-scope="scope">
+          <el-tag size="mini" type="warning" v-if="scope.row.status == 0">待处理</el-tag>
+          <el-tag size="mini" type="success" v-else-if="scope.row.status == 1" >已处理</el-tag>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="扣分/罚款" width="90" align="right">
+        <template slot-scope="scope">
+          {{ scope.row.vlt.fen}} / {{ scope.row.vlt.money }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="违章时间" prop="eventTime" sortable="custom" width="160">
+        <template slot-scope="scope">
+          {{ scope.row.vlt.date }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="违章描述">
+        <template slot-scope="scope">
+          {{ scope.row.vlt.act }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="违章地址" class="hidden-xs-only">
+        <template slot-scope="scope">
+          {{ scope.row.vlt.area }}
+        </template>
+      </el-table-column>
+
+    </el-table>
+
+    <pagination v-show="page.total>0" :total="page.total" :page.sync="page.param.index" :limit.sync="page.param.length" @pagination="fetchData" />
+  </div>
+</template>
+
+<script>
+  import 'element-ui/lib/theme-chalk/display.css';
+import { postJson, postForm, getSort, post } from '@/api/page'
+import Pagination from '@/components/Pagination'
+
+export default {
+  components: { Pagination },
+  data() {
+    return {
+      page: {
+        loading: true,
+        data: null,
+        total: 0,
+        param: {
+          index: 1,
+          length: 10,
+          sort: 'status asc,eventTime desc'
+        }
+      },
+      parentOption: []
+    }
+  },
+  created() {
+    if (this.$route.query.pageIndex !== undefined) {
+      this.page.param.index = this.$route.query.pageIndex
+    }
+    if (this.$route.query.word !== undefined) {
+      this.page.param.title = this.$route.query.word
+    }
+    this.fetchData()
+  },
+  methods: {
+    search() {
+      this.page.param.index = 1
+      this.fetchData()
+    },
+    fetchData() {
+      this.page.loading = true
+      postJson('/api/v1/carevent/pageQuery', this.page.param).then(response => {
+        if (response.code === 200) {
+          this.page.data = response.data
+          this.page.total = response.total
+          this.page.loading = false
+        }
+      })
+    },
+    sortChange(data) {
+      this.page.param.index = 1
+      this.page.param.sort = getSort(data)
+      this.fetchData()
+    },
+    sync() {
+      this.page.loading = true
+      postForm('/api/v1/carevent/sync').then(response => {
+          this.fetchData()
+      })
+    },
+    edit(val) {
+      this.$router.push({ path: '/car/info/add', query: { id: val, pageIndex: this.page.param.index, word: this.page.param.title }})
+    },
+    del(val) {
+      this.$confirm('此操作将删除该数据, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        postJson('/api/v1/carevent/delete', { id: val }).then(response => {
+          if (response.code === 200) {
+            this.fetchData()
+          }
+        })
+      })
+    }
+  }
+}
+</script>

+ 102 - 0
src/views/dashboard/components/BarChart.vue

@@ -0,0 +1,102 @@
+<template>
+  <div :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+require('echarts/theme/macarons') // echarts theme
+import resize from './mixins/resize'
+
+const animationDuration = 6000
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '100%'
+    },
+    height: {
+      type: String,
+      default: '300px'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initChart()
+    })
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(this.$el, 'macarons')
+
+      this.chart.setOption({
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { // 坐标轴指示器,坐标轴触发有效
+            type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
+          }
+        },
+        grid: {
+          top: 10,
+          left: '2%',
+          right: '2%',
+          bottom: '3%',
+          containLabel: true
+        },
+        xAxis: [{
+          type: 'category',
+          data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+          axisTick: {
+            alignWithLabel: true
+          }
+        }],
+        yAxis: [{
+          type: 'value',
+          axisTick: {
+            show: false
+          }
+        }],
+        series: [{
+          name: 'pageA',
+          type: 'bar',
+          stack: 'vistors',
+          barWidth: '60%',
+          data: [79, 52, 200, 334, 390, 330, 220],
+          animationDuration
+        }, {
+          name: 'pageB',
+          type: 'bar',
+          stack: 'vistors',
+          barWidth: '60%',
+          data: [80, 52, 200, 334, 390, 330, 220],
+          animationDuration
+        }, {
+          name: 'pageC',
+          type: 'bar',
+          stack: 'vistors',
+          barWidth: '60%',
+          data: [30, 52, 200, 334, 390, 330, 220],
+          animationDuration
+        }]
+      })
+    }
+  }
+}
+</script>

+ 154 - 0
src/views/dashboard/components/LineChart.vue

@@ -0,0 +1,154 @@
+<template>
+  <div :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+require('echarts/theme/macarons')
+
+export default {
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '100%'
+    },
+    height: {
+      type: String,
+      default: '350px'
+    },
+    autoResize: {
+      type: Boolean,
+      default: true
+    },
+    chartData: {
+      type: Object,
+      required: true
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  watch: {
+    chartData: {
+      deep: true,
+      handler(val) {
+        this.setOptions(val)
+      }
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initChart()
+    })
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(this.$el, 'macarons')
+      this.setOptions(this.chartData)
+    },
+    setOptions({ day, orderInc, userInc, cashInc } = {}) {
+      if(undefined == day) return
+      this.chart.setOption({
+        xAxis: {
+          data: day,
+          boundaryGap: false,
+          axisTick: {
+            show: false
+          }
+        },
+        grid: {
+          left: 10,
+          right: 10,
+          bottom: 20,
+          top: 30,
+          containLabel: true
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            type: 'cross'
+          },
+          padding: [5, 10]
+        },
+        yAxis: {
+          axisTick: {
+            show: false
+          }
+        },
+        legend: {
+          data: ['代理', '订单', '提现']
+        },
+        series: [{
+          name: '代理', itemStyle: {
+            normal: {
+              color: '#FF005A',
+              lineStyle: {
+                color: '#FF005A',
+                width: 2
+              }
+            }
+          },
+          smooth: true,
+          type: 'line',
+          data: userInc,
+          animationDuration: 2800,
+          animationEasing: 'cubicInOut'
+        },
+        {
+          name: '订单',
+          smooth: true,
+          type: 'line',
+          itemStyle: {
+            normal: {
+              color: '#3888fa',
+              lineStyle: {
+                color: '#3888fa',
+                width: 2
+              },
+              areaStyle: {
+                color: '#f3f8ff'
+              }
+            }
+          },
+          data: orderInc,
+          animationDuration: 2800,
+          animationEasing: 'quadraticOut'
+        },
+          {
+            name: '提现',
+            smooth: true,
+            type: 'line',
+            itemStyle: {
+              normal: {
+                color: '#b006b2',
+                lineStyle: {
+                  color: '#b006b2',
+                  width: 2
+                },
+                areaStyle: {
+                  color: '#fff8ff'
+                }
+              }
+            },
+            data: cashInc,
+            animationDuration: 2800,
+            animationEasing: 'quadraticOut'
+          }]
+      })
+    }
+  }
+}
+</script>

+ 178 - 0
src/views/dashboard/components/PanelGroup.vue

@@ -0,0 +1,178 @@
+<template>
+  <el-row :gutter="40" class="panel-group">
+    <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
+      <div class="card-panel">
+        <div class="card-panel-icon-wrapper icon-people">
+          <svg-icon icon-class="peoples" class-name="card-panel-icon"/>
+        </div>
+        <div class="card-panel-description">
+          <div class="card-panel-text">
+            代理总数
+          </div>
+          <count-to :start-val="0" :end-val="data.userTotal" :duration="2600" class="card-panel-num"/>
+        </div>
+      </div>
+    </el-col>
+
+    <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
+      <div class="card-panel">
+        <div class="card-panel-icon-wrapper icon-money">
+          <svg-icon icon-class="money" class-name="card-panel-icon"/>
+        </div>
+        <div class="card-panel-description">
+          <div class="card-panel-text">
+            累计提现
+          </div>
+          <count-to :start-val="0" :end-val="data.cashTotal" :duration="3200" class="card-panel-num"/>
+        </div>
+      </div>
+    </el-col>
+    <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
+      <div class="card-panel">
+        <div class="card-panel-icon-wrapper icon-shopping">
+          <svg-icon icon-class="shopping" class-name="card-panel-icon"/>
+        </div>
+        <div class="card-panel-description">
+          <div class="card-panel-text">
+            累计订单
+          </div>
+          <count-to :start-val="0" :end-val="data.orderTotal" :duration="3600" class="card-panel-num"/>
+        </div>
+      </div>
+    </el-col>
+    <el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
+      <div class="card-panel">
+        <div class="card-panel-icon-wrapper icon-money">
+          <svg-icon icon-class="money" class-name="card-panel-icon"/>
+        </div>
+        <div class="card-panel-description">
+          <div class="card-panel-text">
+            累计面值
+          </div>
+          <count-to :start-val="0" :end-val="data.orderAmount" :duration="3000" class="card-panel-num"/>
+        </div>
+      </div>
+    </el-col>
+  </el-row>
+</template>
+
+<script>
+  import CountTo from 'vue-count-to'
+
+  export default {
+    props: ['data'],
+    components: {
+      CountTo
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .panel-group {
+    margin-top: 18px;
+
+    .card-panel-col {
+      margin-bottom: 32px;
+    }
+
+    .card-panel {
+      height: 108px;
+      cursor: pointer;
+      font-size: 12px;
+      position: relative;
+      overflow: hidden;
+      color: #666;
+      background: #fff;
+      box-shadow: 4px 4px 40px rgba(0, 0, 0, .05);
+      border-color: rgba(0, 0, 0, .05);
+
+      &:hover {
+        .card-panel-icon-wrapper {
+          color: #fff;
+        }
+
+        .icon-people {
+          background: #40c9c6;
+        }
+
+        .icon-message {
+          background: #36a3f7;
+        }
+
+        .icon-money {
+          background: #f4516c;
+        }
+
+        .icon-shopping {
+          background: #34bfa3
+        }
+      }
+
+      .icon-people {
+        color: #40c9c6;
+      }
+
+      .icon-message {
+        color: #36a3f7;
+      }
+
+      .icon-money {
+        color: #f4516c;
+      }
+
+      .icon-shopping {
+        color: #34bfa3
+      }
+
+      .card-panel-icon-wrapper {
+        float: left;
+        margin: 14px 0 0 14px;
+        padding: 16px;
+        transition: all 0.38s ease-out;
+        border-radius: 6px;
+      }
+
+      .card-panel-icon {
+        float: left;
+        font-size: 48px;
+      }
+
+      .card-panel-description {
+        float: right;
+        font-weight: bold;
+        margin: 26px;
+        margin-left: 0px;
+
+        .card-panel-text {
+          line-height: 18px;
+          color: rgba(0, 0, 0, 0.45);
+          font-size: 16px;
+          margin-bottom: 12px;
+        }
+
+        .card-panel-num {
+          font-size: 20px;
+        }
+      }
+    }
+  }
+
+  @media (max-width: 550px) {
+    .card-panel-description {
+      display: none;
+    }
+
+    .card-panel-icon-wrapper {
+      float: none !important;
+      width: 100%;
+      height: 100%;
+      margin: 0 !important;
+
+      .svg-icon {
+        display: block;
+        margin: 14px auto !important;
+        float: none !important;
+      }
+    }
+  }
+</style>

+ 79 - 0
src/views/dashboard/components/PieChart.vue

@@ -0,0 +1,79 @@
+<template>
+  <div :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+require('echarts/theme/macarons') // echarts theme
+import resize from './mixins/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '100%'
+    },
+    height: {
+      type: String,
+      default: '300px'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initChart()
+    })
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(this.$el, 'macarons')
+
+      this.chart.setOption({
+        tooltip: {
+          trigger: 'item',
+          formatter: '{a} <br/>{b} : {c} ({d}%)'
+        },
+        legend: {
+          left: 'center',
+          bottom: '10',
+          data: ['Industries', 'Technology', 'Forex', 'Gold', 'Forecasts']
+        },
+        series: [
+          {
+            name: 'WEEKLY WRITE ARTICLES',
+            type: 'pie',
+            roseType: 'radius',
+            radius: [15, 95],
+            center: ['50%', '38%'],
+            data: [
+              { value: 320, name: 'Industries' },
+              { value: 240, name: 'Technology' },
+              { value: 149, name: 'Forex' },
+              { value: 100, name: 'Gold' },
+              { value: 59, name: 'Forecasts' }
+            ],
+            animationEasing: 'cubicInOut',
+            animationDuration: 2600
+          }
+        ]
+      })
+    }
+  }
+}
+</script>

+ 116 - 0
src/views/dashboard/components/RaddarChart.vue

@@ -0,0 +1,116 @@
+<template>
+  <div :class="className" :style="{height:height,width:width}" />
+</template>
+
+<script>
+import echarts from 'echarts'
+require('echarts/theme/macarons') // echarts theme
+import resize from './mixins/resize'
+
+const animationDuration = 3000
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: 'chart'
+    },
+    width: {
+      type: String,
+      default: '100%'
+    },
+    height: {
+      type: String,
+      default: '300px'
+    }
+  },
+  data() {
+    return {
+      chart: null
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initChart()
+    })
+  },
+  beforeDestroy() {
+    if (!this.chart) {
+      return
+    }
+    this.chart.dispose()
+    this.chart = null
+  },
+  methods: {
+    initChart() {
+      this.chart = echarts.init(this.$el, 'macarons')
+
+      this.chart.setOption({
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { // 坐标轴指示器,坐标轴触发有效
+            type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
+          }
+        },
+        radar: {
+          radius: '66%',
+          center: ['50%', '42%'],
+          splitNumber: 8,
+          splitArea: {
+            areaStyle: {
+              color: 'rgba(127,95,132,.3)',
+              opacity: 1,
+              shadowBlur: 45,
+              shadowColor: 'rgba(0,0,0,.5)',
+              shadowOffsetX: 0,
+              shadowOffsetY: 15
+            }
+          },
+          indicator: [
+            { name: 'Sales', max: 10000 },
+            { name: 'Administration', max: 20000 },
+            { name: 'Information Techology', max: 20000 },
+            { name: 'Customer Support', max: 20000 },
+            { name: 'Development', max: 20000 },
+            { name: 'Marketing', max: 20000 }
+          ]
+        },
+        legend: {
+          left: 'center',
+          bottom: '10',
+          data: ['Allocated Budget', 'Expected Spending', 'Actual Spending']
+        },
+        series: [{
+          type: 'radar',
+          symbolSize: 0,
+          areaStyle: {
+            normal: {
+              shadowBlur: 13,
+              shadowColor: 'rgba(0,0,0,.2)',
+              shadowOffsetX: 0,
+              shadowOffsetY: 10,
+              opacity: 1
+            }
+          },
+          data: [
+            {
+              value: [5000, 7000, 12000, 11000, 15000, 14000],
+              name: 'Allocated Budget'
+            },
+            {
+              value: [4000, 9000, 15000, 15000, 13000, 11000],
+              name: 'Expected Spending'
+            },
+            {
+              value: [5500, 11000, 12000, 15000, 12000, 12000],
+              name: 'Actual Spending'
+            }
+          ],
+          animationDuration: animationDuration
+        }]
+      })
+    }
+  }
+}
+</script>

+ 55 - 0
src/views/dashboard/components/mixins/resize.js

@@ -0,0 +1,55 @@
+import { debounce } from '@/utils'
+
+export default {
+  data() {
+    return {
+      $_sidebarElm: null,
+      $_resizeHandler: null
+    }
+  },
+  mounted() {
+    this.$_resizeHandler = debounce(() => {
+      if (this.chart) {
+        this.chart.resize()
+      }
+    }, 100)
+    this.$_initResizeEvent()
+    this.$_initSidebarResizeEvent()
+  },
+  beforeDestroy() {
+    this.$_destroyResizeEvent()
+    this.$_destroySidebarResizeEvent()
+  },
+  // to fixed bug when cached by keep-alive
+  // https://github.com/PanJiaChen/vue-element-admin/issues/2116
+  activated() {
+    this.$_initResizeEvent()
+    this.$_initSidebarResizeEvent()
+  },
+  deactivated() {
+    this.$_destroyResizeEvent()
+    this.$_destroySidebarResizeEvent()
+  },
+  methods: {
+    // use $_ for mixins properties
+    // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
+    $_initResizeEvent() {
+      window.addEventListener('resize', this.$_resizeHandler)
+    },
+    $_destroyResizeEvent() {
+      window.removeEventListener('resize', this.$_resizeHandler)
+    },
+    $_sidebarResizeHandler(e) {
+      if (e.propertyName === 'width') {
+        this.$_resizeHandler()
+      }
+    },
+    $_initSidebarResizeEvent() {
+      this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
+      this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
+    },
+    $_destroySidebarResizeEvent() {
+      this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
+    }
+  }
+}

+ 31 - 0
src/views/dashboard/index.vue

@@ -0,0 +1,31 @@
+<template>
+  <div class="dashboard-container">
+    <div class="dashboard-text">name: {{ name }}</div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+
+export default {
+  name: 'Dashboard',
+  computed: {
+    ...mapGetters([
+      'name'
+    ])
+  }
+}
+</script>
+
+
+<style lang="scss" scoped>
+.dashboard {
+  &-container {
+    margin: 30px;
+  }
+  &-text {
+    font-size: 30px;
+    line-height: 46px;
+  }
+}
+</style>

+ 85 - 0
src/views/form/index.vue

@@ -0,0 +1,85 @@
+<template>
+  <div class="app-container">
+    <el-form ref="form" :model="form" label-width="120px">
+      <el-form-item label="Activity name">
+        <el-input v-model="form.name" />
+      </el-form-item>
+      <el-form-item label="Activity zone">
+        <el-select v-model="form.region" placeholder="please select your zone">
+          <el-option label="Zone one" value="shanghai" />
+          <el-option label="Zone two" value="beijing" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="Activity time">
+        <el-col :span="11">
+          <el-date-picker v-model="form.date1" type="date" placeholder="Pick a date" style="width: 100%;" />
+        </el-col>
+        <el-col :span="2" class="line">-</el-col>
+        <el-col :span="11">
+          <el-time-picker v-model="form.date2" type="fixed-time" placeholder="Pick a time" style="width: 100%;" />
+        </el-col>
+      </el-form-item>
+      <el-form-item label="Instant delivery">
+        <el-switch v-model="form.delivery" />
+      </el-form-item>
+      <el-form-item label="Activity type">
+        <el-checkbox-group v-model="form.type">
+          <el-checkbox label="Online activities" name="type" />
+          <el-checkbox label="Promotion activities" name="type" />
+          <el-checkbox label="Offline activities" name="type" />
+          <el-checkbox label="Simple brand exposure" name="type" />
+        </el-checkbox-group>
+      </el-form-item>
+      <el-form-item label="Resources">
+        <el-radio-group v-model="form.resource">
+          <el-radio label="Sponsor" />
+          <el-radio label="Venue" />
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="Activity form">
+        <el-input v-model="form.desc" type="textarea" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="onSubmit">Create</el-button>
+        <el-button @click="onCancel">Cancel</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      form: {
+        name: '',
+        region: '',
+        date1: '',
+        date2: '',
+        delivery: false,
+        type: [],
+        resource: '',
+        desc: ''
+      }
+    }
+  },
+  methods: {
+    onSubmit() {
+      this.$message('submit!')
+    },
+    onCancel() {
+      this.$message({
+        message: 'cancel!',
+        type: 'warning'
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.line{
+  text-align: center;
+}
+</style>
+

+ 237 - 0
src/views/login/index.vue

@@ -0,0 +1,237 @@
+<template>
+  <div class="login-container">
+    <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">
+
+      <div class="title-container">
+        <h3 class="title">通冠物流</h3>
+      </div>
+
+      <el-form-item prop="username">
+        <span class="svg-container">
+          <svg-icon icon-class="user" />
+        </span>
+        <el-input
+          ref="username"
+          v-model="loginForm.username"
+          placeholder="Username"
+          name="username"
+          type="text"
+          tabindex="1"
+          auto-complete="on"
+        />
+      </el-form-item>
+
+      <el-form-item prop="password">
+        <span class="svg-container">
+          <svg-icon icon-class="password" />
+        </span>
+        <el-input
+          :key="passwordType"
+          ref="password"
+          v-model="loginForm.password"
+          :type="passwordType"
+          placeholder="Password"
+          name="password"
+          tabindex="2"
+          auto-complete="on"
+          @keyup.enter.native="handleLogin"
+        />
+        <span class="show-pwd" @click="showPwd">
+          <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
+        </span>
+      </el-form-item>
+
+      <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登录</el-button>
+
+      <!--<div class="tips">-->
+        <!--<span style="margin-right:20px;">username: admin</span>-->
+        <!--<span> password: any</span>-->
+      <!--</div>-->
+
+    </el-form>
+  </div>
+</template>
+
+<script>
+import { validUsername } from '@/utils/validate'
+
+export default {
+  name: 'Login',
+  data() {
+    const validateUsername = (rule, value, callback) => {
+      if (!validUsername(value)) {
+        callback(new Error('Please enter the correct user name'))
+      } else {
+        callback()
+      }
+    }
+    const validatePassword = (rule, value, callback) => {
+      if (value.length < 6) {
+        callback(new Error('The password can not be less than 6 digits'))
+      } else {
+        callback()
+      }
+    }
+    return {
+      loginForm: {
+        username: 'admin',
+        password: '111111'
+      },
+      loginRules: {
+        username: [{ required: true, trigger: 'blur', validator: validateUsername }],
+        password: [{ required: true, trigger: 'blur', validator: validatePassword }]
+      },
+      loading: false,
+      passwordType: 'password',
+      redirect: undefined
+    }
+  },
+  watch: {
+    $route: {
+      handler: function(route) {
+        this.redirect = route.query && route.query.redirect
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    showPwd() {
+      if (this.passwordType === 'password') {
+        this.passwordType = ''
+      } else {
+        this.passwordType = 'password'
+      }
+      this.$nextTick(() => {
+        this.$refs.password.focus()
+      })
+    },
+    handleLogin() {
+      this.$refs.loginForm.validate(valid => {
+        if (valid) {
+          this.loading = true
+          this.$store.dispatch('user/login', this.loginForm).then(() => {
+            this.$router.push({ path: this.redirect || '/' })
+            this.loading = false
+          }).catch(() => {
+            this.loading = false
+          })
+        } else {
+          console.log('error submit!!')
+          return false
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+/* 修复input 背景不协调 和光标变色 */
+/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
+
+$bg:#283443;
+$light_gray:#fff;
+$cursor: #fff;
+
+@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
+  .login-container .el-input input {
+    color: $cursor;
+  }
+}
+
+/* reset element-ui css */
+.login-container {
+  .el-input {
+    display: inline-block;
+    height: 47px;
+    width: 85%;
+
+    input {
+      background: transparent;
+      border: 0px;
+      -webkit-appearance: none;
+      border-radius: 0px;
+      padding: 12px 5px 12px 15px;
+      color: $light_gray;
+      height: 47px;
+      caret-color: $cursor;
+
+      &:-webkit-autofill {
+        box-shadow: 0 0 0px 1000px $bg inset !important;
+        -webkit-text-fill-color: $cursor !important;
+      }
+    }
+  }
+
+  .el-form-item {
+    border: 1px solid rgba(255, 255, 255, 0.1);
+    background: rgba(0, 0, 0, 0.1);
+    border-radius: 5px;
+    color: #454545;
+  }
+}
+</style>
+
+<style lang="scss" scoped>
+$bg:#2d3a4b;
+$dark_gray:#889aa4;
+$light_gray:#eee;
+
+.login-container {
+  min-height: 100%;
+  width: 100%;
+  background-color: $bg;
+  overflow: hidden;
+
+  .login-form {
+    position: relative;
+    width: 520px;
+    max-width: 100%;
+    padding: 160px 35px 0;
+    margin: 0 auto;
+    overflow: hidden;
+  }
+
+  .tips {
+    font-size: 14px;
+    color: #fff;
+    margin-bottom: 10px;
+
+    span {
+      &:first-of-type {
+        margin-right: 16px;
+      }
+    }
+  }
+
+  .svg-container {
+    padding: 6px 5px 6px 15px;
+    color: $dark_gray;
+    vertical-align: middle;
+    width: 30px;
+    display: inline-block;
+  }
+
+  .title-container {
+    position: relative;
+
+    .title {
+      font-size: 26px;
+      color: $light_gray;
+      margin: 0px auto 40px auto;
+      text-align: center;
+      font-weight: bold;
+    }
+  }
+
+  .show-pwd {
+    position: absolute;
+    right: 10px;
+    top: 7px;
+    font-size: 16px;
+    color: $dark_gray;
+    cursor: pointer;
+    user-select: none;
+  }
+}
+</style>

+ 7 - 0
src/views/nested/menu1/index.vue

@@ -0,0 +1,7 @@
+<template>
+  <div style="padding:30px;">
+    <el-alert :closable="false" title="menu 1">
+      <router-view />
+    </el-alert>
+  </div>
+</template>

+ 7 - 0
src/views/nested/menu1/menu1-1/index.vue

@@ -0,0 +1,7 @@
+<template>
+  <div style="padding:30px;">
+    <el-alert :closable="false" title="menu 1-1" type="success">
+      <router-view />
+    </el-alert>
+  </div>
+</template>

+ 7 - 0
src/views/nested/menu1/menu1-2/index.vue

@@ -0,0 +1,7 @@
+<template>
+  <div style="padding:30px;">
+    <el-alert :closable="false" title="menu 1-2" type="success">
+      <router-view />
+    </el-alert>
+  </div>
+</template>

+ 5 - 0
src/views/nested/menu1/menu1-2/menu1-2-1/index.vue

@@ -0,0 +1,5 @@
+<template functional>
+  <div style="padding:30px;">
+    <el-alert :closable="false" title="menu 1-2-1" type="warning" />
+  </div>
+</template>

+ 5 - 0
src/views/nested/menu1/menu1-2/menu1-2-2/index.vue

@@ -0,0 +1,5 @@
+<template functional>
+  <div style="padding:30px;">
+    <el-alert :closable="false" title="menu 1-2-2" type="warning" />
+  </div>
+</template>

+ 5 - 0
src/views/nested/menu1/menu1-3/index.vue

@@ -0,0 +1,5 @@
+<template functional>
+  <div style="padding:30px;">
+    <el-alert :closable="false" title="menu 1-3" type="success" />
+  </div>
+</template>

+ 5 - 0
src/views/nested/menu2/index.vue

@@ -0,0 +1,5 @@
+<template>
+  <div style="padding:30px;">
+    <el-alert :closable="false" title="menu 2" />
+  </div>
+</template>

+ 306 - 0
src/views/sys/sys_depart/index.vue

@@ -0,0 +1,306 @@
+<template>
+  <div class="app-container">
+    <el-row>
+      <el-col :span="8">
+        <el-row>
+          <el-input v-model="filterText" placeholder="关键词过滤" style="margin-bottom:30px;width:60%;"/>
+        </el-row>
+        <el-tree
+          ref="tree"
+          :data="data"
+          :props="defaultProps"
+          :filter-node-method="filterNode"
+          @node-click="handleNodeClick"
+          class="filter-tree"
+          default-expand-all
+        />
+      </el-col>
+
+      <el-col :span="16">
+        <el-tabs v-model="activeName" type="card">
+          <el-tab-pane label="部门管理" name="first">
+
+            <el-table
+              v-loading="page.loading"
+              :data="page.data"
+              element-loading-text="Loading"
+              border
+              fit
+              highlight-current-row
+              @sort-change="sortChange"
+            >
+              <el-table-column label="名称" prop="title" sortable="custom">
+                <template slot-scope="scope">
+                  {{ scope.row.title }}
+                </template>
+              </el-table-column>
+
+              <el-table-column label="排序" prop="sort" sortable="custom">
+                <template slot-scope="scope">
+                  {{ scope.row.sort }}
+                </template>
+              </el-table-column>
+
+              <el-table-column align="center" label="操作" width="150">
+                <template slot-scope="scope">
+                  <el-button size="mini" @click="departmentEdit(scope.row.id)">编辑</el-button>
+                  <el-button size="mini" type="danger" @click="departmentDelete(scope.row.id)"><i
+                    class="icon-delete"></i>删除
+                  </el-button>
+                </template>
+              </el-table-column>
+
+            </el-table>
+
+            <pagination v-show="page.total>0" :total="page.total" :page.sync="page.param.index"
+                        :limit.sync="page.param.length" @pagination="departmentFetchData"/>
+
+          </el-tab-pane>
+          <el-tab-pane label="用户管理" name="second">
+
+            <el-row>
+              <el-col style="text-align: left;margin-bottom: 5px;">
+                <el-button @click="userAdd()">添加</el-button>
+              </el-col>
+            </el-row>
+
+            <el-table
+              v-loading="userPage.loading"
+              :data="userPage.data"
+              element-loading-text="Loading"
+              border
+              fit
+              highlight-current-row
+              @sort-change="userSortChange"
+            >
+              <el-table-column label="账号" prop="userName" sortable="custom">
+                <template slot-scope="scope">
+                  {{ scope.row.userName }}
+                </template>
+              </el-table-column>
+
+              <el-table-column label="电话" prop="mobile" sortable="custom">
+                <template slot-scope="scope">
+                  {{ scope.row.mobile }}
+                </template>
+              </el-table-column>
+
+              <el-table-column label="部门" prop="departmentId" sortable="custom">
+                <template slot-scope="scope">
+                  {{ scope.row.real_name }}
+                </template>
+              </el-table-column>
+
+              <el-table-column align="center" prop="status" label="状态" width="80" sortable="custom">
+                <template slot-scope="scope">
+                  <span v-if="scope.row.status == 1">正常</span>
+                  <span v-if="scope.row.status == 2">临时</span>
+                  <span v-if="scope.row.status == 3">停用</span>
+                  <span v-if="scope.row.status == 4">锁定</span>
+                  <span v-if="scope.row.status == 5">删除</span>
+                </template>
+              </el-table-column>
+
+              <el-table-column align="center" label="操作" width="240">
+                <template slot-scope="scope">
+                  <el-button size="mini" @click="userRes(scope.row.acc_id)">授权</el-button>
+                  <el-button size="mini" @click="userEdit(scope.row.acc_id)">编辑</el-button>
+                  <el-button size="mini" type="danger" @click="userDelete(scope.row.acc_id)"><i class="icon-delete"></i>删除
+                  </el-button>
+                </template>
+              </el-table-column>
+
+            </el-table>
+
+            <pagination v-show="userPage.total>0" :total="userPage.total" :page.sync="userPage.param.index"
+                        :limit.sync="userPage.param.length" @pagination="userFetchData"/>
+
+          </el-tab-pane>
+
+        </el-tabs>
+      </el-col>
+    </el-row>
+
+
+  </div>
+</template>
+
+<script>
+
+  import { postJson, getSort } from '@/api/page'
+  import Pagination from '@/components/Pagination'
+  import { getToken } from '@/utils/auth'
+
+  export default {
+    components: { Pagination },
+    data() {
+      return {
+        filterText: '',
+        checked: false,
+        data: [],
+        activeName: 'first',
+        orgId: '',
+        header: { 'X-Token': getToken() },
+        defaultProps: {
+          children: 'children',
+          label: 'label'
+        },
+
+        page: {
+          loading: false,
+          data: null,
+          total: 0,
+          param: {
+            index: 1,
+            length: 10,
+            sort: 'updateTime desc',
+          }
+        },
+        userPage: {
+          loading: false,
+          data: null,
+          total: 0,
+          param: {
+            index: 1,
+            length: 10,
+            sort: 'updateTime desc',
+          }
+        },
+        excelImport: {
+          url: '',
+          templateUrl: '/api/v1/machine/excelTemplate?name=machine.xls'
+        }
+      }
+    },
+    watch: {
+      filterText(val) {
+        this.$refs.tree.filter(val)
+      }
+    },
+    created() {
+      this.fetchTreeData()
+    },
+    methods: {
+      filterNode(value, data) {
+        if (!value) return true
+        return data.label.indexOf(value) !== -1
+      },
+      handleNodeClick(data) {
+        this.page.param.parentId = data.id
+        this.userPage.param.departmentId = data.id
+        this.excelImport.url = '/api/v1/machine/excelImport/' + data.id
+        this.departmentFetchData()
+        this.userFetchData()
+      },
+      fetchTreeData() {
+        postJson('/api/v1/sys/department/tree').then(response => {
+          if (response.code === 200) {
+            this.data = response.data
+          }
+        })
+      },
+      change(){
+        this.fetchTreeData()
+      },
+      sortChange(data) {
+        this.page.param.index = 1
+        this.page.param.sort = getSort(data)
+        this.departmentFetchData()
+      },
+      departmentFetchData() {
+        this.page.loading = true
+        postJson('/api/v1/sys/department/pageQuery', this.page.param).then(response => {
+          if (response.code === 200) {
+            this.page.data = response.data
+            this.page.total = response.total
+            this.page.loading = false
+          }
+        })
+      },
+      departmentEdit(val) {
+        this.$router.push({
+          path: '/data/org/add',
+          query: { id: val, pageIndex: this.page.param.index, word: this.page.param.title }
+        })
+      },
+      departmentDelete(val) {
+        this.$confirm('此操作将删除该数据, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          postJson('/api/v1/org/delete', { id: val }).then(response => {
+            if (response.code === 200) {
+              this.fetchData()
+            }
+          })
+        })
+      },
+
+      userFetchData() {
+        this.userPage.loading = true
+        postJson('/api/v1/sys/user/pageQuery', this.userPage.param).then(response => {
+          if (response.code === 200) {
+            this.userPage.data = response.data
+            this.userPage.total = response.total
+            this.userPage.loading = false
+          }
+        })
+      },
+      userSortChange(data) {
+        this.userPage.param.index = 1
+        this.userPage.param.sort = getSort(data)
+        this.userFetchData()
+      },
+      userAdd() {
+        this.$router.push('/data/account/add')
+      },
+      userEdit(val) {
+        this.$router.push({
+          path: '/data/account/add',
+          query: { id: val, pageIndex: this.userPage.param.index, word: this.userPage.param.title }
+        })
+      },
+      userRes(val) {
+        this.$router.push({
+          path: '/data/account/res',
+          query: { id: val, pageIndex: this.userPage.param.index, word: this.userPage.param.title }
+        })
+      },
+      userDelete(val) {
+        this.$confirm('此操作将删除该数据, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          postJson('/api/v1/account/delete', { id: val }).then(response => {
+            if (response.code === 200) {
+              this.userFetchData()
+            }
+          })
+        })
+      },
+
+
+      onSuccess(data) {
+        if (data.code === 200) {
+          this.$message({
+            message: '导入成功',
+            type: 'success'
+          })
+          this.deviceFetchData()
+          this.carFetchData()
+        } else {
+          this.$message.error(data.message)
+        }
+      },
+      /**
+       * 下载模板
+       */
+      getTemplate() {
+        window.location.href = this.excelImport.templateUrl
+      }
+    }
+  }
+</script>
+

+ 90 - 0
src/views/sys/sys_dict/add.vue

@@ -0,0 +1,90 @@
+<template>
+  <div class="app-container">
+
+    <el-form :model="Form" status-icon ref="ruleForm" label-position="right" size="small" label-width='150px'
+             slot="dialog" :rules='rules'>
+
+      <el-form-item prop='groupName' label="分组编码:">
+        <el-input v-model="Form.groupName" style="width:400px"  @blur="loadTitle" placeholder="分组编码"></el-input>
+      </el-form-item>
+
+      <el-form-item prop='groupTitle' label="分组名称:">
+        <el-input v-model="Form.groupTitle" style="width:400px"  placeholder="分组名称"></el-input>
+      </el-form-item>
+
+      <el-form-item prop="name" label="编码:">
+        <el-input v-model="Form.name" style="width:400px" placeholder="编码"></el-input>
+      </el-form-item>
+
+      <el-form-item prop="value" label="值:">
+        <el-input v-model="Form.value" style="width:400px" placeholder="值"></el-input>
+      </el-form-item>
+
+      <el-form-item prop="sort" label="排序:">
+        <el-input v-model="Form.sort" style="width:400px" placeholder="排序"></el-input>
+      </el-form-item>
+
+      <el-button @click="submit()">提交</el-button>
+
+    </el-form>
+
+  </div>
+</template>
+
+<script>/* eslint-disable no-unused-vars,no-undef,quotes */
+
+import { postJson, postForm } from '@/api/page'
+
+export default {
+  data() {
+    this.rules = {
+      groupName: { type: 'string', required: true, trigger: 'blur', message: '分组必填!' },
+      name: { type: 'string', required: true, trigger: 'blur', message: '编码必填!' },
+      value: { type: 'string', required: true, trigger: 'blur', message: '值必填!' }
+    }
+    return {
+      options: [],
+      Form: {
+        groupTitle: ''
+      }
+    }
+  },
+  mounted() {
+    this.edit()
+  },
+  methods: {
+    edit() {
+      if (this.$route.query.id !== undefined) {
+        postForm('/api/v1/sys/dict/get', { id: this.$route.query.id }).then(res => {
+          if (res.status === 200) {
+            this.Form = res.data.data
+          }
+        })
+      }
+    },
+    loadTitle(){
+      if('' != this.Form.groupName) {
+        let that = this
+        postForm('/api/v1/sys/dict/queryGroupTitle', { groupName: this.Form.groupName }).then(res => {
+          if (res.status === 200) {
+            that.Form.groupTitle = res.data.data.groupTitle
+            that.Form.sort = res.data.data.sort + 1
+          }
+        })
+      }
+    },
+    submit() {
+      let that = this
+      this.$refs.ruleForm.validate(valid => {
+        if (valid) {
+          postJson('/api/v1/sys/dict/save', this.Form).then(response => {
+            if (response.code === 200) {
+              that.$router.push({ path: '/system/dict', query: { pageIndex: this.pageIndex, word: this.title }})
+            }
+          })
+        }
+      })
+    }
+  }
+}
+</script>

+ 173 - 0
src/views/sys/sys_dict/index.vue

@@ -0,0 +1,173 @@
+<template>
+  <div class="app-container">
+
+    <el-row>
+
+      <el-form :inline="true" class="demo-form-inline">
+        <el-form-item label="编码">
+          <el-input v-model="page.param.name" placeholder="编码"></el-input>
+        </el-form-item>
+
+        <el-form-item label="值">
+          <el-input v-model="page.param.value" placeholder="值"></el-input>
+        </el-form-item>
+
+        <el-form-item label="分组">
+          <el-select v-model="page.param.groupName" filterable placeholder="选择分组">
+            <el-option label="全部" value=""></el-option>
+            <el-option
+              v-for="item in parentOption"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value">
+            </el-option>
+          </el-select>
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="primary" @click="search">查询</el-button>
+        </el-form-item>
+      </el-form>
+
+    </el-row>
+
+    <el-row>
+      <el-col style="text-align: left;margin-bottom: 5px;">
+        <el-button @click="add()">添加</el-button>
+      </el-col>
+    </el-row>
+
+    <el-table
+      v-loading="page.loading"
+      :data="page.data"
+      element-loading-text="Loading"
+      border
+      fit
+      highlight-current-row
+      @sort-change="sortChange"
+    >
+      <el-table-column label="分组编码">
+        <template slot-scope="scope" prop="groupName" sortable="custom">
+          {{ scope.row.groupName }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="分组">
+        <template slot-scope="scope" prop="groupName" sortable="custom">
+          {{ scope.row.groupTitle }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="编码" prop="name" sortable="custom">
+        <template slot-scope="scope">
+          {{ scope.row.name }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="值" prop="value" sortable="custom">
+        <template slot-scope="scope">
+          {{ scope.row.value }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="排序" prop="sort" sortable="custom" width="100">
+        <template slot-scope="scope">
+          {{ scope.row.sort }}
+        </template>
+      </el-table-column>
+
+      <el-table-column align="center" label="操作" width="150">
+        <template slot-scope="scope">
+          <el-button size="mini" @click="edit(scope.row.id)">编辑</el-button>
+          <el-button size="mini" type="danger" @click="del(scope.row.id)"><i class="icon-delete"></i>删除</el-button>
+        </template>
+      </el-table-column>
+
+    </el-table>
+
+    <pagination v-show="page.total>0" :total="page.total" :page.sync="page.param.index" :limit.sync="page.param.length" @pagination="fetchData" />
+  </div>
+</template>
+
+<script>
+import { postJson, postForm, getSort } from '@/api/page'
+import Pagination from '@/components/Pagination'
+
+export default {
+  components: { Pagination },
+  data() {
+    return {
+      page: {
+        loading: true,
+        data: null,
+        total: 0,
+        param: {
+          index: 1,
+          length: 10,
+          sort: 'groupName desc,sort asc',
+        }
+      },
+      parentOption: []
+    }
+  },
+  created() {
+    if (this.$route.query.pageIndex !== undefined) {
+      this.page.param.index = this.$route.query.pageIndex
+    }
+    if (this.$route.query.word !== undefined) {
+      this.page.param.title = this.$route.query.word
+    }
+    this.parentQuery()
+    this.fetchData()
+  },
+  methods: {
+    search() {
+      this.page.param.index = 1
+      this.fetchData()
+    },
+    fetchData() {
+      this.page.loading = true
+      postJson('/api/v1/sys/dict/pageQuery', this.page.param).then(response => {
+        if (response.code === 200) {
+          this.page.data = response.data
+          this.page.total = response.total
+          this.page.loading = false
+        }
+      })
+    },
+    sortChange(data) {
+      this.page.param.index = 1
+      this.page.param.sort = getSort(data)
+      this.fetchData()
+    },
+    add() {
+      this.$router.push('/system/dict/add')
+    },
+    edit(val) {
+      this.$router.push({ path: '/system/dict/add', query: { id: val, pageIndex: this.page.param.index, word: this.page.param.title }})
+    },
+    del(val) {
+      this.$confirm('此操作将删除该数据, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        center: true
+      }).then(() => {
+        postForm('/api/v1/sys/dict/delete', { id: val }).then(res => {
+          if (res.data.code === 200) {
+            this.fetchData()
+          }
+        })
+      })
+    },
+    parentQuery() {
+      this.page.loading = true
+      postJson('/api/v1/sys/dict/groups').then(response => {
+        if (response.code === 200) {
+          this.parentOption = response.data
+        }
+      })
+    }
+  }
+}
+</script>

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