wang jun il y a 2 ans
commit
1872d297c3
100 fichiers modifiés avec 3750 ajouts et 0 suppressions
  1. 5 0
      .babelrc
  2. 9 0
      .editorconfig
  3. 0 0
      .eslintignore
  4. 21 0
      .eslintrc.js
  5. 29 0
      .gitignore
  6. 8 0
      .htaccess
  7. 8 0
      .htaccess_1
  8. 5 0
      .postcssrc.js
  9. 5 0
      .travis.yml
  10. 21 0
      LICENSE
  11. 115 0
      README.md
  12. 3 0
      cypress.json
  13. 78 0
      package.json
  14. BIN
      public/favicon.ico
  15. 17 0
      public/index.html
  16. 27 0
      src/App.vue
  17. 166 0
      src/api/data.js
  18. 14 0
      src/api/gaode.js
  19. 17 0
      src/api/home.js
  20. 107 0
      src/api/other.js
  21. 11 0
      src/api/routers.js
  22. 265 0
      src/api/user.js
  23. 4 0
      src/assets/icons/iconfont.css
  24. BIN
      src/assets/icons/iconfont.eot
  25. 56 0
      src/assets/icons/iconfont.svg
  26. BIN
      src/assets/icons/iconfont.ttf
  27. BIN
      src/assets/icons/iconfont.woff
  28. 0 0
      src/assets/images/error-page/error-401.svg
  29. 0 0
      src/assets/images/error-page/error-404.svg
  30. 0 0
      src/assets/images/error-page/error-500.svg
  31. BIN
      src/assets/images/icon-qr-qq-wechat.png
  32. 0 0
      src/assets/images/icon-social-bilibili.svg
  33. 1 0
      src/assets/images/icon-social-juejin.svg
  34. 0 0
      src/assets/images/icon-social-twitter.svg
  35. 1 0
      src/assets/images/icon-social-zhihu.svg
  36. BIN
      src/assets/images/login-bg.jpg
  37. BIN
      src/assets/images/logo-min.jpg
  38. BIN
      src/assets/images/logo-min_old.jpg
  39. BIN
      src/assets/images/logo.jpg
  40. BIN
      src/assets/images/logo_old.jpg
  41. BIN
      src/assets/images/talkingdata.png
  42. 63 0
      src/components/charts/bar.vue
  43. 3 0
      src/components/charts/index.js
  44. 70 0
      src/components/charts/pie.vue
  45. 491 0
      src/components/charts/theme.json
  46. 42 0
      src/components/common-icon/common-icon.vue
  47. 2 0
      src/components/common-icon/index.js
  48. 8 0
      src/components/common/common.less
  49. 3 0
      src/components/common/util.js
  50. 174 0
      src/components/count-to/count-to.vue
  51. 2 0
      src/components/count-to/index.js
  52. 10 0
      src/components/count-to/index.less
  53. 2 0
      src/components/cropper/index.js
  54. 35 0
      src/components/cropper/index.less
  55. 139 0
      src/components/cropper/index.vue
  56. 18 0
      src/components/drag-drawer/drag-drawer-trigger.vue
  57. 156 0
      src/components/drag-drawer/drag-drawer.vue
  58. 2 0
      src/components/drag-drawer/index.js
  59. 70 0
      src/components/drag-drawer/index.less
  60. 7 0
      src/components/drag-drawer/mixin.js
  61. 92 0
      src/components/drag-list/drag-list.vue
  62. 2 0
      src/components/drag-list/index.js
  63. 75 0
      src/components/editor/editor.vue
  64. 2 0
      src/components/editor/index.js
  65. 35 0
      src/components/icons/icons.vue
  66. 2 0
      src/components/icons/index.js
  67. 2 0
      src/components/info-card/index.js
  68. 94 0
      src/components/info-card/infor-card.vue
  69. 2 0
      src/components/login-form/index.js
  70. 73 0
      src/components/login-form/login-form.vue
  71. 2 0
      src/components/main/components/a-back-top/index.js
  72. 90 0
      src/components/main/components/a-back-top/index.vue
  73. 49 0
      src/components/main/components/error-store/error-store.vue
  74. 2 0
      src/components/main/components/error-store/index.js
  75. 84 0
      src/components/main/components/fullscreen/fullscreen.vue
  76. 2 0
      src/components/main/components/fullscreen/index.js
  77. 4 0
      src/components/main/components/header-bar/custom-bread-crumb/custom-bread-crumb.less
  78. 46 0
      src/components/main/components/header-bar/custom-bread-crumb/custom-bread-crumb.vue
  79. 2 0
      src/components/main/components/header-bar/custom-bread-crumb/index.js
  80. 14 0
      src/components/main/components/header-bar/header-bar.less
  81. 34 0
      src/components/main/components/header-bar/header-bar.vue
  82. 2 0
      src/components/main/components/header-bar/index.js
  83. 2 0
      src/components/main/components/header-bar/sider-trigger/index.js
  84. 21 0
      src/components/main/components/header-bar/sider-trigger/sider-trigger.less
  85. 27 0
      src/components/main/components/header-bar/sider-trigger/sider-trigger.vue
  86. 2 0
      src/components/main/components/language/index.js
  87. 51 0
      src/components/main/components/language/language.vue
  88. 51 0
      src/components/main/components/side-menu/collapsed-menu.vue
  89. 2 0
      src/components/main/components/side-menu/index.js
  90. 21 0
      src/components/main/components/side-menu/item-mixin.js
  91. 18 0
      src/components/main/components/side-menu/mixin.js
  92. 26 0
      src/components/main/components/side-menu/side-menu-item.vue
  93. 40 0
      src/components/main/components/side-menu/side-menu.less
  94. 114 0
      src/components/main/components/side-menu/side-menu.vue
  95. 2 0
      src/components/main/components/tags-nav/index.js
  96. 87 0
      src/components/main/components/tags-nav/tags-nav.less
  97. 209 0
      src/components/main/components/tags-nav/tags-nav.vue
  98. 2 0
      src/components/main/components/user/index.js
  99. 12 0
      src/components/main/components/user/user.less
  100. 63 0
      src/components/main/components/user/user.vue

+ 5 - 0
.babelrc

@@ -0,0 +1,5 @@
+{
+  "presets": [
+    "@vue/app"
+  ]
+}

+ 9 - 0
.editorconfig

@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true

+ 0 - 0
.eslintignore


+ 21 - 0
.eslintrc.js

@@ -0,0 +1,21 @@
+module.exports = {
+  root: true,
+  'extends': [
+    'plugin:vue/essential',
+    '@vue/standard'
+  ],
+  rules: {
+    // allow async-await
+    'generator-star-spacing': 'off',
+    // allow debugger during development
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+    'vue/no-parsing-error': [2, {
+      'x-invalid-end-tag': false
+    }],
+    'no-undef': 'off',
+    'camelcase': 'off'
+  },
+  parserOptions: {
+    parser: 'babel-eslint'
+  }
+}

+ 29 - 0
.gitignore

@@ -0,0 +1,29 @@
+.DS_Store
+node_modules
+/dist
+
+package-lock.json
+
+/tests/e2e/videos/
+/tests/e2e/screenshots/
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw*
+
+build/env.js
+src/view_unuse

+ 8 - 0
.htaccess

@@ -0,0 +1,8 @@
+<IfModule mod_rewrite.c>
+  RewriteEngine On
+  RewriteBase /
+  RewriteRule ^index\.html$ - [L]
+  RewriteCond %{REQUEST_FILENAME} !-f
+  RewriteCond %{REQUEST_FILENAME} !-d
+  RewriteRule . /index.html [L]
+</IfModule>

+ 8 - 0
.htaccess_1

@@ -0,0 +1,8 @@
+<IfModule mod_rewrite.c>
+  RewriteEngine On
+  RewriteBase /
+  RewriteRule ^index\.html$ - [L]
+  RewriteCond %{REQUEST_FILENAME} !-f
+  RewriteCond %{REQUEST_FILENAME} !-d
+  RewriteRule . /index.html [L]
+</IfModule>

+ 5 - 0
.postcssrc.js

@@ -0,0 +1,5 @@
+module.exports = {
+  plugins: {
+    autoprefixer: {}
+  }
+}

+ 5 - 0
.travis.yml

@@ -0,0 +1,5 @@
+language: node_js
+node_js: stable
+script: npm run lint
+notifications:
+  email: false

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 iView
+
+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.

+ 115 - 0
README.md

@@ -0,0 +1,115 @@
+<p align="center">
+    <a href="https://www.iviewui.com">
+        <img width="200" src="https://file.iviewui.com/logo-new.svg">
+    </a>
+</p>
+
+<h1>
+iView Admin
+    <h3>Vue.js 2.0 admin management system template based on iView.</h3>
+</h1>
+
+[![](https://img.shields.io/github/release/iview/iview-admin.svg)](https://github.com/iview/iview-admin/releases)
+[![](https://img.shields.io/travis/iview/iview-admin.svg?style=flat-square)](https://travis-ci.org/iview/iview-admin)
+[![vue](https://img.shields.io/badge/vue-2.5.17-brightgreen.svg?style=flat-square)](https://github.com/vuejs/vue)
+[![iview ui](https://img.shields.io/badge/iview-3.2.2-brightgreen.svg?style=flat-square)](https://github.com/iview/iview)
+[![npm](https://img.shields.io/npm/l/express.svg)]()
+
+<h2 align="center">Special Sponsors</h2>
+<table>
+      <tbody>
+        <tr>
+          <td align="center" valign="middle">
+            <a href="https://segmentfault.com/ls/1650000016424063" target="_blank">
+              <img width="300" src="https://file.iviewui.com/asd/asd-i-2.png">
+            </a>
+          </td>
+          <td align="center" valign="middle">
+            <a href="https://e.coding.net/?utm_source=iview" target="_blank">
+              <img width="300" src="https://file.iviewui.com/asd/asd-coding4.png">
+            </a>
+          </td>
+            <td align="center" valign="middle">
+            <a href="https://cn.udacity.com/fend/?utm_source=iviewui&utm_medium=banner&utm_campaign=fend" target="_blank">
+              <img width="300" src="https://file.iviewui.com/asd/asd-u-new-2.png">
+            </a>
+          </td>
+        </tr>
+      </tbody>
+</table>
+
+> If you'd like be a sponsor, to show your ads in GitHub and iView doc, please email admin@aresn.com to get more infomation.
+
+## Introduction
+
+iView Admin is a front-end management background integration solution. It based on [Vue.js](https://github.com/vuejs/vue) and use the UI Toolkit [iView](https://github.com/iview/iview).
+
+- [Document](https://lison16.github.io/iview-admin-doc/)
+- [Preview](https://admin.iviewui.com/)
+- [Base template recommends using](https://github.com/iview/iview-admin/tree/template)
+
+![image](https://file.iviewui.com/admin-dist/admin-preview.png)
+
+## Features
+
+- Login / Logout
+- Permission Authentication
+    - A list of filters
+    - Permission to switch
+- i18n
+- Components
+    - Rich Text Editor
+    - Markdown Editor
+    - City Cascader
+    - Photos preview and edit
+    - Draggable list
+    - File upload
+    - Digital gradient
+    - split-pane
+- Form
+    - The article published
+    - Workflow
+- Table
+    - Drag-and-drop sort
+    - Searchable form
+    - Table export data
+        - Export to Csv file
+        - Export to Xls file
+    - Table to picture
+- Error Page
+    - 403
+    - 404
+    - 500
+- Router
+    - Dynamic routing
+    - With reference page
+- Theme
+- Shrink the sidebar
+- Tag navigation
+- Breadcrumb navigation
+- Full screen / exit full screen
+- Lock screen
+- The message center
+- Personal center
+
+## Getting started
+```bush
+# clone the project
+git clone https://github.com/iview/iview-admin.git
+
+// install dependencies
+npm install
+
+// develop
+npm run dev
+```
+
+## Build
+```bush
+npm run build
+```
+
+## License
+[MIT](http://opensource.org/licenses/MIT)
+
+Copyright (c) 2016-present, iView

+ 3 - 0
cypress.json

@@ -0,0 +1,3 @@
+{
+  "pluginsFile": "tests/e2e/plugins/index.js"
+}

+ 78 - 0
package.json

@@ -0,0 +1,78 @@
+{
+  "name": "iview-admin",
+  "version": "2.0.0",
+  "author": "Lison<lison16new@163.com>",
+  "private": false,
+  "scripts": {
+    "dev": "vue-cli-service serve --open",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint",
+    "test:unit": "vue-cli-service test:unit",
+    "test:e2e": "vue-cli-service test:e2e"
+  },
+  "dependencies": {
+    "axios": "^0.18.0",
+    "clipboard": "^2.0.0",
+    "codemirror": "^5.38.0",
+    "core-baidu-map": "^1.0.0",
+    "countup": "^1.8.2",
+    "cropperjs": "^1.2.2",
+    "dayjs": "^1.7.7",
+    "echarts": "^4.0.4",
+    "html2canvas": "^1.0.0-alpha.12",
+    "iview": "^3.2.2",
+    "iview-area": "^1.5.17",
+    "jquery": "^3.6.0",
+    "js-cookie": "^2.2.0",
+    "jsonp": "^0.2.1",
+    "quill": "^1.3.7",
+    "simplemde": "^1.11.2",
+    "sortablejs": "^1.7.0",
+    "tree-table-vue": "^1.1.0",
+    "v-org-tree": "^1.0.6",
+    "v-viewer": "^1.6.4",
+    "vue": "^2.5.10",
+    "vue-baidu-map": "^0.21.22",
+    "vue-i18n": "^7.8.0",
+    "vue-jsonp": "^2.0.0",
+    "vue-router": "^3.0.1",
+    "vue2-editor": "^2.10.3",
+    "vuedraggable": "^2.16.0",
+    "vuex": "^3.0.1",
+    "wangeditor": "^3.1.1",
+    "xlsx": "^0.13.3"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "^3.0.1",
+    "@vue/cli-plugin-eslint": "^3.0.1",
+    "@vue/cli-plugin-unit-mocha": "^3.0.1",
+    "@vue/cli-service": "^3.0.1",
+    "@vue/eslint-config-standard": "^3.0.0-beta.10",
+    "@vue/test-utils": "^1.0.0-beta.10",
+    "chai": "^4.1.2",
+    "eslint-plugin-cypress": "^2.0.1",
+    "less": "^2.7.3",
+    "less-loader": "^4.0.5",
+    "lint-staged": "^6.0.0",
+    "mockjs": "^1.0.1-beta3",
+    "vue-template-compiler": "^2.5.13"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 8"
+  ],
+  "gitHooks": {
+    "pre-commit": "lint-staged"
+  },
+  "lint-staged": {
+    "*.js": [
+      "vue-cli-service lint",
+      "git add"
+    ],
+    "*.vue": [
+      "vue-cli-service lint",
+      "git add"
+    ]
+  }
+}

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">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title></title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but iview-admin 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>

+ 27 - 0
src/App.vue

@@ -0,0 +1,27 @@
+<template>
+  <div id="app">
+    <router-view/>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'App'
+}
+</script>
+
+<style lang="less">
+.size{
+  width: 100%;
+  height: 100%;
+}
+html,body{
+  .size;
+  overflow: hidden;
+  margin: 0;
+  padding: 0;
+}
+#app {
+  .size;
+}
+</style>

+ 166 - 0
src/api/data.js

@@ -0,0 +1,166 @@
+import axios from '@/libs/api.request'
+
+export const getTableData = () => {
+  return axios.request({
+    url: 'get_table_data',
+    method: 'get'
+  })
+}
+
+export const getDragList = () => {
+  return axios.request({
+    url: 'get_drag_list',
+    method: 'get'
+  })
+}
+
+export const errorReq = () => {
+  return axios.request({
+    url: 'error_url',
+    method: 'post'
+  })
+}
+
+export const saveErrorLogger = info => {
+  return axios.request({
+    url: 'save_error_logger',
+    data: info,
+    method: 'post'
+  })
+}
+
+export const uploadImg = formData => {
+  return axios.request({
+    url: 'image/upload',
+    data: formData
+  })
+}
+
+export const getOrgData = () => {
+  return axios.request({
+    url: 'get_org_data',
+    method: 'get'
+  })
+}
+
+export const getTreeSelectData = () => {
+  return axios.request({
+    url: 'get_tree_select_data',
+    method: 'get'
+  })
+}
+
+// new 20221205
+
+// 判断获取配送中心列表
+export const getcenterlist = searchData => {
+  return axios.request({
+    url: '/admin/meal/getcenterlist',
+    method: 'post',
+    data: searchData
+  })
+}
+// 根据id获取配送中心信息
+export const getmealcenterinfobyid = ({ id }) => {
+  return axios.request({
+    url: '/admin/meal/getmealcenterinfobyid',
+    method: 'post',
+    data: { 'cmid': id }
+  })
+}
+/**
+ * 获取套餐列表
+ * @param {*} param0
+ * @returns
+ */
+export const getmeallist = searchData => {
+  return axios.request({
+    url: '/admin/meal/getmeallist',
+    method: 'post',
+    data: searchData
+  })
+}
+/**
+ * 改套餐可用
+ * @param {*} param0
+ * @returns
+ */
+export const updatemealisactive = ({ id, is_active }) => {
+  return axios.request({
+    url: '/admin/meal/updatemealisactive',
+    method: 'post',
+    data: { id, is_active }
+  })
+}
+
+/**
+ * 改套餐可用
+ * @param {*} param0
+ * @returns
+ */
+export const editmeal = formData => {
+  return axios.request({
+    url: '/admin/meal/editmeal',
+    method: 'post',
+    data: formData
+  })
+}
+export const getmealinfo = ({ id }) => {
+  return axios.request({
+    url: '/admin/meal/getmealinfo',
+    method: 'post',
+    data: { id }
+  })
+}
+// 获取菜品列表
+export const getcooks = ({ center_id }) => {
+  return axios.request({
+    url: '/admin/meal/getcooks',
+    method: 'post',
+    data: { center_id }
+  })
+}
+// 保存套餐菜谱
+export const savemealcooks = ({ center_id, meal_id, week, items }) => {
+  return axios.request({
+    url: '/admin/meal/savemealcooks',
+    method: 'post',
+    data: { center_id, meal_id, week, items }
+  })
+}
+
+// 获取套餐菜谱列表
+export const getmealcooklist = searchData => {
+  return axios.request({
+    url: '/admin/meal/getmealcooklist',
+    method: 'post',
+    data: searchData
+  })
+}
+
+// 获取菜品列表
+export const getcookslist = searchData => {
+  return axios.request({
+    url: '/admin/meal/getcookslist',
+    method: 'post',
+    data: searchData
+  })
+}
+
+// 菜品修改
+export const editcookinfo = formData => {
+  return axios.request({
+    url: '/admin/meal/editcookinfo',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 菜品修改可用
+export const updatecookisactive = formData => {
+  return axios.request({
+    url: '/admin/meal/updatecookisactive',
+    method: 'post',
+    data: formData
+  })
+}

+ 14 - 0
src/api/gaode.js

@@ -0,0 +1,14 @@
+var key = 'b7e81de97ba4810541998fc029436f4c'
+export const gao = ($address) => {
+  var url = 'https://restapi.amap.com/v3/geocode/regeo?key=' + key + '&address=' + $address
+  let result = axios.request({
+    url: url,
+    method: 'get'
+  })
+  if (result.status === 1) {
+    let location = result.geocodes.location
+    let locationArr = location.split(',')
+    result.locationArr = locationArr
+  }
+  return result
+}

+ 17 - 0
src/api/home.js

@@ -0,0 +1,17 @@
+import axios from '@/libs/api.request'
+
+// 获取总数
+export const getcountlist = () => {
+  return axios.request({
+    url: '/index/homepage/getcount',
+    method: 'post'
+  })
+}
+
+// 获取分析数据
+export const getstatistics = () => {
+  return axios.request({
+    url: '/index/homepage/getstatistics',
+    method: 'post'
+  })
+}

+ 107 - 0
src/api/other.js

@@ -0,0 +1,107 @@
+import axios from '@/libs/api.request'
+// import config from '@/config'
+
+// const baseUrl_lab = process.env.NODE_ENV === 'development' ? config.baseUrl.dev_lab : config.baseUrl.pro_lab
+
+// 统计数据
+// 获取电话统计数据
+export const getTelnoConnectNum = () => {
+  return axios.request({
+    url: '/index/homepage/getTelnoConnectNum',
+    method: 'post'
+  })
+}
+// 获取无代发数据
+export const getinventcount1 = () => {
+  return axios.request({
+    url: '/index/homepage/getinventcount1',
+    method: 'post'
+  })
+}
+
+// 已招满数据
+export const getinventfull = () => {
+  return axios.request({
+    url: '/index/homepage/getinventfull',
+    method: 'post'
+  })
+}
+
+// 未接电话
+export const getinventnotanswer = () => {
+  return axios.request({
+    url: '/index/homepage/getinventnotanswer',
+    method: 'post'
+  })
+}
+
+// 各招工电话联系 联系数量 列表
+export const getinventitemtelnoconnectlist = ({ page, size }) => {
+  return axios.request({
+    url: '/index/homepage/getinventitemtelnoconnectlist',
+    method: 'post',
+    data: {
+      page,
+      size
+    }
+  })
+}
+// 各招工电话联系 联系数量 列表 今日
+export const getinventitemtelnoconnectbydaylist = ({ page, size }) => {
+  return axios.request({
+    url: '/index/homepage/getinventitemtelnoconnectbydaylist',
+    method: 'post',
+    data: {
+      page,
+      size
+    }
+  })
+}
+
+// 统计近昨日日招工联系列表
+export const getinventcount2 = () => {
+  return axios.request({
+    url: '/index/Homepage/getinventcount2',
+    method: 'post'
+  })
+}
+
+// 各招工电话联系 间隔时间 列表
+export const getinventitemtelnointervallist = ({ page, size }) => {
+  return axios.request({
+    url: '/index/homepage/getinventitemtelnointervallist',
+    method: 'post',
+    data: {
+      page,
+      size
+    }
+  })
+}
+// 各招工电话联系 间隔时间 列表 平均值
+export const getinventitemtelnoavgbydaylist = () => {
+  return axios.request({
+    url: '/index/homepage/getinventitemtelnoavgbydaylist',
+    method: 'post'
+  })
+}
+
+// 统计用户积分
+export const getuseraccount = ({ page, size }) => {
+  return axios.request({
+    url: '/index/homepage/getuseraccount',
+    method: 'post',
+    data: {
+      page,
+      size
+    }
+  })
+}
+
+// 获取订单列表
+export const getorderlistbywhere = searchData => {
+  return axios.request({
+    url: '/index/homepage/getorderlistbywhere',
+    method: 'post',
+    data: searchData
+  })
+}

+ 11 - 0
src/api/routers.js

@@ -0,0 +1,11 @@
+import axios from '@/libs/api.request'
+
+export const getRouterReq = (access) => {
+  return axios.request({
+    url: 'get_router',
+    params: {
+      access
+    },
+    method: 'get'
+  })
+}

+ 265 - 0
src/api/user.js

@@ -0,0 +1,265 @@
+import axios from '@/libs/api.request'
+// import config from '@/config'
+
+// const baseUrl_lab = process.env.NODE_ENV === 'development' ? config.baseUrl.dev_lab : config.baseUrl.pro_lab
+// const baseUrl_wc = process.env.NODE_ENV === 'development' ? config.baseUrl.dev : config.baseUrl.pro
+
+export const login = ({ userName, password }) => {
+  const data = {
+    userName,
+    password
+  }
+  return axios.request({
+    url: '/admin/weblogin/login',
+    data,
+    method: 'post'
+  })
+}
+
+export const getUserInfo = (token) => {
+  return axios.request({
+    url: '/admin/weblogin/queryinfobytoken',
+    params: {
+      token
+    },
+    method: 'get'
+  })
+}
+export const getUserInfobyid = (id) => {
+  return axios.request({
+    url: '/admin/Userinfo/queryinfobyidforadmin',
+    params: {
+      id
+    },
+    method: 'post'
+  })
+}
+export const searchUserInfo = (id) => {
+  return axios.request({
+    url: '/admin/webuser/searchUserInfo',
+    params: {
+      id
+    },
+    method: 'post'
+  })
+}
+
+export const logout = (token) => {
+  return axios.request({
+    url: '/admin/weblogin/loginout',
+    method: 'post',
+    params: {
+      token
+    }
+  })
+}
+
+export const getUnreadCount = () => {
+  return axios.request({
+    url: 'message/count',
+    method: 'get'
+  })
+}
+
+export const getMessage = () => {
+  return axios.request({
+    url: 'message/init',
+    method: 'get'
+  })
+}
+
+export const getContentByMsgId = msg_id => {
+  return axios.request({
+    url: 'message/content',
+    method: 'get',
+    params: {
+      msg_id
+    }
+  })
+}
+
+export const hasRead = msg_id => {
+  return axios.request({
+    url: 'message/has_read',
+    method: 'post',
+    data: {
+      msg_id
+    }
+  })
+}
+
+export const removeReaded = msg_id => {
+  return axios.request({
+    url: 'message/remove_readed',
+    method: 'post',
+    data: {
+      msg_id
+    }
+  })
+}
+
+export const restoreTrash = msg_id => {
+  return axios.request({
+    url: 'message/restore',
+    method: 'post',
+    data: {
+      msg_id
+    }
+  })
+}
+// webuser
+
+// 添加编辑
+export const editwebuser = formdata => {
+  return axios.request({
+    url: '/admin/webuser/editwebuser',
+    method: 'post',
+    data: formdata
+  })
+}
+
+// 列表
+export const listwebuser = searchdata => {
+  return axios.request({
+    url: '/admin/webuser/getalllist',
+    method: 'post',
+    data: searchdata
+  })
+}
+
+// 用户可用状态修改
+export const updatewebuseractive = ({ id, isactive }) => {
+  const data = {
+    id,
+    isactive
+  }
+  return axios.request({
+    url: '/admin/webuser/updateisactive',
+    method: 'post',
+    data: data
+  })
+}
+
+// 改用户密码
+export const changewebuserpasswd = formdata => {
+  return axios.request({
+    url: '/admin/webuser/updatepasswd',
+    method: 'post',
+    data: formdata
+  })
+}
+
+// 获取用户组列表 用于用户组列表
+export const getwebusergrouplist = () => {
+  return axios.request({
+    url: '/admin/role/getlisttree',
+    method: 'post'
+  })
+}
+
+// 获取用户组列表 用于用户设置
+// is_active =1
+export const getwebusergrouplist2 = () => {
+  return axios.request({
+    url: '/admin/role/getlisttree',
+    method: 'post',
+    data: { is_active: 1 }
+  })
+}
+
+// 获取顶层用户组 用于用户组设置
+export const getgroupname = () => {
+  return axios.request({
+    url: '/admin/role/getlistgroupname',
+    method: 'post'
+  })
+}
+
+// 保存用户组
+export const saverole = (formdata) => {
+  return axios.request({
+    url: '/admin/role/newinfo',
+    method: 'post',
+    data: formdata
+  })
+}
+
+// 修改用户组
+export const updaterole = (formdata) => {
+  return axios.request({
+    url: '/admin/role/updateinfo',
+    method: 'post',
+    data: formdata
+  })
+}
+
+// 保存用户组和用户关系
+export const saveuserrole = (formdata) => {
+  return axios.request({
+    url: '/admin/webuser/saveuserrole',
+    method: 'post',
+    data: formdata
+  })
+}
+
+// 根据条件获取会员列表
+export const getuserlist = (formdata) => {
+  return axios.request({
+    url: '/admin/userinfo/getuserlist',
+    method: 'post',
+    data: formdata
+  })
+}
+
+// 改isactive用户
+export const updateuserisright = formData => {
+  return axios.request({
+    url: '/admin/userinfo/updateuserisright',
+    method: 'post',
+    data: formData
+  })
+}
+
+// 获取后台用户行为日志
+export const getlogs = searchData => {
+  return axios.request({
+    url: '/admin/webuser/getlogs',
+    method: 'post',
+    data: searchData
+  })
+}
+// 根据电话号码获取用户信息
+export const getinfobytelno = ({ telno }) => {
+  return axios.request({
+    url: '/admin/userinfo/getinfobytelnoforadmin',
+    method: 'post',
+    data: { telno }
+  })
+}
+
+// 判断用户是否有名片信息
+export const haswcinfobyuid = ({ uid }) => {
+  return axios.request({
+    url: '/admin/workercard/hasdatabyuidforadmin',
+    method: 'post',
+    data: { uid }
+  })
+}
+
+// 获取名片详情
+export const getwcinfobyid = ({ id }) => {
+  return axios.request({
+    url: '/admin/workercard/getinfobyidforadmin',
+    method: 'post',
+    data: { id }
+  })
+}
+
+// 获取图片信息
+export const getphotosbyid = ({ id }) => {
+  return axios.request({
+    url: '/admin/workercard/getphotosbyidforadmin',
+    method: 'post',
+    data: { id }
+  })
+}

Fichier diff supprimé car celui-ci est trop grand
+ 4 - 0
src/assets/icons/iconfont.css


BIN
src/assets/icons/iconfont.eot


+ 56 - 0
src/assets/icons/iconfont.svg

@@ -0,0 +1,56 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<!--
+2013-9-30: Created.
+-->
+<svg>
+<metadata>
+Created by iconfont
+</metadata>
+<defs>
+
+<font id="iconfont" horiz-adv-x="1024" >
+  <font-face
+    font-family="iconfont"
+    font-weight="500"
+    font-stretch="normal"
+    units-per-em="1024"
+    ascent="896"
+    descent="-128"
+  />
+    <missing-glyph />
+    
+    <glyph glyph-name="bear" unicode="&#58880;" d="M1024 683.008q0-70.656-46.08-121.856 46.08-89.088 46.08-193.536 0-96.256-39.936-181.248t-109.568-147.968-162.816-99.328-199.68-36.352-199.68 36.352-162.304 99.328-109.568 147.968-40.448 181.248q0 104.448 46.08 193.536-46.08 51.2-46.08 121.856 0 37.888 13.824 71.168t37.376 58.368 55.808 39.424 68.096 14.336q43.008 0 78.848-18.432t59.392-50.176q46.08 17.408 96.256 26.624t102.4 9.216 102.4-9.216 96.256-26.624q24.576 31.744 59.904 50.176t78.336 18.432q36.864 0 68.608-14.336t55.296-39.424 37.376-58.368 13.824-71.168zM205.824 268.288q10.24 0 18.944 10.24t15.36 28.672 10.24 42.496 3.584 51.712-3.584 51.712-10.24 41.984-15.36 28.16-18.944 10.24q-9.216 0-17.92-10.24t-15.36-28.16-10.752-41.984-4.096-51.712 4.096-51.712 10.752-42.496 15.36-28.672 17.92-10.24zM512-31.744000000000028q53.248 0 99.84 13.312t81.408 35.84 54.784 52.736 19.968 65.024q0 33.792-19.968 64t-54.784 52.736-81.408 35.84-99.84 13.312-99.84-13.312-81.408-35.84-54.784-52.736-19.968-64q0-34.816 19.968-65.024t54.784-52.736 81.408-35.84 99.84-13.312zM818.176 268.288q10.24 0 18.944 10.24t15.36 28.672 10.24 42.496 3.584 51.712-3.584 51.712-10.24 41.984-15.36 28.16-18.944 10.24q-9.216 0-17.92-10.24t-15.36-28.16-10.752-41.984-4.096-51.712 4.096-51.712 10.752-42.496 15.36-28.672 17.92-10.24zM512 235.51999999999998q39.936 0 68.096-9.728t28.16-24.064-28.16-24.064-68.096-9.728-68.096 9.728-28.16 24.064 28.16 24.064 68.096 9.728z"  horiz-adv-x="1024" />
+
+    
+    <glyph glyph-name="resize-vertical" unicode="&#59331;" d="M512 896C229.248 896 0 666.752 0 384s229.248-512 512-512 512 229.248 512 512S794.752 896 512 896zM576 192l64 0-128-128-128 128 64 0L448 576l-64 0 128 128 128-128-64 0L576 192z"  horiz-adv-x="1024" />
+
+    
+    <glyph glyph-name="chuizhifanzhuan" unicode="&#58977;" d="M286.01856 645.08416l472.4224 0 0-146.2784-472.4224 0 0 146.2784ZM87.19872 420.37248l885.80096 0 0-70.87104-885.80096 0 0 70.87104ZM773.55008 268.05248l0-31.0016L270.6688 237.05088l0 31.0016L773.55008 268.05248zM773.55008 121.4208l0-31.0016L270.6688 90.4192l0 31.0016L773.55008 121.4208zM742.54848 240.75776l31.0016 0 0-123.04896-31.0016 0L742.54848 240.75776zM270.70464 240.57856l31.0016 0 0-123.04896-31.0016 0L270.70464 240.57856z"  horiz-adv-x="1024" />
+
+    
+    <glyph glyph-name="shuipingfanzhuan" unicode="&#58978;" d="M252.76928 596.096l146.2784 0 0-472.42752-146.2784 0 0 472.42752ZM477.48096 810.65472l70.87104 0 0-885.80608-70.87104 0 0 885.80608ZM629.80096 611.2l31.0016 0 0-502.88128-31.0016 0L629.80096 611.2zM776.42752 611.2l31.0016 0 0-502.88128-31.0016 0L776.42752 611.2zM657.09056 580.1984l0 31.0016 123.04896 0 0-31.0016L657.09056 580.1984zM657.27488 108.35456l0 31.0016 123.04896 0 0-31.0016L657.27488 108.35456z"  horiz-adv-x="1024" />
+
+    
+    <glyph glyph-name="qq" unicode="&#58889;" d="M147.372058 491.394284c-5.28997-13.909921 2.431986-22.698872 0-75.732573-0.682996-14.25092-62.165649-78.762555-86.569511-145.791177-24.192863-66.517625-27.519845-135.978232 9.811944-163.285078 37.419789-27.305846 72.191593 90.879487 76.757567 73.685584 1.961989-7.509958 4.436975-15.317914 7.423958-23.338868a331.945126 331.945126 0 0 1 61.140655-101.162429c5.929967-6.783962-36.009797-19.199892-61.140655-61.99365-25.173858-42.751759 7.209959-120.49032 132.223254-120.49032 161.27909 0 197.288886 56.70368 200.574868 56.447681 12.031932-0.895995 12.841928 0 25.599855 0 15.572912 0 9.129948-1.279993 23.593867 0 7.807956 0.682996 86.186514-67.839617 194.686901-56.447681 184.873956 19.45589 156.586116 81.40754 142.079198 120.48932-15.103915 40.83277-68.692612 59.946662-66.303626 62.549647 44.28775 48.938724 51.285711 79.018554 66.346626 123.9463 6.143965 18.473896 49.066723-101.674426 82.089537-73.685584 13.781922 11.690934 41.301767 60.24566 13.781922 163.285078-27.519845 102.996419-80.767544 126.505286-79.615551 145.791177 2.389987 40.191773 1.023994 68.436614-1.023994 75.732573-9.812945 35.4128-30.378829 27.604844-30.378829 35.4128C858.450044 730.752933 705.10691 896 515.966978 896s-342.398067-165.289067-342.398068-369.192916c0-16.169909-14.378919-4.223976-26.154852-35.4128z"  horiz-adv-x="1024" />
+
+    
+    <glyph glyph-name="frown" unicode="&#59262;" d="M336 475m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM688 475m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM512 832C264.6 832 64 631.4 64 384s200.6-448 448-448 448 200.6 448 448S759.4 832 512 832z m263-711c-34.2-34.2-74-61-118.3-79.8C611 21.8 562.3 12 512 12c-50.3 0-99 9.8-144.8 29.2-44.3 18.7-84.1 45.6-118.3 79.8-34.2 34.2-61 74-79.8 118.3C149.8 285 140 333.7 140 384s9.8 99 29.2 144.8c18.7 44.3 45.6 84.1 79.8 118.3 34.2 34.2 74 61 118.3 79.8C413 746.2 461.7 756 512 756c50.3 0 99-9.8 144.8-29.2 44.3-18.7 84.1-45.6 118.3-79.8 34.2-34.2 61-74 79.8-118.3C874.2 483 884 434.3 884 384s-9.8-99-29.2-144.8c-18.7-44.3-45.6-84.1-79.8-118.2zM512 363c-85.5 0-155.6-67.3-160-151.6-0.2-4.6 3.4-8.4 8-8.4h48.1c4.2 0 7.8 3.2 8.1 7.4C420 259.9 461.5 299 512 299s92.1-39.1 95.8-88.6c0.3-4.2 3.9-7.4 8.1-7.4H664c4.6 0 8.2 3.8 8 8.4-4.4 84.3-74.5 151.6-160 151.6z"  horiz-adv-x="1024" />
+
+    
+    <glyph glyph-name="meh" unicode="&#59264;" d="M336 475m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM688 475m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM512 832C264.6 832 64 631.4 64 384s200.6-448 448-448 448 200.6 448 448S759.4 832 512 832z m263-711c-34.2-34.2-74-61-118.3-79.8C611 21.8 562.3 12 512 12c-50.3 0-99 9.8-144.8 29.2-44.3 18.7-84.1 45.6-118.3 79.8-34.2 34.2-61 74-79.8 118.3C149.8 285 140 333.7 140 384s9.8 99 29.2 144.8c18.7 44.3 45.6 84.1 79.8 118.3 34.2 34.2 74 61 118.3 79.8C413 746.2 461.7 756 512 756c50.3 0 99-9.8 144.8-29.2 44.3-18.7 84.1-45.6 118.3-79.8 34.2-34.2 61-74 79.8-118.3C874.2 483 884 434.3 884 384s-9.8-99-29.2-144.8c-18.7-44.3-45.6-84.1-79.8-118.2zM664 331H360c-4.4 0-8-3.6-8-8v-48c0-4.4 3.6-8 8-8h304c4.4 0 8 3.6 8 8v48c0 4.4-3.6 8-8 8z"  horiz-adv-x="1024" />
+
+    
+    <glyph glyph-name="smile" unicode="&#59267;" d="M336 475m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM688 475m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM512 832C264.6 832 64 631.4 64 384s200.6-448 448-448 448 200.6 448 448S759.4 832 512 832z m263-711c-34.2-34.2-74-61-118.3-79.8C611 21.8 562.3 12 512 12c-50.3 0-99 9.8-144.8 29.2-44.3 18.7-84.1 45.6-118.3 79.8-34.2 34.2-61 74-79.8 118.3C149.8 285 140 333.7 140 384s9.8 99 29.2 144.8c18.7 44.3 45.6 84.1 79.8 118.3 34.2 34.2 74 61 118.3 79.8C413 746.2 461.7 756 512 756c50.3 0 99-9.8 144.8-29.2 44.3-18.7 84.1-45.6 118.3-79.8 34.2-34.2 61-74 79.8-118.3C874.2 483 884 434.3 884 384s-9.8-99-29.2-144.8c-18.7-44.3-45.6-84.1-79.8-118.2zM664 363h-48.1c-4.2 0-7.8-3.2-8.1-7.4C604 306.1 562.5 267 512 267s-92.1 39.1-95.8 88.6c-0.3 4.2-3.9 7.4-8.1 7.4H360c-4.6 0-8.2-3.8-8-8.4 4.4-84.3 74.5-151.6 160-151.6s155.6 67.3 160 151.6c0.2 4.6-3.4 8.4-8 8.4z"  horiz-adv-x="1024" />
+
+    
+    <glyph glyph-name="man" unicode="&#59362;" d="M874 776H622c-3.3 0-6-2.7-6-6v-56c0-3.3 2.7-6 6-6h160.4L583.1 508.7c-50 38.5-111 59.3-175.1 59.3-76.9 0-149.3-30-203.6-84.4S120 356.9 120 280s30-149.3 84.4-203.6C258.7 22 331.1-8 408-8s149.3 30 203.6 84.4C666 130.7 696 203.1 696 280c0 64.1-20.8 124.9-59.2 174.9L836 654.1V494c0-3.3 2.7-6 6-6h56c3.3 0 6 2.7 6 6V746c0 16.5-13.5 30-30 30zM408 68c-116.9 0-212 95.1-212 212s95.1 212 212 212 212-95.1 212-212-95.1-212-212-212z"  horiz-adv-x="1024" />
+
+    
+    <glyph glyph-name="woman" unicode="&#59365;" d="M909.7 739.4l-42.2 42.2c-3.1 3.1-8.2 3.1-11.3 0L764 689.4l-84.2 84.2c-3.1 3.1-8.2 3.1-11.3 0l-42.1-42.1c-3.1-3.1-3.1-8.1 0-11.3l84.2-84.2-135.5-135.3c-50 38.5-111 59.3-175.1 59.3-76.9 0-149.3-30-203.6-84.4S112 348.9 112 272s30-149.3 84.4-203.6C250.7 14 323.1-16 400-16s149.3 30 203.6 84.4C658 122.7 688 195.1 688 272c0 64.2-20.9 125.1-59.3 175.1l135.4 135.4 84.2-84.2c3.1-3.1 8.2-3.1 11.3 0l42.1 42.1c3.1 3.1 3.1 8.1 0 11.3l-84.2 84.2 92.2 92.2c3.1 3.1 3.1 8.2 0 11.3zM400 60c-116.9 0-212 95.1-212 212s95.1 212 212 212 212-95.1 212-212-95.1-212-212-212z"  horiz-adv-x="1024" />
+
+    
+
+
+  </font>
+</defs></svg>

BIN
src/assets/icons/iconfont.ttf


BIN
src/assets/icons/iconfont.woff


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
src/assets/images/error-page/error-401.svg


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
src/assets/images/error-page/error-404.svg


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
src/assets/images/error-page/error-500.svg


BIN
src/assets/images/icon-qr-qq-wechat.png


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
src/assets/images/icon-social-bilibili.svg


+ 1 - 0
src/assets/images/icon-social-juejin.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1547543883733" class="icon" style="" viewBox="0 0 1272 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16831" xmlns:xlink="http://www.w3.org/1999/xlink" width="496.875" height="400"><defs><style type="text/css"></style></defs><path d="M729.64116345 165.27693991L634.32650881 90.125l-99.5625 78.52693991-5.17887981 4.16056009 104.74137981 83.50215546 105.09051682-83.50215546-9.77586218-7.53556009z m361.21228445 291.47198236l-456.78879245 360.19396555-456.49784537-359.99030128L110.125 511.12715547l523.93965546 413.11745671 524.23060335-413.35021555-67.44181091-54.14547436z m-456.78879245 29.21120673L385.4784479 290.00646554 318.06573237 344.12284454l315.96982771 249.16810336 316.28987101-249.40086136-67.41271555-54.14547436-248.84806008 196.21551682z" fill="#006cff" p-id="16832"></path></svg>

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
src/assets/images/icon-social-twitter.svg


+ 1 - 0
src/assets/images/icon-social-zhihu.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1547543863835" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16165" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="400"><defs><style type="text/css"></style></defs><path d="M512 1024C229.236364 1024 0 794.763636 0 512S229.236364 0 512 0s512 229.236364 512 512-229.236364 512-512 512z m-129.861818-756.48s-36.212364 2.094545-48.989091 24.482909c-12.8 22.365091-54.318545 137.378909-54.318546 137.378909s13.847273 6.376727 37.28291-10.658909c23.435636-17.035636 30.882909-46.848 30.882909-46.848l42.589091-2.117818 1.070545 121.390545s-73.495273-1.070545-88.413091 0c-14.894545 1.047273-23.412364 40.448-23.412364 40.448h111.825455s-9.588364 67.095273-38.353455 116.084364c-28.741818 48.989091-83.060364 87.319273-83.060363 87.319273s39.424 15.965091 77.730909-6.4c38.353455-22.341818 66.629818-120.692364 66.629818-120.692364l89.925818 110.056727s8.192-52.386909-1.466182-67.188363c-9.658182-14.778182-62.208-74.286545-62.208-74.286546l-22.946909 20.247273 16.337455-65.117091h97.954909s0-38.353455-19.153455-40.494545c-19.176727-2.094545-78.801455 0-78.801454 0V371.898182h88.389818s-1.070545-39.400727-18.106182-39.400727h-143.755636l22.341818-64.954182z m169.984 61.184v358.562909h36.002909l13.102545 45.009455 63.348364-45.009455h89.064727V328.704h-201.518545z" fill="#0f84fd" p-id="16166"></path><path d="M594.781091 368.64h117.899636v277.876364h-41.890909l-53.364363 40.261818-11.636364-40.261818h-11.008V368.64z" fill="#0f84fd" p-id="16167"></path></svg>

BIN
src/assets/images/login-bg.jpg


BIN
src/assets/images/logo-min.jpg


BIN
src/assets/images/logo-min_old.jpg


BIN
src/assets/images/logo.jpg


BIN
src/assets/images/logo_old.jpg


BIN
src/assets/images/talkingdata.png


+ 63 - 0
src/components/charts/bar.vue

@@ -0,0 +1,63 @@
+<template>
+  <div ref="dom" class="charts chart-bar"></div>
+</template>
+
+<script>
+import echarts from 'echarts'
+import tdTheme from './theme.json'
+import { on, off } from '@/libs/tools'
+echarts.registerTheme('tdTheme', tdTheme)
+export default {
+  name: 'ChartBar',
+  props: {
+    value: Object,
+    text: String,
+    subtext: String
+  },
+  data () {
+    return {
+      dom: null
+    }
+  },
+  methods: {
+    resize () {
+      this.dom.resize()
+    }
+  },
+  mounted () {
+    this.$nextTick(() => {
+      let xAxisData = Object.keys(this.value)
+      let seriesData = Object.values(this.value)
+      let option = {
+        title: {
+          text: this.text,
+          subtext: this.subtext,
+          x: 'center'
+        },
+        xAxis: {
+          type: 'category',
+          axisLabel: { interval: 0, rotate: 30 },
+          data: xAxisData
+        },
+        yAxis: {
+          type: 'value'
+        },
+        series: [{
+          data: seriesData,
+          type: 'bar',
+          label: {
+            show: true,
+            position: 'inside'
+          }
+        }]
+      }
+      this.dom = echarts.init(this.$refs.dom, 'tdTheme')
+      this.dom.setOption(option)
+      on(window, 'resize', this.resize)
+    })
+  },
+  beforeDestroy () {
+    off(window, 'resize', this.resize)
+  }
+}
+</script>

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

@@ -0,0 +1,3 @@
+import ChartPie from './pie.vue'
+import ChartBar from './bar.vue'
+export { ChartPie, ChartBar }

+ 70 - 0
src/components/charts/pie.vue

@@ -0,0 +1,70 @@
+<template>
+  <div ref="dom" class="charts chart-pie"></div>
+</template>
+
+<script>
+import echarts from 'echarts'
+import tdTheme from './theme.json'
+import { on, off } from '@/libs/tools'
+echarts.registerTheme('tdTheme', tdTheme)
+export default {
+  name: 'ChartPie',
+  props: {
+    value: Array,
+    text: String,
+    subtext: String
+  },
+  data () {
+    return {
+      dom: null
+    }
+  },
+  methods: {
+    resize () {
+      this.dom.resize()
+    }
+  },
+  mounted () {
+    this.$nextTick(() => {
+      let legend = this.value.map(_ => _.name)
+      let option = {
+        title: {
+          text: this.text,
+          subtext: this.subtext,
+          x: 'center'
+        },
+        tooltip: {
+          trigger: 'item',
+          formatter: '{a} <br/>{b} : {c} ({d}%)'
+        },
+        legend: {
+          orient: 'vertical',
+          left: 'left',
+          data: legend
+        },
+        series: [
+          {
+            type: 'pie',
+            radius: '55%',
+            center: ['50%', '60%'],
+            data: this.value,
+            itemStyle: {
+              emphasis: {
+                shadowBlur: 10,
+                shadowOffsetX: 0,
+                shadowColor: 'rgba(0, 0, 0, 0.5)'
+              }
+            }
+          }
+        ]
+      }
+      this.dom = echarts.init(this.$refs.dom, 'tdTheme')
+      this.dom.setOption(option)
+      on(window, 'resize', this.resize)
+    })
+  },
+  beforeDestroy () {
+    off(window, 'resize', this.resize)
+  }
+}
+</script>

+ 491 - 0
src/components/charts/theme.json

@@ -0,0 +1,491 @@
+
+{
+    "color": [
+        "#2d8cf0",
+        "#19be6b",
+        "#ff9900",
+        "#E46CBB",
+        "#9A66E4",
+        "#ed3f14"
+    ],
+    "backgroundColor": "rgba(0,0,0,0)",
+    "textStyle": {},
+    "title": {
+        "textStyle": {
+            "color": "#516b91"
+        },
+        "subtextStyle": {
+            "color": "#93b7e3"
+        }
+    },
+    "line": {
+        "itemStyle": {
+            "normal": {
+                "borderWidth": "2"
+            }
+        },
+        "lineStyle": {
+            "normal": {
+                "width": "2"
+            }
+        },
+        "symbolSize": "6",
+        "symbol": "emptyCircle",
+        "smooth": true
+    },
+    "radar": {
+        "itemStyle": {
+            "normal": {
+                "borderWidth": "2"
+            }
+        },
+        "lineStyle": {
+            "normal": {
+                "width": "2"
+            }
+        },
+        "symbolSize": "6",
+        "symbol": "emptyCircle",
+        "smooth": true
+    },
+    "bar": {
+        "itemStyle": {
+            "normal": {
+                "barBorderWidth": 0,
+                "barBorderColor": "#ccc"
+            },
+            "emphasis": {
+                "barBorderWidth": 0,
+                "barBorderColor": "#ccc"
+            }
+        }
+    },
+    "pie": {
+        "itemStyle": {
+            "normal": {
+                "borderWidth": 0,
+                "borderColor": "#ccc"
+            },
+            "emphasis": {
+                "borderWidth": 0,
+                "borderColor": "#ccc"
+            }
+        }
+    },
+    "scatter": {
+        "itemStyle": {
+            "normal": {
+                "borderWidth": 0,
+                "borderColor": "#ccc"
+            },
+            "emphasis": {
+                "borderWidth": 0,
+                "borderColor": "#ccc"
+            }
+        }
+    },
+    "boxplot": {
+        "itemStyle": {
+            "normal": {
+                "borderWidth": 0,
+                "borderColor": "#ccc"
+            },
+            "emphasis": {
+                "borderWidth": 0,
+                "borderColor": "#ccc"
+            }
+        }
+    },
+    "parallel": {
+        "itemStyle": {
+            "normal": {
+                "borderWidth": 0,
+                "borderColor": "#ccc"
+            },
+            "emphasis": {
+                "borderWidth": 0,
+                "borderColor": "#ccc"
+            }
+        }
+    },
+    "sankey": {
+        "itemStyle": {
+            "normal": {
+                "borderWidth": 0,
+                "borderColor": "#ccc"
+            },
+            "emphasis": {
+                "borderWidth": 0,
+                "borderColor": "#ccc"
+            }
+        }
+    },
+    "funnel": {
+        "itemStyle": {
+            "normal": {
+                "borderWidth": 0,
+                "borderColor": "#ccc"
+            },
+            "emphasis": {
+                "borderWidth": 0,
+                "borderColor": "#ccc"
+            }
+        }
+    },
+    "gauge": {
+        "itemStyle": {
+            "normal": {
+                "borderWidth": 0,
+                "borderColor": "#ccc"
+            },
+            "emphasis": {
+                "borderWidth": 0,
+                "borderColor": "#ccc"
+            }
+        }
+    },
+    "candlestick": {
+        "itemStyle": {
+            "normal": {
+                "color": "#edafda",
+                "color0": "transparent",
+                "borderColor": "#d680bc",
+                "borderColor0": "#8fd3e8",
+                "borderWidth": "2"
+            }
+        }
+    },
+    "graph": {
+        "itemStyle": {
+            "normal": {
+                "borderWidth": 0,
+                "borderColor": "#ccc"
+            }
+        },
+        "lineStyle": {
+            "normal": {
+                "width": 1,
+                "color": "#aaa"
+            }
+        },
+        "symbolSize": "6",
+        "symbol": "emptyCircle",
+        "smooth": true,
+        "color": [
+            "#2d8cf0",
+            "#19be6b",
+            "#f5ae4a",
+            "#9189d5",
+            "#56cae2",
+            "#cbb0e3"
+        ],
+        "label": {
+            "normal": {
+                "textStyle": {
+                    "color": "#eee"
+                }
+            }
+        }
+    },
+    "map": {
+        "itemStyle": {
+            "normal": {
+                "areaColor": "#f3f3f3",
+                "borderColor": "#516b91",
+                "borderWidth": 0.5
+            },
+            "emphasis": {
+                "areaColor": "rgba(165,231,240,1)",
+                "borderColor": "#516b91",
+                "borderWidth": 1
+            }
+        },
+        "label": {
+            "normal": {
+                "textStyle": {
+                    "color": "#000"
+                }
+            },
+            "emphasis": {
+                "textStyle": {
+                    "color": "rgb(81,107,145)"
+                }
+            }
+        }
+    },
+    "geo": {
+        "itemStyle": {
+            "normal": {
+                "areaColor": "#f3f3f3",
+                "borderColor": "#516b91",
+                "borderWidth": 0.5
+            },
+            "emphasis": {
+                "areaColor": "rgba(165,231,240,1)",
+                "borderColor": "#516b91",
+                "borderWidth": 1
+            }
+        },
+        "label": {
+            "normal": {
+                "textStyle": {
+                    "color": "#000"
+                }
+            },
+            "emphasis": {
+                "textStyle": {
+                    "color": "rgb(81,107,145)"
+                }
+            }
+        }
+    },
+    "categoryAxis": {
+        "axisLine": {
+            "show": true,
+            "lineStyle": {
+                "color": "#cccccc"
+            }
+        },
+        "axisTick": {
+            "show": false,
+            "lineStyle": {
+                "color": "#333"
+            }
+        },
+        "axisLabel": {
+            "show": true,
+            "textStyle": {
+                "color": "#999999"
+            }
+        },
+        "splitLine": {
+            "show": true,
+            "lineStyle": {
+                "color": [
+                    "#eeeeee"
+                ]
+            }
+        },
+        "splitArea": {
+            "show": false,
+            "areaStyle": {
+                "color": [
+                    "rgba(250,250,250,0.05)",
+                    "rgba(200,200,200,0.02)"
+                ]
+            }
+        }
+    },
+    "valueAxis": {
+        "axisLine": {
+            "show": true,
+            "lineStyle": {
+                "color": "#cccccc"
+            }
+        },
+        "axisTick": {
+            "show": false,
+            "lineStyle": {
+                "color": "#333"
+            }
+        },
+        "axisLabel": {
+            "show": true,
+            "textStyle": {
+                "color": "#999999"
+            }
+        },
+        "splitLine": {
+            "show": true,
+            "lineStyle": {
+                "color": [
+                    "#eeeeee"
+                ]
+            }
+        },
+        "splitArea": {
+            "show": false,
+            "areaStyle": {
+                "color": [
+                    "rgba(250,250,250,0.05)",
+                    "rgba(200,200,200,0.02)"
+                ]
+            }
+        }
+    },
+    "logAxis": {
+        "axisLine": {
+            "show": true,
+            "lineStyle": {
+                "color": "#cccccc"
+            }
+        },
+        "axisTick": {
+            "show": false,
+            "lineStyle": {
+                "color": "#333"
+            }
+        },
+        "axisLabel": {
+            "show": true,
+            "textStyle": {
+                "color": "#999999"
+            }
+        },
+        "splitLine": {
+            "show": true,
+            "lineStyle": {
+                "color": [
+                    "#eeeeee"
+                ]
+            }
+        },
+        "splitArea": {
+            "show": false,
+            "areaStyle": {
+                "color": [
+                    "rgba(250,250,250,0.05)",
+                    "rgba(200,200,200,0.02)"
+                ]
+            }
+        }
+    },
+    "timeAxis": {
+        "axisLine": {
+            "show": true,
+            "lineStyle": {
+                "color": "#cccccc"
+            }
+        },
+        "axisTick": {
+            "show": false,
+            "lineStyle": {
+                "color": "#333"
+            }
+        },
+        "axisLabel": {
+            "show": true,
+            "textStyle": {
+                "color": "#999999"
+            }
+        },
+        "splitLine": {
+            "show": true,
+            "lineStyle": {
+                "color": [
+                    "#eeeeee"
+                ]
+            }
+        },
+        "splitArea": {
+            "show": false,
+            "areaStyle": {
+                "color": [
+                    "rgba(250,250,250,0.05)",
+                    "rgba(200,200,200,0.02)"
+                ]
+            }
+        }
+    },
+    "toolbox": {
+        "iconStyle": {
+            "normal": {
+                "borderColor": "#999"
+            },
+            "emphasis": {
+                "borderColor": "#666"
+            }
+        }
+    },
+    "legend": {
+        "textStyle": {
+            "color": "#999999"
+        }
+    },
+    "tooltip": {
+        "axisPointer": {
+            "lineStyle": {
+                "color": "#ccc",
+                "width": 1
+            },
+            "crossStyle": {
+                "color": "#ccc",
+                "width": 1
+            }
+        }
+    },
+    "timeline": {
+        "lineStyle": {
+            "color": "#8fd3e8",
+            "width": 1
+        },
+        "itemStyle": {
+            "normal": {
+                "color": "#8fd3e8",
+                "borderWidth": 1
+            },
+            "emphasis": {
+                "color": "#8fd3e8"
+            }
+        },
+        "controlStyle": {
+            "normal": {
+                "color": "#8fd3e8",
+                "borderColor": "#8fd3e8",
+                "borderWidth": 0.5
+            },
+            "emphasis": {
+                "color": "#8fd3e8",
+                "borderColor": "#8fd3e8",
+                "borderWidth": 0.5
+            }
+        },
+        "checkpointStyle": {
+            "color": "#8fd3e8",
+            "borderColor": "rgba(138,124,168,0.37)"
+        },
+        "label": {
+            "normal": {
+                "textStyle": {
+                    "color": "#8fd3e8"
+                }
+            },
+            "emphasis": {
+                "textStyle": {
+                    "color": "#8fd3e8"
+                }
+            }
+        }
+    },
+    "visualMap": {
+        "color": [
+            "#516b91",
+            "#59c4e6",
+            "#a5e7f0"
+        ]
+    },
+    "dataZoom": {
+        "backgroundColor": "rgba(0,0,0,0)",
+        "dataBackgroundColor": "rgba(255,255,255,0.3)",
+        "fillerColor": "rgba(167,183,204,0.4)",
+        "handleColor": "#a7b7cc",
+        "handleSize": "100%",
+        "textStyle": {
+            "color": "#333"
+        }
+    },
+    "markPoint": {
+        "label": {
+            "normal": {
+                "textStyle": {
+                    "color": "#eee"
+                }
+            },
+            "emphasis": {
+                "textStyle": {
+                    "color": "#eee"
+                }
+            }
+        }
+    }
+}

+ 42 - 0
src/components/common-icon/common-icon.vue

@@ -0,0 +1,42 @@
+<template>
+  <component :is="iconType" :type="iconName" :color="iconColor" :size="iconSize"/>
+</template>
+
+<script>
+import Icons from '_c/icons'
+export default {
+  name: 'CommonIcon',
+  components: { Icons },
+  props: {
+    type: {
+      type: String,
+      required: true
+    },
+    color: String,
+    size: Number
+  },
+  computed: {
+    iconType () {
+      return this.type.indexOf('_') === 0 ? 'Icons' : 'Icon'
+    },
+    iconName () {
+      return this.iconType === 'Icons' ? this.getCustomIconName(this.type) : this.type
+    },
+    iconSize () {
+      return this.size || (this.iconType === 'Icons' ? 12 : undefined)
+    },
+    iconColor () {
+      return this.color || ''
+    }
+  },
+  methods: {
+    getCustomIconName (iconName) {
+      return iconName.slice(1)
+    }
+  }
+}
+</script>
+
+<style>
+
+</style>

+ 2 - 0
src/components/common-icon/index.js

@@ -0,0 +1,2 @@
+import CommonIcon from './common-icon.vue'
+export default CommonIcon

+ 8 - 0
src/components/common/common.less

@@ -0,0 +1,8 @@
+.no-select{
+  -webkit-touch-callout: none;
+  -webkit-user-select: none;
+  -khtml-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}

+ 3 - 0
src/components/common/util.js

@@ -0,0 +1,3 @@
+export const showTitle = (item, vm) => {
+  return vm.$config.useI18n ? vm.$t(item.name) : ((item.meta && item.meta.title) || item.name)
+}

+ 174 - 0
src/components/count-to/count-to.vue

@@ -0,0 +1,174 @@
+<template>
+  <div class="count-to-wrapper">
+    <slot name="left"/>
+    <p class="content-outer"><span :class="['count-to-count-text', countClass]" :id="counterId">{{ init }}</span><i :class="['count-to-unit-text', unitClass]">{{ unitText }}</i></p>
+    <slot name="right"/>
+  </div>
+</template>
+
+<script>
+import CountUp from 'countup'
+import './index.less'
+export default {
+  name: 'CountTo',
+  props: {
+    init: {
+      type: Number,
+      default: 0
+    },
+    /**
+     * @description 起始值,即动画开始前显示的数值
+     */
+    startVal: {
+      type: Number,
+      default: 0
+    },
+    /**
+     * @description 结束值,即动画结束后显示的数值
+     */
+    end: {
+      type: Number,
+      required: true
+    },
+    /**
+     * @description 保留几位小数
+     */
+    decimals: {
+      type: Number,
+      default: 0
+    },
+    /**
+     * @description 分隔整数和小数的符号,默认是小数点
+     */
+    decimal: {
+      type: String,
+      default: '.'
+    },
+    /**
+     * @description 动画持续的时间,单位是秒
+     */
+    duration: {
+      type: Number,
+      default: 2
+    },
+    /**
+     * @description 动画延迟开始的时间,单位是秒
+     */
+    delay: {
+      type: Number,
+      default: 0
+    },
+    /**
+     * @description 是否禁用easing动画效果
+     */
+    uneasing: {
+      type: Boolean,
+      default: false
+    },
+    /**
+     * @description 是否使用分组,分组后每三位会用一个符号分隔
+     */
+    usegroup: {
+      type: Boolean,
+      default: false
+    },
+    /**
+     * @description 用于分组(usegroup)的符号
+     */
+    separator: {
+      type: String,
+      default: ','
+    },
+    /**
+     * @description 是否简化显示,设为true后会使用unit单位来做相关省略
+     */
+    simplify: {
+      type: Boolean,
+      default: false
+    },
+    /**
+     * @description 自定义单位,如[3, 'K+'], [6, 'M+']即大于3位数小于6位数的用k+来做省略
+     *              1000即显示为1K+
+     */
+    unit: {
+      type: Array,
+      default () {
+        return [[3, 'K+'], [6, 'M+'], [9, 'B+']]
+      }
+    },
+    countClass: {
+      type: String,
+      default: ''
+    },
+    unitClass: {
+      type: String,
+      default: ''
+    }
+  },
+  data () {
+    return {
+      counter: null,
+      unitText: ''
+    }
+  },
+  computed: {
+    counterId () {
+      return `count_to_${this._uid}`
+    }
+  },
+  methods: {
+    getHandleVal (val, len) {
+      return {
+        endVal: parseInt(val / Math.pow(10, this.unit[len - 1][0])),
+        unitText: this.unit[len - 1][1]
+      }
+    },
+    transformValue (val) {
+      let len = this.unit.length
+      let res = {
+        endVal: 0,
+        unitText: ''
+      }
+      if (val < Math.pow(10, this.unit[0][0])) res.endVal = val
+      else {
+        for (let i = 1; i < len; i++) {
+          if (val >= Math.pow(10, this.unit[i - 1][0]) && val < Math.pow(10, this.unit[i][0])) res = this.getHandleVal(val, i)
+        }
+      }
+      if (val > Math.pow(10, this.unit[len - 1][0])) res = this.getHandleVal(val, len)
+      return res
+    },
+    getValue (val) {
+      let res = 0
+      if (this.simplify) {
+        let { endVal, unitText } = this.transformValue(val)
+        this.unitText = unitText
+        res = endVal
+      } else {
+        res = val
+      }
+      return res
+    }
+  },
+  mounted () {
+    this.$nextTick(() => {
+      let endVal = this.getValue(this.end)
+      this.counter = new CountUp(this.counterId, this.startVal, endVal, this.decimals, this.duration, {
+        useEasing: !this.uneasing,
+        useGrouping: this.useGroup,
+        separator: this.separator,
+        decimal: this.decimal
+      })
+      setTimeout(() => {
+        if (!this.counter.error) this.counter.start()
+      }, this.delay)
+    })
+  },
+  watch: {
+    end (newVal) {
+      let endVal = this.getValue(newVal)
+      this.counter.update(endVal)
+    }
+  }
+}
+</script>

+ 2 - 0
src/components/count-to/index.js

@@ -0,0 +1,2 @@
+import countTo from './count-to.vue'
+export default countTo

+ 10 - 0
src/components/count-to/index.less

@@ -0,0 +1,10 @@
+@prefix: ~"count-to";
+
+.@{prefix}-wrapper{
+  .content-outer{
+    display: inline-block;
+    .@{prefix}-unit-text{
+      font-style: normal;
+    }
+  }
+}

+ 2 - 0
src/components/cropper/index.js

@@ -0,0 +1,2 @@
+import Cropper from './index.vue'
+export default Cropper

+ 35 - 0
src/components/cropper/index.less

@@ -0,0 +1,35 @@
+.bg{
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC")
+}
+.cropper-wrapper{
+  width: 600px;
+  height: 340px;
+  .img-box{
+    height: 340px;
+    width: 430px;
+    border: 1px solid #ebebeb;
+    display: inline-block;
+    .bg;
+    img{
+      max-width: 100%;
+      display: block;
+    }
+  }
+  .right-con{
+    display: inline-block;
+    width: 170px;
+    vertical-align: top;
+    box-sizing: border-box;
+    padding: 0 10px;
+    .preview-box{
+      height: 150px !important;
+      width: 100% !important;
+      overflow: hidden;
+      border: 1px solid #ebebeb;
+      .bg;
+    }
+    .button-box{
+      padding: 10px 0 0;
+    }
+  }
+}

+ 139 - 0
src/components/cropper/index.vue

@@ -0,0 +1,139 @@
+<template>
+  <div class="cropper-wrapper">
+    <div class="img-box">
+      <img class="cropper-image" :id="imgId" alt="">
+    </div>
+    <div class="right-con">
+      <div v-if="preview" class="preview-box" :id="previewId"></div>
+      <div class="button-box">
+        <slot>
+          <Upload action="image/upload" :before-upload="beforeUpload">
+            <Button style="width: 150px;" type="primary">上传图片</Button>
+          </Upload>
+        </slot>
+        <div v-show="insideSrc">
+          <Button type="primary" @click="rotate">
+            <Icon type="md-refresh" :size="18"/>
+          </Button>
+          <Button type="primary" @click="shrink">
+            <Icon type="md-remove" :size="18"/>
+          </Button>
+          <Button type="primary" @click="magnify">
+            <Icon type="md-add" :size="18"/>
+          </Button>
+          <Button type="primary" @click="scale('X')">
+            <Icon custom="iconfont icon-shuipingfanzhuan" :size="18"/>
+          </Button>
+          <Button type="primary" @click="scale('Y')">
+            <Icon custom="iconfont icon-chuizhifanzhuan" :size="18"/>
+          </Button>
+          <Button type="primary" @click="move(0, -moveStep)">
+            <Icon type="md-arrow-round-up" :size="18"/>
+          </Button>
+          <Button type="primary" @click="move(-moveStep, 0)">
+            <Icon type="md-arrow-round-back" :size="18"/>
+          </Button>
+          <Button type="primary" @click="move(0, moveStep)">
+            <Icon type="md-arrow-round-down" :size="18"/>
+          </Button>
+          <Button type="primary" @click="move(moveStep, 0)">
+            <Icon type="md-arrow-round-forward" :size="18"/>
+          </Button>
+          <Button style="width: 150px;margin-top: 10px;" type="primary" @click="crop">{{ cropButtonText }}</Button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Cropper from 'cropperjs'
+import './index.less'
+import 'cropperjs/dist/cropper.min.css'
+export default {
+  name: 'Cropper',
+  props: {
+    src: {
+      type: String,
+      default: ''
+    },
+    preview: {
+      type: Boolean,
+      default: true
+    },
+    moveStep: {
+      type: Number,
+      default: 4
+    },
+    cropButtonText: {
+      type: String,
+      default: '裁剪'
+    }
+  },
+  data () {
+    return {
+      cropper: null,
+      insideSrc: ''
+    }
+  },
+  computed: {
+    imgId () {
+      return `cropper${this._uid}`
+    },
+    previewId () {
+      return `cropper_preview${this._uid}`
+    }
+  },
+  watch: {
+    src (src) {
+      this.replace(src)
+    },
+    insideSrc (src) {
+      this.replace(src)
+    }
+  },
+  methods: {
+    beforeUpload (file) {
+      const reader = new FileReader()
+      reader.readAsDataURL(file)
+      reader.onload = (event) => {
+        this.insideSrc = event.srcElement.result
+      }
+      return false
+    },
+    replace (src) {
+      this.cropper.replace(src)
+      this.insideSrc = src
+    },
+    rotate () {
+      this.cropper.rotate(90)
+    },
+    shrink () {
+      this.cropper.zoom(-0.1)
+    },
+    magnify () {
+      this.cropper.zoom(0.1)
+    },
+    scale (d) {
+      this.cropper[`scale${d}`](-this.cropper.getData()[`scale${d}`])
+    },
+    move (...argu) {
+      this.cropper.move(...argu)
+    },
+    crop () {
+      this.cropper.getCroppedCanvas().toBlob(blob => {
+        this.$emit('on-crop', blob)
+      })
+    }
+  },
+  mounted () {
+    this.$nextTick(() => {
+      let dom = document.getElementById(this.imgId)
+      this.cropper = new Cropper(dom, {
+        preview: `#${this.previewId}`,
+        checkCrossOrigin: true
+      })
+    })
+  }
+}
+</script>

+ 18 - 0
src/components/drag-drawer/drag-drawer-trigger.vue

@@ -0,0 +1,18 @@
+<template>
+  <div :class="`${prefix}-move-trigger`">
+    <div :class="`${prefix}-move-trigger-point`">
+      <i></i><i></i><i></i><i></i><i></i>
+    </div>
+  </div>
+</template>
+
+<script>
+import Mixin from './mixin'
+export default {
+  name: 'DragDrawerTrigger',
+  mixins: [Mixin]
+}
+</script>
+
+<style>
+</style>

+ 156 - 0
src/components/drag-drawer/drag-drawer.vue

@@ -0,0 +1,156 @@
+<template>
+  <Drawer ref="drawerWrapper"
+          :value="value"
+          @input="handleInput"
+          :width="width"
+          :class-name="outerClasses"
+          v-bind="$attrs"
+          v-on="$listeners">
+    <!-- 所有插槽内容显示在这里 ↓ -->
+
+    <template v-for="(slots, slotsName) in $slots">
+      <template v-if="slotsName !== 'default'">
+        <render-dom v-for="(render, index) in slots"
+                    :key="`b_drawer_${slotsName}_${index}`"
+                    :render="() => render"
+                    :slot="slotsName">
+        </render-dom>
+      </template>
+      <template v-else>
+        <div :class="`${prefix}-body-wrapper`"
+             :key="`b_drawer_${slotsName}`">
+          <render-dom v-for="(render, index) in slots"
+                      :key="`b_drawer_${slotsName}_${index}`"
+                      :render="() => render"
+                      :slot="slotsName">
+          </render-dom>
+        </div>
+      </template>
+    </template>
+    <!-- 所有插槽内容显示在这里 ↑ -->
+    <div v-if="draggable"
+         :style="triggerStyle"
+         :class="`${prefix}-trigger-wrapper`"
+         @mousedown="handleTriggerMousedown">
+      <slot name="trigger">
+        <drag-drawer-trigger></drag-drawer-trigger>
+      </slot>
+    </div>
+    <div v-if="$slots.footer"
+         :class="`${prefix}-footer`">
+      <slot name="footer"></slot>
+    </div>
+  </Drawer>
+</template>
+
+<script>
+import RenderDom from '@/libs/render-dom'
+import DragDrawerTrigger from './drag-drawer-trigger.vue'
+import Mixin from './mixin'
+import { on, off } from '@/libs/tools'
+import './index.less'
+export default {
+  name: 'BDrawer',
+  components: {
+    RenderDom,
+    DragDrawerTrigger
+  },
+  mixins: [Mixin],
+  props: {
+    value: {
+      type: Boolean,
+      default: false
+    },
+    width: {
+      type: [String, Number],
+      default: 256
+    },
+    // 是否可拖动修改宽度
+    draggable: {
+      type: Boolean,
+      default: false
+    },
+    // 最小拖动宽度
+    minWidth: {
+      type: [String, Number],
+      default: 256
+    }
+  },
+  data () {
+    return {
+      canMove: false,
+      wrapperWidth: 0,
+      wrapperLeft: 0
+    }
+  },
+  computed: {
+    outerClasses () {
+      const classesArray = [
+        `${this.prefix}-wrapper`,
+        this.canMove ? 'no-select pointer-events-none' : ''
+      ]
+      return classesArray.join(' ')
+    },
+    placement () {
+      return this.$attrs.placement
+    },
+    innerWidth () {
+      const width = this.width
+      return width <= 100 ? (this.wrapperWidth * width) / 100 : width
+    },
+    triggerStyle () {
+      return {
+        [this.placement]: `${this.innerWidth}px`,
+        position: this.$attrs.inner ? 'absolute' : 'fixed'
+      }
+    }
+  },
+  methods: {
+    handleInput (status) {
+      this.$emit('input', status)
+    },
+    handleTriggerMousedown (event) {
+      this.canMove = true
+      this.$emit('on-resize-start')
+      // 防止鼠标选中抽屉中文字,造成拖动trigger触发浏览器原生拖动行为
+      window.getSelection().removeAllRanges()
+    },
+    handleMousemove (event) {
+      if (!this.canMove) return
+      // 更新容器宽度和距离左侧页面距离,如果是window则距左侧距离为0
+      this.setWrapperWidth()
+      const left = event.pageX - this.wrapperLeft
+      // 如果抽屉方向为右边,宽度计算需用容器宽度减去left
+      let width = this.placement === 'right' ? this.wrapperWidth - left : left
+      // 限定做小宽度
+      width = Math.max(width, parseFloat(this.minWidth))
+      event.atMin = width === parseFloat(this.minWidth)
+      // 如果当前width不大于100,视为百分比
+      if (width <= 100) width = (width / this.wrapperWidth) * 100
+      this.$emit('update:width', parseInt(width))
+      this.$emit('on-resize', event)
+    },
+    handleMouseup (event) {
+      this.canMove = false
+      this.$emit('on-resize-end')
+    },
+    setWrapperWidth () {
+      const {
+        width,
+        left
+      } = this.$refs.drawerWrapper.$el.getBoundingClientRect()
+      this.wrapperWidth = width
+      this.wrapperLeft = left
+    }
+  },
+  mounted () {
+    on(document, 'mousemove', this.handleMousemove)
+    on(document, 'mouseup', this.handleMouseup)
+    this.setWrapperWidth()
+  },
+  beforeDestroy () {
+    off(document, 'mousemove', this.handleMousemove)
+    off(document, 'mouseup', this.handleMouseup)
+  }
+}
+</script>

+ 2 - 0
src/components/drag-drawer/index.js

@@ -0,0 +1,2 @@
+import DragDrawer from './drag-drawer.vue'
+export default DragDrawer

+ 70 - 0
src/components/drag-drawer/index.less

@@ -0,0 +1,70 @@
+@prefix: ~"drag-drawer";
+@drag-drawer-trigger-height: 100px;
+@drag-drawer-trigger-width: 8px;
+
+.@{prefix}-wrapper{
+  &.no-select{
+    user-select: none;
+  }
+  &.pointer-events-none{
+    pointer-events: none;
+    & .@{prefix}-trigger-wrapper{
+      pointer-events: all;
+    }
+  }
+  .ivu-drawer{
+    &-header{
+      overflow: hidden !important;
+      box-sizing: border-box;
+    }
+    &-body{
+      padding: 0;
+      overflow: visible;
+      position: static;
+      display: flex;
+      flex-direction: column;
+    }
+  }
+  .@{prefix}-body-wrapper{
+    width: 100%;
+    height: 100%;
+    padding: 16px;
+    overflow: auto;
+  }
+  .@{prefix}-trigger-wrapper{
+    top: 0;
+    height: 100%;
+    width: 0;
+    .@{prefix}-move-trigger{
+      position: absolute;
+      top: 50%;
+      height: @drag-drawer-trigger-height;
+      width: @drag-drawer-trigger-width;
+      background: rgb(243, 243, 243);
+      transform: translate(-50%, -50%);
+      border-radius: ~"4px / 6px";
+      box-shadow: 0 0 1px 1px rgba(0, 0, 0, .2);
+      line-height: @drag-drawer-trigger-height;
+      cursor: col-resize;
+      &-point{
+        display: inline-block;
+        width: 50%;
+        transform: translateX(50%);
+        i{
+          display: block;
+          border-bottom: 1px solid rgb(192, 192, 192);
+          padding-bottom: 2px;
+        }
+      }
+    }
+  }
+  .@{prefix}-footer{
+    flex-grow: 1;
+    width: 100%;
+    bottom: 0;
+    left: 0;
+    border-top: 1px solid #e8e8e8;
+    padding: 10px 16px;
+    background: #fff;
+  }
+}

+ 7 - 0
src/components/drag-drawer/mixin.js

@@ -0,0 +1,7 @@
+export default {
+  data () {
+    return {
+      prefix: 'drag-drawer'
+    }
+  }
+}

+ 92 - 0
src/components/drag-list/drag-list.vue

@@ -0,0 +1,92 @@
+<template>
+  <div class="drag-list-wrapper">
+    <div class="drag-list-con con1">
+      <slot name="left-title"></slot>
+      <draggable class="drop-box1" :class="dropConClass.left" :options="options" :value="list1" @input="handleListChange($event, 'left')" @end="handleEnd($event, 'left')">
+        <div class="drag-list-item" v-for="(itemLeft, index) in list1" :key="`drag_li1_${index}`">
+          <slot name="left" :itemLeft="itemLeft">{{ itemLeft }}</slot>
+        </div>
+      </draggable>
+    </div>
+    <div class="drag-list-con con2">
+      <slot name="right-title"></slot>
+      <draggable class="drop-box2" :class="dropConClass.right" :options="options" :value="list2" @input="handleListChange($event, 'right')" @end="handleEnd($event, 'right')">
+        <div class="drag-list-item" v-for="(itemRight, index) in list2" :key="`drag_li2_${index}`">
+          <slot name="right" :itemRight="itemRight">{{ itemRight }}</slot>
+        </div>
+      </draggable>
+    </div>
+  </div>
+</template>
+<script>
+import draggable from 'vuedraggable'
+export default {
+  name: 'DragList',
+  components: {
+    draggable
+  },
+  props: {
+    list1: {
+      type: Array,
+      required: true
+    },
+    list2: {
+      type: Array,
+      default: () => []
+    },
+    dropConClass: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  data () {
+    return {
+      options: { group: 'drag_list' }
+    }
+  },
+  methods: {
+    handleListChange (value, type) {
+      if (type === 'left') this.$emit('update:list1', value)
+      else this.$emit('update:list2', value)
+    },
+    handleEnd (event, type) {
+      const srcClassName = (event.srcElement || event.target).classList[0]
+      const targetClassName = event.to.classList[0]
+      let src = ''
+      let target = ''
+      if (srcClassName === targetClassName) {
+        if (type === 'left') {
+          src = 'left'
+          target = 'left'
+        } else {
+          src = 'right'
+          target = 'right'
+        }
+      } else {
+        if (type === 'left') {
+          src = 'left'
+          target = 'right'
+        } else {
+          src = 'right'
+          target = 'left'
+        }
+      }
+      this.$emit('on-change', {
+        src: src,
+        target: target,
+        oldIndex: event.oldIndex,
+        newIndex: event.newIndex
+      })
+    }
+  }
+}
+</script>
+<style lang="less">
+.drag-list-wrapper{
+  height: 100%;
+  .drag-list-con{
+    width: 50%;
+    float: left;
+  }
+}
+</style>

+ 2 - 0
src/components/drag-list/index.js

@@ -0,0 +1,2 @@
+import DragList from './drag-list.vue'
+export default DragList

+ 75 - 0
src/components/editor/editor.vue

@@ -0,0 +1,75 @@
+<template>
+  <div class="editor-wrapper">
+    <div :id="editorId"></div>
+  </div>
+</template>
+
+<script>
+import Editor from 'wangeditor'
+import 'wangeditor/release/wangEditor.min.css'
+import { oneOf } from '@/libs/tools'
+export default {
+  name: 'Editor',
+  props: {
+    value: {
+      type: String,
+      default: ''
+    },
+    /**
+     * 绑定的值的类型, enum: ['html', 'text']
+     */
+    valueType: {
+      type: String,
+      default: 'html',
+      validator: (val) => {
+        return oneOf(val, ['html', 'text'])
+      }
+    },
+    /**
+     * @description 设置change事件触发时间间隔
+     */
+    changeInterval: {
+      type: Number,
+      default: 200
+    },
+    /**
+     * @description 是否开启本地存储
+     */
+    cache: {
+      type: Boolean,
+      default: true
+    }
+  },
+  computed: {
+    editorId () {
+      return `editor${this._uid}`
+    }
+  },
+  methods: {
+    setHtml (val) {
+      this.editor.txt.html(val)
+    }
+  },
+  mounted () {
+    this.editor = new Editor(`#${this.editorId}`)
+    this.editor.customConfig.onchange = (html) => {
+      let text = this.editor.txt.text()
+      if (this.cache) localStorage.editorCache = html
+      this.$emit('input', this.valueType === 'html' ? html : text)
+      this.$emit('on-change', html, text)
+    }
+    this.editor.customConfig.onchangeTimeout = this.changeInterval
+    // create这个方法一定要在所有配置项之后调用
+    this.editor.create()
+    // 如果本地有存储加载本地存储内容
+    let html = this.value || localStorage.editorCache
+    if (html) this.editor.txt.html(html)
+  }
+}
+</script>
+
+<style lang="less">
+.editor-wrapper *{
+  z-index: 100 !important;
+}
+</style>

+ 2 - 0
src/components/editor/index.js

@@ -0,0 +1,2 @@
+import Editor from './editor.vue'
+export default Editor

+ 35 - 0
src/components/icons/icons.vue

@@ -0,0 +1,35 @@
+<template>
+  <i :class="`iconfont icon-${type}`" :style="styles"></i>
+</template>
+
+<script>
+export default {
+  name: 'Icons',
+  props: {
+    type: {
+      type: String,
+      required: true
+    },
+    color: {
+      type: String,
+      default: '#5c6b77'
+    },
+    size: {
+      type: Number,
+      default: 16
+    }
+  },
+  computed: {
+    styles () {
+      return {
+        fontSize: `${this.size}px`,
+        color: this.color
+      }
+    }
+  }
+}
+</script>
+
+<style>
+
+</style>

+ 2 - 0
src/components/icons/index.js

@@ -0,0 +1,2 @@
+import Icons from './icons.vue'
+export default Icons

+ 2 - 0
src/components/info-card/index.js

@@ -0,0 +1,2 @@
+import InforCard from './infor-card.vue'
+export default InforCard

+ 94 - 0
src/components/info-card/infor-card.vue

@@ -0,0 +1,94 @@
+<template>
+  <Card :shadow="shadow" class="info-card-wrapper" :padding="0">
+    <div class="content-con">
+      <div class="left-area" :style="{background: color, width: leftWidth}">
+        <common-icon class="icon" :type="icon" :size="iconSize" color="#fff"/>
+      </div>
+      <div class="right-area" :style="{width: rightWidth}">
+        <div>
+          <slot></slot>
+        </div>
+      </div>
+    </div>
+  </Card>
+</template>
+
+<script>
+import CommonIcon from '_c/common-icon'
+export default {
+  name: 'InforCard',
+  components: {
+    CommonIcon
+  },
+  props: {
+    left: {
+      type: Number,
+      default: 36
+    },
+    color: {
+      type: String,
+      default: '#2d8cf0'
+    },
+    icon: {
+      type: String,
+      default: ''
+    },
+    iconSize: {
+      type: Number,
+      default: 20
+    },
+    shadow: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    leftWidth () {
+      return `${this.left}%`
+    },
+    rightWidth () {
+      return `${100 - this.left}%`
+    }
+  }
+}
+</script>
+
+<style lang="less">
+.common{
+  float: left;
+  height: 100%;
+  display: table;
+  text-align: center;
+}
+.size{
+  width: 100%;
+  height: 100%;
+}
+.middle-center{
+  display: table-cell;
+  vertical-align: middle;
+}
+.info-card-wrapper{
+  .size;
+  overflow: hidden;
+  .ivu-card-body{
+    .size;
+  }
+  .content-con{
+    .size;
+    position: relative;
+    .left-area{
+      .common;
+      & > .icon{
+        .middle-center;
+      }
+    }
+    .right-area{
+      .common;
+      & > div{
+        .middle-center;
+      }
+    }
+  }
+}
+</style>

+ 2 - 0
src/components/login-form/index.js

@@ -0,0 +1,2 @@
+import LoginForm from './login-form.vue'
+export default LoginForm

+ 73 - 0
src/components/login-form/login-form.vue

@@ -0,0 +1,73 @@
+<template>
+  <Form ref="loginForm" :model="form" :rules="rules" @keydown.enter.native="handleSubmit">
+    <FormItem prop="userName">
+      <Input v-model="form.userName" placeholder="请输入用户名">
+        <span slot="prepend">
+          <Icon :size="16" type="ios-person"></Icon>
+        </span>
+      </Input>
+    </FormItem>
+    <FormItem prop="password">
+      <Input type="password" v-model="form.password" placeholder="请输入密码">
+        <span slot="prepend">
+          <Icon :size="14" type="md-lock"></Icon>
+        </span>
+      </Input>
+    </FormItem>
+    <FormItem>
+      <Button @click="handleSubmit" type="primary" long>登录</Button>
+    </FormItem>
+  </Form>
+</template>
+<script>
+export default {
+  name: 'LoginForm',
+  props: {
+    userNameRules: {
+      type: Array,
+      default: () => {
+        return [
+          { required: true, message: '账号不能为空', trigger: 'blur' }
+        ]
+      }
+    },
+    passwordRules: {
+      type: Array,
+      default: () => {
+        return [
+          { required: true, message: '密码不能为空', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  data () {
+    return {
+      form: {
+        // userName: 'super_admin',
+        userName: '',
+        password: ''
+      }
+    }
+  },
+  computed: {
+    rules () {
+      return {
+        userName: this.userNameRules,
+        password: this.passwordRules
+      }
+    }
+  },
+  methods: {
+    handleSubmit () {
+      this.$refs.loginForm.validate((valid) => {
+        if (valid) {
+          this.$emit('on-success-valid', {
+            userName: this.form.userName,
+            password: this.form.password
+          })
+        }
+      })
+    }
+  }
+}
+</script>

+ 2 - 0
src/components/main/components/a-back-top/index.js

@@ -0,0 +1,2 @@
+import ABackTop from './index.vue'
+export default ABackTop

+ 90 - 0
src/components/main/components/a-back-top/index.vue

@@ -0,0 +1,90 @@
+<template>
+    <div :class="classes" :style="styles" @click="back">
+        <slot>
+            <div :class="innerClasses">
+                <i class="ivu-icon ivu-icon-ios-arrow-up"></i>
+            </div>
+        </slot>
+    </div>
+</template>
+<script>
+import { scrollTop } from '@/libs/util'
+import { on, off } from '@/libs/tools'
+const prefixCls = 'ivu-back-top'
+
+export default {
+  name: 'ABackTop',
+  props: {
+    height: {
+      type: Number,
+      default: 400
+    },
+    bottom: {
+      type: Number,
+      default: 30
+    },
+    right: {
+      type: Number,
+      default: 30
+    },
+    duration: {
+      type: Number,
+      default: 1000
+    },
+    container: {
+      type: null,
+      default: window
+    }
+  },
+  data () {
+    return {
+      backTop: false
+    }
+  },
+  mounted () {
+    // window.addEventListener('scroll', this.handleScroll, false)
+    // window.addEventListener('resize', this.handleScroll, false)
+    on(this.containerEle, 'scroll', this.handleScroll)
+    on(this.containerEle, 'resize', this.handleScroll)
+  },
+  beforeDestroy () {
+    // window.removeEventListener('scroll', this.handleScroll, false)
+    // window.removeEventListener('resize', this.handleScroll, false)
+    off(this.containerEle, 'scroll', this.handleScroll)
+    off(this.containerEle, 'resize', this.handleScroll)
+  },
+  computed: {
+    classes () {
+      return [
+        `${prefixCls}`,
+        {
+          [`${prefixCls}-show`]: this.backTop
+        }
+      ]
+    },
+    styles () {
+      return {
+        bottom: `${this.bottom}px`,
+        right: `${this.right}px`
+      }
+    },
+    innerClasses () {
+      return `${prefixCls}-inner`
+    },
+    containerEle () {
+      return this.container === window ? window : document.querySelector(this.container)
+    }
+  },
+  methods: {
+    handleScroll () {
+      this.backTop = this.containerEle.scrollTop >= this.height
+    },
+    back () {
+      let target = typeof this.container === 'string' ? this.containerEle : (document.documentElement || document.body)
+      const sTop = target.scrollTop
+      scrollTop(this.containerEle, sTop, 0, this.duration)
+      this.$emit('on-click')
+    }
+  }
+}
+</script>

+ 49 - 0
src/components/main/components/error-store/error-store.vue

@@ -0,0 +1,49 @@
+<template>
+  <div class="error-store">
+    <Badge dot :count="countComputed">
+      <Button type="text" @click="openErrorLoggerPage">
+        <Icon :size="20" type="ios-bug"/>
+      </Button>
+    </Badge>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ErrorStore',
+  props: {
+    count: {
+      type: Number,
+      default: 0
+    },
+    hasRead: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    countComputed () {
+      return this.hasRead ? 0 : this.count
+    }
+  },
+  methods: {
+    openErrorLoggerPage () {
+      this.$router.push({
+        name: 'error_logger_page'
+      })
+    }
+  }
+}
+</script>
+
+<style lang="less">
+.error-store{
+  margin-right: 12px;
+  .ivu-badge-dot{
+    top: 20px;
+  }
+  .ivu-btn.ivu-btn-text{
+    padding: 5px 1px 6px;
+  }
+}
+</style>

+ 2 - 0
src/components/main/components/error-store/index.js

@@ -0,0 +1,2 @@
+import ErrorStore from './error-store.vue'
+export default ErrorStore

+ 84 - 0
src/components/main/components/fullscreen/fullscreen.vue

@@ -0,0 +1,84 @@
+<template>
+  <div v-if="showFullScreenBtn" class="full-screen-btn-con">
+    <Tooltip :content="value ? '退出全屏' : '全屏'" placement="bottom">
+      <Icon @click.native="handleChange" :type="value ? 'md-contract' : 'md-expand'" :size="23"></Icon>
+    </Tooltip>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Fullscreen',
+  computed: {
+    showFullScreenBtn () {
+      return window.navigator.userAgent.indexOf('MSIE') < 0
+    }
+  },
+  props: {
+    value: {
+      type: Boolean,
+      default: false
+    }
+  },
+  methods: {
+    handleFullscreen () {
+      let main = document.body
+      if (this.value) {
+        if (document.exitFullscreen) {
+          document.exitFullscreen()
+        } else if (document.mozCancelFullScreen) {
+          document.mozCancelFullScreen()
+        } else if (document.webkitCancelFullScreen) {
+          document.webkitCancelFullScreen()
+        } else if (document.msExitFullscreen) {
+          document.msExitFullscreen()
+        }
+      } else {
+        if (main.requestFullscreen) {
+          main.requestFullscreen()
+        } else if (main.mozRequestFullScreen) {
+          main.mozRequestFullScreen()
+        } else if (main.webkitRequestFullScreen) {
+          main.webkitRequestFullScreen()
+        } else if (main.msRequestFullscreen) {
+          main.msRequestFullscreen()
+        }
+      }
+    },
+    handleChange () {
+      this.handleFullscreen()
+    }
+  },
+  mounted () {
+    let isFullscreen = document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen
+    isFullscreen = !!isFullscreen
+    document.addEventListener('fullscreenchange', () => {
+      this.$emit('input', !this.value)
+      this.$emit('on-change', !this.value)
+    })
+    document.addEventListener('mozfullscreenchange', () => {
+      this.$emit('input', !this.value)
+      this.$emit('on-change', !this.value)
+    })
+    document.addEventListener('webkitfullscreenchange', () => {
+      this.$emit('input', !this.value)
+      this.$emit('on-change', !this.value)
+    })
+    document.addEventListener('msfullscreenchange', () => {
+      this.$emit('input', !this.value)
+      this.$emit('on-change', !this.value)
+    })
+    this.$emit('input', isFullscreen)
+  }
+}
+</script>
+
+<style lang="less">
+.full-screen-btn-con .ivu-tooltip-rel{
+  height: 64px;
+  line-height: 56px;
+  i{
+    cursor: pointer;
+  }
+}
+</style>

+ 2 - 0
src/components/main/components/fullscreen/index.js

@@ -0,0 +1,2 @@
+import Fullscreen from './fullscreen.vue'
+export default Fullscreen

+ 4 - 0
src/components/main/components/header-bar/custom-bread-crumb/custom-bread-crumb.less

@@ -0,0 +1,4 @@
+.custom-bread-crumb{
+  display: inline-block;
+  vertical-align: top;
+}

+ 46 - 0
src/components/main/components/header-bar/custom-bread-crumb/custom-bread-crumb.vue

@@ -0,0 +1,46 @@
+<template>
+  <div class="custom-bread-crumb">
+    <Breadcrumb :style="{fontSize: `${fontSize}px`}">
+      <BreadcrumbItem v-for="item in list" :to="item.to" :key="`bread-crumb-${item.name}`">
+        <common-icon style="margin-right: 4px;" :type="item.icon || ''"/>
+        {{ showTitle(item) }}
+      </BreadcrumbItem>
+    </Breadcrumb>
+  </div>
+</template>
+<script>
+import { showTitle } from '@/libs/util'
+import CommonIcon from '_c/common-icon'
+import './custom-bread-crumb.less'
+export default {
+  name: 'customBreadCrumb',
+  components: {
+    CommonIcon
+  },
+  props: {
+    list: {
+      type: Array,
+      default: () => []
+    },
+    fontSize: {
+      type: Number,
+      default: 14
+    },
+    showIcon: {
+      type: Boolean,
+      default: false
+    }
+  },
+  methods: {
+    showTitle (item) {
+      return showTitle(item, this)
+    },
+    isCustomIcon (iconName) {
+      return iconName.indexOf('_') === 0
+    },
+    getCustomIconName (iconName) {
+      return iconName.slice(1)
+    }
+  }
+}
+</script>

+ 2 - 0
src/components/main/components/header-bar/custom-bread-crumb/index.js

@@ -0,0 +1,2 @@
+import customBreadCrumb from './custom-bread-crumb.vue'
+export default customBreadCrumb

+ 14 - 0
src/components/main/components/header-bar/header-bar.less

@@ -0,0 +1,14 @@
+.header-bar{
+  width: 100%;
+  height: 100%;
+  position: relative;
+  .custom-content-con{
+    float: right;
+    height: auto;
+    padding-right: 20px;
+    line-height: 64px;
+    & > *{
+      float: right;
+    }
+  }
+}

+ 34 - 0
src/components/main/components/header-bar/header-bar.vue

@@ -0,0 +1,34 @@
+<template>
+  <div class="header-bar">
+    <sider-trigger :collapsed="collapsed" icon="md-menu" @on-change="handleCollpasedChange"></sider-trigger>
+    <custom-bread-crumb show-icon style="margin-left: 30px;" :list="breadCrumbList"></custom-bread-crumb>
+    <div class="custom-content-con">
+      <slot></slot>
+    </div>
+  </div>
+</template>
+<script>
+import siderTrigger from './sider-trigger'
+import customBreadCrumb from './custom-bread-crumb'
+import './header-bar.less'
+export default {
+  name: 'HeaderBar',
+  components: {
+    siderTrigger,
+    customBreadCrumb
+  },
+  props: {
+    collapsed: Boolean
+  },
+  computed: {
+    breadCrumbList () {
+      return this.$store.state.app.breadCrumbList
+    }
+  },
+  methods: {
+    handleCollpasedChange (state) {
+      this.$emit('on-coll-change', state)
+    }
+  }
+}
+</script>

+ 2 - 0
src/components/main/components/header-bar/index.js

@@ -0,0 +1,2 @@
+import HeaderBar from './header-bar'
+export default HeaderBar

+ 2 - 0
src/components/main/components/header-bar/sider-trigger/index.js

@@ -0,0 +1,2 @@
+import siderTrigger from './sider-trigger.vue'
+export default siderTrigger

+ 21 - 0
src/components/main/components/header-bar/sider-trigger/sider-trigger.less

@@ -0,0 +1,21 @@
+.trans{
+  transition: transform .2s ease;
+}
+@size: 40px;
+.sider-trigger-a{
+  padding: 6px;
+  width: @size;
+  height: @size;
+  display: inline-block;
+  text-align: center;
+  color: #5c6b77;
+  margin-top: 12px;
+  i{
+    .trans;
+    vertical-align: top;
+  }
+  &.collapsed i{
+    transform: rotateZ(90deg);
+    .trans;
+  }
+}

+ 27 - 0
src/components/main/components/header-bar/sider-trigger/sider-trigger.vue

@@ -0,0 +1,27 @@
+<template>
+  <a @click="handleChange" type="text" :class="['sider-trigger-a', collapsed ? 'collapsed' : '']"><Icon :type="icon" :size="size" /></a>
+</template>
+<script>
+export default {
+  name: 'siderTrigger',
+  props: {
+    collapsed: Boolean,
+    icon: {
+      type: String,
+      default: 'navicon-round'
+    },
+    size: {
+      type: Number,
+      default: 26
+    }
+  },
+  methods: {
+    handleChange () {
+      this.$emit('on-change', !this.collapsed)
+    }
+  }
+}
+</script>
+<style lang="less">
+@import './sider-trigger.less';
+</style>

+ 2 - 0
src/components/main/components/language/index.js

@@ -0,0 +1,2 @@
+import Language from './language.vue'
+export default Language

+ 51 - 0
src/components/main/components/language/language.vue

@@ -0,0 +1,51 @@
+<template>
+  <div>
+    <Dropdown trigger="click" @on-click="selectLang">
+      <a href="javascript:void(0)">
+        {{ title }}
+        <Icon :size="18" type="md-arrow-dropdown" />
+      </a>
+      <DropdownMenu slot="list">
+        <DropdownItem v-for="(value, key) in localList" :name="key" :key="`lang-${key}`">{{ value }}</DropdownItem>
+      </DropdownMenu>
+    </Dropdown>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Language',
+  props: {
+    lang: String
+  },
+  data () {
+    return {
+      langList: {
+        'zh-CN': '语言',
+        'zh-TW': '語言',
+        'en-US': 'Lang'
+      },
+      localList: {
+        'zh-CN': '中文简体',
+        'zh-TW': '中文繁体',
+        'en-US': 'English'
+      }
+    }
+  },
+  watch: {
+    lang (lang) {
+      this.$i18n.locale = lang
+    }
+  },
+  computed: {
+    title () {
+      return this.langList[this.lang]
+    }
+  },
+  methods: {
+    selectLang (name) {
+      this.$emit('on-lang-change', name)
+    }
+  }
+}
+</script>

+ 51 - 0
src/components/main/components/side-menu/collapsed-menu.vue

@@ -0,0 +1,51 @@
+<template>
+  <Dropdown ref="dropdown" @on-click="handleClick" :class="hideTitle ? '' : 'collased-menu-dropdown'" :transfer="hideTitle" :placement="placement">
+    <a class="drop-menu-a" type="text" @mouseover="handleMousemove($event, children)" :style="{textAlign: !hideTitle ? 'left' : ''}"><common-icon :size="rootIconSize" :color="textColor" :type="parentItem.icon"/><span class="menu-title" v-if="!hideTitle">{{ showTitle(parentItem) }}</span><Icon style="float: right;" v-if="!hideTitle" type="ios-arrow-forward" :size="16"/></a>
+    <DropdownMenu ref="dropdown" slot="list">
+      <template v-for="child in children">
+        <collapsed-menu v-if="showChildren(child)" :icon-size="iconSize" :parent-item="child" :key="`drop-${child.name}`"></collapsed-menu>
+        <DropdownItem v-else :key="`drop-${child.name}`" :name="child.name"><common-icon :size="iconSize" :type="child.icon"/><span class="menu-title">{{ showTitle(child) }}</span></DropdownItem>
+      </template>
+    </DropdownMenu>
+  </Dropdown>
+</template>
+<script>
+import mixin from './mixin'
+import itemMixin from './item-mixin'
+import { findNodeUpperByClasses } from '@/libs/util'
+
+export default {
+  name: 'CollapsedMenu',
+  mixins: [ mixin, itemMixin ],
+  props: {
+    hideTitle: {
+      type: Boolean,
+      default: false
+    },
+    rootIconSize: {
+      type: Number,
+      default: 16
+    }
+  },
+  data () {
+    return {
+      placement: 'right-end'
+    }
+  },
+  methods: {
+    handleClick (name) {
+      this.$emit('on-click', name)
+    },
+    handleMousemove (event, children) {
+      const { pageY } = event
+      const height = children.length * 38
+      const isOverflow = pageY + height < window.innerHeight
+      this.placement = isOverflow ? 'right-start' : 'right-end'
+    }
+  },
+  mounted () {
+    let dropdown = findNodeUpperByClasses(this.$refs.dropdown.$el, ['ivu-select-dropdown', 'ivu-dropdown-transfer'])
+    if (dropdown) dropdown.style.overflow = 'visible'
+  }
+}
+</script>

+ 2 - 0
src/components/main/components/side-menu/index.js

@@ -0,0 +1,2 @@
+import SideMenu from './side-menu.vue'
+export default SideMenu

+ 21 - 0
src/components/main/components/side-menu/item-mixin.js

@@ -0,0 +1,21 @@
+export default {
+  props: {
+    parentItem: {
+      type: Object,
+      default: () => {}
+    },
+    theme: String,
+    iconSize: Number
+  },
+  computed: {
+    parentName () {
+      return this.parentItem.name
+    },
+    children () {
+      return this.parentItem.children
+    },
+    textColor () {
+      return this.theme === 'dark' ? '#fff' : '#495060'
+    }
+  }
+}

+ 18 - 0
src/components/main/components/side-menu/mixin.js

@@ -0,0 +1,18 @@
+import CommonIcon from '_c/common-icon'
+import { showTitle } from '@/libs/util'
+export default {
+  components: {
+    CommonIcon
+  },
+  methods: {
+    showTitle (item) {
+      return showTitle(item, this)
+    },
+    showChildren (item) {
+      return item.children && (item.children.length > 1 || (item.meta && item.meta.showAlways))
+    },
+    getNameOrHref (item, children0) {
+      return item.href ? `isTurnByHref_${item.href}` : (children0 ? item.children[0].name : item.name)
+    }
+  }
+}

+ 26 - 0
src/components/main/components/side-menu/side-menu-item.vue

@@ -0,0 +1,26 @@
+<template>
+  <Submenu :name="`${parentName}`">
+    <template slot="title">
+      <common-icon :type="parentItem.icon || ''"/>
+      <span>{{ showTitle(parentItem) }}</span>
+    </template>
+    <template v-for="item in children">
+      <template v-if="item.children && item.children.length === 1">
+        <side-menu-item v-if="showChildren(item)" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item>
+        <menu-item v-else :name="getNameOrHref(item, true)" :key="`menu-${item.children[0].name}`"><common-icon :type="item.children[0].icon || ''"/><span>{{ showTitle(item.children[0]) }}</span></menu-item>
+      </template>
+      <template v-else>
+        <side-menu-item v-if="showChildren(item)" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item>
+        <menu-item v-else :name="getNameOrHref(item)" :key="`menu-${item.name}`"><common-icon :type="item.icon || ''"/><span>{{ showTitle(item) }}</span></menu-item>
+      </template>
+    </template>
+  </Submenu>
+</template>
+<script>
+import mixin from './mixin'
+import itemMixin from './item-mixin'
+export default {
+  name: 'SideMenuItem',
+  mixins: [ mixin, itemMixin ]
+}
+</script>

+ 40 - 0
src/components/main/components/side-menu/side-menu.less

@@ -0,0 +1,40 @@
+.side-menu-wrapper{
+  user-select: none;
+  .menu-collapsed{
+    padding-top: 10px;
+
+    .ivu-dropdown{
+      width: 100%;
+      .ivu-dropdown-rel a{
+        width: 100%;
+      }
+    }
+    .ivu-tooltip{
+      width: 100%;
+      .ivu-tooltip-rel{
+        width: 100%;
+      }
+      .ivu-tooltip-popper .ivu-tooltip-content{
+        .ivu-tooltip-arrow{
+          border-right-color: #fff;
+        }
+        .ivu-tooltip-inner{
+          background: #fff;
+          color: #495060;
+        }
+      }
+    }
+
+
+  }
+  a.drop-menu-a{
+    display: inline-block;
+    padding: 6px 15px;
+    width: 100%;
+    text-align: center;
+    color: #495060;
+  }
+}
+.menu-title{
+  padding-left: 6px;
+}

+ 114 - 0
src/components/main/components/side-menu/side-menu.vue

@@ -0,0 +1,114 @@
+<template>
+  <div class="side-menu-wrapper">
+    <slot></slot>
+    <Menu ref="menu" v-show="!collapsed" :active-name="activeName" :open-names="openedNames" :accordion="accordion" :theme="theme" width="auto" @on-select="handleSelect">
+      <template v-for="item in menuList">
+        <template v-if="item.children && item.children.length === 1">
+          <side-menu-item v-if="showChildren(item)" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item>
+          <menu-item v-else :name="getNameOrHref(item, true)" :key="`menu-${item.children[0].name}`"><common-icon :type="item.children[0].icon || ''"/><span>{{ showTitle(item.children[0]) }}</span></menu-item>
+        </template>
+        <template v-else>
+          <side-menu-item v-if="showChildren(item)" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item>
+          <menu-item v-else :name="getNameOrHref(item)" :key="`menu-${item.name}`"><common-icon :type="item.icon || ''"/><span>{{ showTitle(item) }}</span></menu-item>
+        </template>
+      </template>
+    </Menu>
+    <div class="menu-collapsed" v-show="collapsed" :list="menuList">
+      <template v-for="item in menuList">
+        <collapsed-menu v-if="item.children && item.children.length > 1" @on-click="handleSelect" hide-title :root-icon-size="rootIconSize" :icon-size="iconSize" :theme="theme" :parent-item="item" :key="`drop-menu-${item.name}`"></collapsed-menu>
+        <Tooltip transfer v-else :content="showTitle(item.children && item.children[0] ? item.children[0] : item)" placement="right" :key="`drop-menu-${item.name}`">
+          <a @click="handleSelect(getNameOrHref(item, true))" class="drop-menu-a" :style="{textAlign: 'center'}"><common-icon :size="rootIconSize" :color="textColor" :type="item.icon || (item.children && item.children[0].icon)"/></a>
+        </Tooltip>
+      </template>
+    </div>
+  </div>
+</template>
+<script>
+import SideMenuItem from './side-menu-item.vue'
+import CollapsedMenu from './collapsed-menu.vue'
+import { getUnion } from '@/libs/tools'
+import mixin from './mixin'
+
+export default {
+  name: 'SideMenu',
+  mixins: [ mixin ],
+  components: {
+    SideMenuItem,
+    CollapsedMenu
+  },
+  props: {
+    menuList: {
+      type: Array,
+      default () {
+        return []
+      }
+    },
+    collapsed: {
+      type: Boolean
+    },
+    theme: {
+      type: String,
+      default: 'dark'
+    },
+    rootIconSize: {
+      type: Number,
+      default: 20
+    },
+    iconSize: {
+      type: Number,
+      default: 16
+    },
+    accordion: Boolean,
+    activeName: {
+      type: String,
+      default: ''
+    },
+    openNames: {
+      type: Array,
+      default: () => []
+    }
+  },
+  data () {
+    return {
+      openedNames: []
+    }
+  },
+  methods: {
+    handleSelect (name) {
+      this.$emit('on-select', name)
+    },
+    getOpenedNamesByActiveName (name) {
+      return this.$route.matched.map(item => item.name).filter(item => item !== name)
+    },
+    updateOpenName (name) {
+      if (name === this.$config.homeName) this.openedNames = []
+      else this.openedNames = this.getOpenedNamesByActiveName(name)
+    }
+  },
+  computed: {
+    textColor () {
+      return this.theme === 'dark' ? '#fff' : '#495060'
+    }
+  },
+  watch: {
+    activeName (name) {
+      if (this.accordion) this.openedNames = this.getOpenedNamesByActiveName(name)
+      else this.openedNames = getUnion(this.openedNames, this.getOpenedNamesByActiveName(name))
+    },
+    openNames (newNames) {
+      this.openedNames = newNames
+    },
+    openedNames () {
+      this.$nextTick(() => {
+        this.$refs.menu.updateOpened()
+      })
+    }
+  },
+  mounted () {
+    this.openedNames = getUnion(this.openedNames, this.getOpenedNamesByActiveName(name))
+  }
+}
+</script>
+<style lang="less">
+@import './side-menu.less';
+</style>

+ 2 - 0
src/components/main/components/tags-nav/index.js

@@ -0,0 +1,2 @@
+import TagsNav from './tags-nav.vue'
+export default TagsNav

+ 87 - 0
src/components/main/components/tags-nav/tags-nav.less

@@ -0,0 +1,87 @@
+.no-select{
+  -webkit-touch-callout: none;
+  -webkit-user-select: none;
+  -khtml-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.size{
+  width: 100%;
+  height: 100%;
+}
+.tags-nav{
+  position: relative;
+  border-top: 1px solid #F0F0F0;
+  border-bottom: 1px solid #F0F0F0;
+  .no-select;
+  .size;
+  .close-con{
+    position: absolute;
+    right: 0;
+    top: 0;
+    height: 100%;
+    width: 32px;
+    background: #fff;
+    text-align: center;
+    z-index: 10;
+  }
+  .btn-con{
+    position: absolute;
+    top: 0px;
+    height: 100%;
+    background: #fff;
+    padding-top: 3px;
+    z-index: 10;
+    button{
+      padding: 6px 4px;
+      line-height: 14px;
+      text-align: center;
+    }
+    &.left-btn{
+      left: 0px;
+    }
+    &.right-btn{
+      right: 32px;
+      border-right: 1px solid #F0F0F0;
+    }
+  }
+  .scroll-outer{
+    position: absolute;
+    left: 28px;
+    right: 61px;
+    top: 0;
+    bottom: 0;
+    box-shadow: 0px 0 3px 2px rgba(100,100,100,.1) inset;
+    .scroll-body{
+      height: ~"calc(100% - 1px)";
+      display: inline-block;
+      padding: 1px 4px 0;
+      position: absolute;
+      overflow: visible;
+      white-space: nowrap;
+      transition: left .3s ease;
+      .ivu-tag-dot-inner{
+        transition: background .2s ease;
+      }
+    }
+  }
+  .contextmenu {
+    position: absolute;
+    margin: 0;
+    padding: 5px 0;
+    background: #fff;
+    z-index: 1000;
+    list-style-type: none;
+    border-radius: 4px;
+    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .1);
+    li {
+      margin: 0;
+      padding: 5px 15px;
+      cursor: pointer;
+      &:hover {
+        background: #eee;
+      }
+    }
+  }
+}

+ 209 - 0
src/components/main/components/tags-nav/tags-nav.vue

@@ -0,0 +1,209 @@
+<template>
+  <div class="tags-nav">
+    <div class="close-con">
+      <Dropdown transfer @on-click="handleTagsOption" style="margin-top:7px;">
+        <Button size="small" type="text">
+          <Icon :size="18" type="ios-close-circle-outline" />
+        </Button>
+        <DropdownMenu slot="list">
+          <DropdownItem name="close-all">关闭所有</DropdownItem>
+          <DropdownItem name="close-others">关闭其他</DropdownItem>
+        </DropdownMenu>
+      </Dropdown>
+    </div>
+    <ul v-show="visible" :style="{left: contextMenuLeft + 'px', top: contextMenuTop + 'px'}" class="contextmenu">
+      <li v-for="(item, key) of menuList" @click="handleTagsOption(key)" :key="key">{{item}}</li>
+    </ul>
+    <div class="btn-con left-btn">
+      <Button type="text" @click="handleScroll(240)">
+        <Icon :size="18" type="ios-arrow-back" />
+      </Button>
+    </div>
+    <div class="btn-con right-btn">
+      <Button type="text" @click="handleScroll(-240)">
+        <Icon :size="18" type="ios-arrow-forward" />
+      </Button>
+    </div>
+    <div class="scroll-outer" ref="scrollOuter" @DOMMouseScroll="handlescroll" @mousewheel="handlescroll">
+      <div ref="scrollBody" class="scroll-body" :style="{left: tagBodyLeft + 'px'}">
+        <transition-group name="taglist-moving-animation">
+          <Tag
+            type="dot"
+            v-for="(item, index) in list"
+            ref="tagsPageOpened"
+            :key="`tag-nav-${index}`"
+            :name="item.name"
+            :data-route-item="item"
+            @on-close="handleClose(item)"
+            @click.native="handleClick(item)"
+            :closable="item.name !== $config.homeName"
+            :color="isCurrentTag(item) ? 'primary' : 'default'"
+            @contextmenu.prevent.native="contextMenu(item, $event)"
+          >{{ showTitleInside(item) }}</Tag>
+        </transition-group>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { showTitle, routeEqual } from '@/libs/util'
+import beforeClose from '@/router/before-close'
+export default {
+  name: 'TagsNav',
+  props: {
+    value: Object,
+    list: {
+      type: Array,
+      default () {
+        return []
+      }
+    }
+  },
+  data () {
+    return {
+      tagBodyLeft: 0,
+      rightOffset: 40,
+      outerPadding: 4,
+      contextMenuLeft: 0,
+      contextMenuTop: 0,
+      visible: false,
+      menuList: {
+        others: '关闭其他',
+        all: '关闭所有'
+      }
+    }
+  },
+  computed: {
+    currentRouteObj () {
+      const { name, params, query } = this.value
+      return { name, params, query }
+    }
+  },
+  methods: {
+    handlescroll (e) {
+      var type = e.type
+      let delta = 0
+      if (type === 'DOMMouseScroll' || type === 'mousewheel') {
+        delta = (e.wheelDelta) ? e.wheelDelta : -(e.detail || 0) * 40
+      }
+      this.handleScroll(delta)
+    },
+    handleScroll (offset) {
+      const outerWidth = this.$refs.scrollOuter.offsetWidth
+      const bodyWidth = this.$refs.scrollBody.offsetWidth
+      if (offset > 0) {
+        this.tagBodyLeft = Math.min(0, this.tagBodyLeft + offset)
+      } else {
+        if (outerWidth < bodyWidth) {
+          if (this.tagBodyLeft < -(bodyWidth - outerWidth)) {
+            this.tagBodyLeft = this.tagBodyLeft
+          } else {
+            this.tagBodyLeft = Math.max(this.tagBodyLeft + offset, outerWidth - bodyWidth)
+          }
+        } else {
+          this.tagBodyLeft = 0
+        }
+      }
+    },
+    handleTagsOption (type) {
+      if (type.includes('all')) {
+        // 关闭所有,除了home
+        let res = this.list.filter(item => item.name === this.$config.homeName)
+        this.$emit('on-close', res, 'all')
+      } else if (type.includes('others')) {
+        // 关闭除当前页和home页的其他页
+        let res = this.list.filter(item => routeEqual(this.currentRouteObj, item) || item.name === this.$config.homeName)
+        this.$emit('on-close', res, 'others', this.currentRouteObj)
+        setTimeout(() => {
+          this.getTagElementByRoute(this.currentRouteObj)
+        }, 100)
+      }
+    },
+    handleClose (current) {
+      if (current.meta && current.meta.beforeCloseName && current.meta.beforeCloseName in beforeClose) {
+        new Promise(beforeClose[current.meta.beforeCloseName]).then(close => {
+          if (close) {
+            this.close(current)
+          }
+        })
+      } else {
+        this.close(current)
+      }
+    },
+    close (route) {
+      let res = this.list.filter(item => !routeEqual(route, item))
+      this.$emit('on-close', res, undefined, route)
+    },
+    handleClick (item) {
+      this.$emit('input', item)
+    },
+    showTitleInside (item) {
+      return showTitle(item, this)
+    },
+    isCurrentTag (item) {
+      return routeEqual(this.currentRouteObj, item)
+    },
+    moveToView (tag) {
+      const outerWidth = this.$refs.scrollOuter.offsetWidth
+      const bodyWidth = this.$refs.scrollBody.offsetWidth
+      if (bodyWidth < outerWidth) {
+        this.tagBodyLeft = 0
+      } else if (tag.offsetLeft < -this.tagBodyLeft) {
+        // 标签在可视区域左侧
+        this.tagBodyLeft = -tag.offsetLeft + this.outerPadding
+      } else if (tag.offsetLeft > -this.tagBodyLeft && tag.offsetLeft + tag.offsetWidth < -this.tagBodyLeft + outerWidth) {
+        // 标签在可视区域
+        this.tagBodyLeft = Math.min(0, outerWidth - tag.offsetWidth - tag.offsetLeft - this.outerPadding)
+      } else {
+        // 标签在可视区域右侧
+        this.tagBodyLeft = -(tag.offsetLeft - (outerWidth - this.outerPadding - tag.offsetWidth))
+      }
+    },
+    getTagElementByRoute (route) {
+      this.$nextTick(() => {
+        this.refsTag = this.$refs.tagsPageOpened
+        this.refsTag.forEach((item, index) => {
+          if (routeEqual(route, item.$attrs['data-route-item'])) {
+            let tag = this.refsTag[index].$el
+            this.moveToView(tag)
+          }
+        })
+      })
+    },
+    contextMenu (item, e) {
+      if (item.name === this.$config.homeName) {
+        return
+      }
+      this.visible = true
+      const offsetLeft = this.$el.getBoundingClientRect().left
+      this.contextMenuLeft = e.clientX - offsetLeft + 10
+      this.contextMenuTop = e.clientY - 64
+    },
+    closeMenu () {
+      this.visible = false
+    }
+  },
+  watch: {
+    '$route' (to) {
+      this.getTagElementByRoute(to)
+    },
+    visible (value) {
+      if (value) {
+        document.body.addEventListener('click', this.closeMenu)
+      } else {
+        document.body.removeEventListener('click', this.closeMenu)
+      }
+    }
+  },
+  mounted () {
+    setTimeout(() => {
+      this.getTagElementByRoute(this.$route)
+    }, 200)
+  }
+}
+</script>
+
+<style lang="less">
+@import './tags-nav.less';
+</style>

+ 2 - 0
src/components/main/components/user/index.js

@@ -0,0 +1,2 @@
+import User from './user.vue'
+export default User

+ 12 - 0
src/components/main/components/user/user.less

@@ -0,0 +1,12 @@
+.user{
+  &-avatar-dropdown{
+    cursor: pointer;
+    display: inline-block;
+    // height: 64px;
+    vertical-align: middle;
+    // line-height: 64px;
+    .ivu-badge-dot{
+      top: 16px;
+    }
+  }
+}

+ 63 - 0
src/components/main/components/user/user.vue

@@ -0,0 +1,63 @@
+<template>
+  <div class="user-avatar-dropdown">
+    <Dropdown @on-click="handleClick">
+      <Badge :dot="!!messageUnreadCount">
+        <Avatar :src="userAvatar"/>&nbsp;{{userName}}
+      </Badge>
+      <Icon :size="18" type="md-arrow-dropdown"></Icon>
+      <DropdownMenu slot="list">
+        <!--<DropdownItem name="message">
+          消息中心<Badge style="margin-left: 10px" :count="messageUnreadCount"></Badge>
+        </DropdownItem>-->
+        <DropdownItem name="logout">退出登录</DropdownItem>
+      </DropdownMenu>
+    </Dropdown>
+  </div>
+</template>
+
+<script>
+import './user.less'
+import { mapActions } from 'vuex'
+export default {
+  name: 'User',
+  props: {
+    userAvatar: {
+      type: String,
+      default: ''
+    },
+    userName: {
+      type: String,
+      default: ''
+    },
+    messageUnreadCount: {
+      type: Number,
+      default: 0
+    }
+  },
+  methods: {
+    ...mapActions([
+      'handleLogOut'
+    ]),
+    logout () {
+      this.handleLogOut().then(() => {
+        this.$router.push({
+          name: 'login'
+        })
+      })
+    },
+    message () {
+      this.$router.push({
+        name: 'message_page'
+      })
+    },
+    handleClick (name) {
+      switch (name) {
+        case 'logout': this.logout()
+          break
+        case 'message': this.message()
+          break
+      }
+    }
+  }
+}
+</script>

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff