soft5566 2 years ago
parent
commit
c6ef64ebbc
100 changed files with 3592 additions and 1 deletions
  1. 14 0
      .editorconfig
  2. 5 0
      .env.development
  3. 6 0
      .env.production
  4. 8 0
      .env.staging
  5. 4 0
      .eslintignore
  6. 198 0
      .eslintrc.js
  7. 16 0
      .gitignore
  8. 5 0
      .travis.yml
  9. 21 0
      LICENSE
  10. 1 1
      README.md
  11. 14 0
      babel.config.js
  12. 35 0
      build/index.js
  13. 24 0
      jest.config.js
  14. 9 0
      jsconfig.json
  15. 57 0
      mock/index.js
  16. 81 0
      mock/mock-server.js
  17. 29 0
      mock/table.js
  18. 86 0
      mock/user.js
  19. 25 0
      mock/utils.js
  20. 65 0
      package.json
  21. 8 0
      postcss.config.js
  22. BIN
      public/favicon.ico
  23. 18 0
      public/index.html
  24. 101 0
      src/App.vue
  25. 65 0
      src/api/accountMgr.js
  26. 93 0
      src/api/data.js
  27. 59 0
      src/api/house.js
  28. 16 0
      src/api/systemSet.js
  29. 30 0
      src/api/user.js
  30. BIN
      src/assets/404_images/404.png
  31. BIN
      src/assets/404_images/404_cloud.png
  32. 89 0
      src/components/Breadcrumb/index.vue
  33. 40 0
      src/components/Hamburger/index.vue
  34. 62 0
      src/components/SvgIcon/index.vue
  35. BIN
      src/icons/images/index/title_1.png
  36. BIN
      src/icons/images/index/title_icon.png
  37. BIN
      src/icons/images/index/user.png
  38. BIN
      src/icons/images/index/yewu.png
  39. BIN
      src/icons/images/index/yujing.png
  40. BIN
      src/icons/images/index/zhuangtai.png
  41. BIN
      src/icons/images/index/zoushi.png
  42. BIN
      src/icons/images/login/login_bg.png
  43. BIN
      src/icons/images/login/login_form_bg.png
  44. BIN
      src/icons/images/login/logo_login.png
  45. 9 0
      src/icons/index.js
  46. BIN
      src/icons/serveAC/del_warning.png
  47. 2 0
      src/icons/svg/accountMgr.svg
  48. 2 0
      src/icons/svg/consumptionRecord.svg
  49. 1 0
      src/icons/svg/exit.svg
  50. 1 0
      src/icons/svg/eye-open.svg
  51. 1 0
      src/icons/svg/eye.svg
  52. 2 0
      src/icons/svg/feilvSet.svg
  53. 2 0
      src/icons/svg/home.svg
  54. 1 0
      src/icons/svg/login_home.svg
  55. 1 0
      src/icons/svg/nested.svg
  56. 1 0
      src/icons/svg/operationRecord.svg
  57. 1 0
      src/icons/svg/order.svg
  58. 2 0
      src/icons/svg/panel-home.svg
  59. 1 0
      src/icons/svg/password.svg
  60. 2 0
      src/icons/svg/power.svg
  61. 2 0
      src/icons/svg/rechargeRecord.svg
  62. 2 0
      src/icons/svg/serveAC.svg
  63. 2 0
      src/icons/svg/shuxian.svg
  64. 2 0
      src/icons/svg/systemSet.svg
  65. 1 0
      src/icons/svg/user.svg
  66. 2 0
      src/icons/svg/user_menu.svg
  67. 22 0
      src/icons/svgo.yml
  68. 53 0
      src/js/common.js
  69. 41 0
      src/layout/components/AppMain.vue
  70. 316 0
      src/layout/components/Navbar.vue
  71. 26 0
      src/layout/components/Sidebar/FixiOSBug.js
  72. 41 0
      src/layout/components/Sidebar/Item.vue
  73. 43 0
      src/layout/components/Sidebar/Link.vue
  74. 97 0
      src/layout/components/Sidebar/Logo.vue
  75. 98 0
      src/layout/components/Sidebar/SidebarItem.vue
  76. 56 0
      src/layout/components/Sidebar/index.vue
  77. 3 0
      src/layout/components/index.js
  78. 101 0
      src/layout/index.vue
  79. 45 0
      src/layout/mixin/ResizeHandler.js
  80. 42 0
      src/main.js
  81. 75 0
      src/permission.js
  82. 126 0
      src/router/index.js
  83. 16 0
      src/settings.js
  84. 11 0
      src/store/getters.js
  85. 19 0
      src/store/index.js
  86. 48 0
      src/store/modules/app.js
  87. 32 0
      src/store/modules/settings.js
  88. 178 0
      src/store/modules/user.js
  89. 49 0
      src/styles/element-ui.scss
  90. 65 0
      src/styles/index.scss
  91. 28 0
      src/styles/mixin.scss
  92. 273 0
      src/styles/sidebar.scss
  93. 48 0
      src/styles/transition.scss
  94. 28 0
      src/styles/variables.scss
  95. 29 0
      src/utils/auth.js
  96. 36 0
      src/utils/common.js
  97. 10 0
      src/utils/get-page-title.js
  98. 117 0
      src/utils/index.js
  99. 96 0
      src/utils/request.js
  100. 0 0
      src/utils/validate.js

+ 14 - 0
.editorconfig

@@ -0,0 +1,14 @@
+# http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+insert_final_newline = false
+trim_trailing_whitespace = false

+ 5 - 0
.env.development

@@ -0,0 +1,5 @@
+# just a flag
+ENV = 'development'
+
+# base api
+VUE_APP_BASE_API = ''

+ 6 - 0
.env.production

@@ -0,0 +1,6 @@
+# just a flag
+ENV = 'production'
+
+# base api
+VUE_APP_BASE_API = 'https://chtech.ncjti.edu.cn/hotelReservation/zhotel'
+

+ 8 - 0
.env.staging

@@ -0,0 +1,8 @@
+NODE_ENV = production
+
+# just a flag
+ENV = 'staging'
+
+# base api
+VUE_APP_BASE_API = '/stage-api'
+

+ 4 - 0
.eslintignore

@@ -0,0 +1,4 @@
+build/*.js
+src/assets
+public
+dist

+ 198 - 0
.eslintrc.js

@@ -0,0 +1,198 @@
+module.exports = {
+  root: true,
+  parserOptions: {
+    parser: 'babel-eslint',
+    sourceType: 'module'
+  },
+  env: {
+    browser: true,
+    node: true,
+    es6: true,
+  },
+  extends: ['plugin:vue/recommended', 'eslint:recommended'],
+
+  // add your custom rules here
+  //it is base on https://github.com/vuejs/eslint-config-vue
+  rules: {
+    "vue/max-attributes-per-line": [2, {
+      "singleline": 10,
+      "multiline": {
+        "max": 1,
+        "allowFirstLine": false
+      }
+    }],
+    "vue/singleline-html-element-content-newline": "off",
+    "vue/multiline-html-element-content-newline":"off",
+    "vue/name-property-casing": ["error", "PascalCase"],
+    "vue/no-v-html": "off",
+    'accessor-pairs': 2,
+    'arrow-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'block-spacing': [2, 'always'],
+    'brace-style': [2, '1tbs', {
+      'allowSingleLine': true
+    }],
+    'camelcase': [0, {
+      'properties': 'always'
+    }],
+    'comma-dangle': [2, 'never'],
+    'comma-spacing': [2, {
+      'before': false,
+      'after': true
+    }],
+    'comma-style': [2, 'last'],
+    'constructor-super': 2,
+    'curly': [2, 'multi-line'],
+    'dot-location': [2, 'property'],
+    'eol-last': 2,
+    'eqeqeq': ["error", "always", {"null": "ignore"}],
+    'generator-star-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'handle-callback-err': [2, '^(err|error)$'],
+    'indent': [2, 2, {
+      'SwitchCase': 1
+    }],
+    'jsx-quotes': [2, 'prefer-single'],
+    'key-spacing': [2, {
+      'beforeColon': false,
+      'afterColon': true
+    }],
+    'keyword-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'new-cap': [2, {
+      'newIsCap': true,
+      'capIsNew': false
+    }],
+    'new-parens': 2,
+    'no-array-constructor': 2,
+    'no-caller': 2,
+    'no-console': 'off',
+    'no-class-assign': 2,
+    'no-cond-assign': 2,
+    'no-const-assign': 2,
+    'no-control-regex': 0,
+    'no-delete-var': 2,
+    'no-dupe-args': 2,
+    'no-dupe-class-members': 2,
+    'no-dupe-keys': 2,
+    'no-duplicate-case': 2,
+    'no-empty-character-class': 2,
+    'no-empty-pattern': 2,
+    'no-eval': 2,
+    'no-ex-assign': 2,
+    'no-extend-native': 2,
+    'no-extra-bind': 2,
+    'no-extra-boolean-cast': 2,
+    'no-extra-parens': [2, 'functions'],
+    'no-fallthrough': 2,
+    'no-floating-decimal': 2,
+    'no-func-assign': 2,
+    'no-implied-eval': 2,
+    'no-inner-declarations': [2, 'functions'],
+    'no-invalid-regexp': 2,
+    'no-irregular-whitespace': 2,
+    'no-iterator': 2,
+    'no-label-var': 2,
+    'no-labels': [2, {
+      'allowLoop': false,
+      'allowSwitch': false
+    }],
+    'no-lone-blocks': 2,
+    'no-mixed-spaces-and-tabs': 2,
+    'no-multi-spaces': 2,
+    'no-multi-str': 2,
+    'no-multiple-empty-lines': [2, {
+      'max': 1
+    }],
+    'no-native-reassign': 2,
+    'no-negated-in-lhs': 2,
+    'no-new-object': 2,
+    'no-new-require': 2,
+    'no-new-symbol': 2,
+    'no-new-wrappers': 2,
+    'no-obj-calls': 2,
+    'no-octal': 2,
+    'no-octal-escape': 2,
+    'no-path-concat': 2,
+    'no-proto': 2,
+    'no-redeclare': 2,
+    'no-regex-spaces': 2,
+    'no-return-assign': [2, 'except-parens'],
+    'no-self-assign': 2,
+    'no-self-compare': 2,
+    'no-sequences': 2,
+    'no-shadow-restricted-names': 2,
+    'no-spaced-func': 2,
+    'no-sparse-arrays': 2,
+    'no-this-before-super': 2,
+    'no-throw-literal': 2,
+    'no-trailing-spaces': 2,
+    'no-undef': 2,
+    'no-undef-init': 2,
+    'no-unexpected-multiline': 2,
+    'no-unmodified-loop-condition': 2,
+    'no-unneeded-ternary': [2, {
+      'defaultAssignment': false
+    }],
+    'no-unreachable': 2,
+    'no-unsafe-finally': 2,
+    'no-unused-vars': [2, {
+      'vars': 'all',
+      'args': 'none'
+    }],
+    'no-useless-call': 2,
+    'no-useless-computed-key': 2,
+    'no-useless-constructor': 2,
+    'no-useless-escape': 0,
+    'no-whitespace-before-property': 2,
+    'no-with': 2,
+    'one-var': [2, {
+      'initialized': 'never'
+    }],
+    'operator-linebreak': [2, 'after', {
+      'overrides': {
+        '?': 'before',
+        ':': 'before'
+      }
+    }],
+    'padded-blocks': [2, 'never'],
+    'quotes': [2, 'single', {
+      'avoidEscape': true,
+      'allowTemplateLiterals': true
+    }],
+    'semi': [2, 'never'],
+    'semi-spacing': [2, {
+      'before': false,
+      'after': true
+    }],
+    'space-before-blocks': [2, 'always'],
+    'space-before-function-paren': [2, 'never'],
+    'space-in-parens': [2, 'never'],
+    'space-infix-ops': 2,
+    'space-unary-ops': [2, {
+      'words': true,
+      'nonwords': false
+    }],
+    'spaced-comment': [2, 'always', {
+      'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
+    }],
+    'template-curly-spacing': [2, 'never'],
+    'use-isnan': 2,
+    'valid-typeof': 2,
+    'wrap-iife': [2, 'any'],
+    'yield-star-spacing': [2, 'both'],
+    'yoda': [2, 'never'],
+    'prefer-const': 2,
+    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
+    'object-curly-spacing': [2, 'always', {
+      objectsInObjects: false
+    }],
+    'array-bracket-spacing': [2, 'never']
+  }
+}

+ 16 - 0
.gitignore

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

+ 5 - 0
.travis.yml

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

+ 21 - 0
LICENSE

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

+ 1 - 1
README.md

@@ -1 +1 @@
-#Homestay_manage_owner
+#airConditionerWebManager

+ 14 - 0
babel.config.js

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

+ 35 - 0
build/index.js

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

+ 24 - 0
jest.config.js

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

+ 9 - 0
jsconfig.json

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

+ 57 - 0
mock/index.js

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

+ 81 - 0
mock/mock-server.js

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

+ 29 - 0
mock/table.js

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

+ 86 - 0
mock/user.js

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

+ 25 - 0
mock/utils.js

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

+ 65 - 0
package.json

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

+ 8 - 0
postcss.config.js

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

BIN
public/favicon.ico


+ 18 - 0
public/index.html

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

+ 101 - 0
src/App.vue

@@ -0,0 +1,101 @@
+<template>
+	<div id="app">
+		<router-view />
+	</div>
+</template>
+
+<script>
+	let timmer = null //超时退出 定时器 必须放到全局,解决vue数据同步导致无法清除定时器
+	export default {
+		name: 'App',
+		data() {
+			return {
+				lastTime: null, //超时退出 最后时间
+			}
+		},
+		created() {
+			// 在页面加载时读取sessionStorage里的状态信息
+			if (sessionStorage.getItem('store')) {
+				this.$store.replaceState(
+					Object.assign({},
+						this.$store.state,
+						JSON.parse(sessionStorage.getItem('store'))
+					)
+				)
+			}
+			// 在页面刷新时将vuex里的信息保存到sessionStorage里
+			// beforeunload事件在页面刷新时先触发
+			window.addEventListener('beforeunload', () => {
+				sessionStorage.setItem('store', JSON.stringify(this.$store.state))
+			});
+			// // 超时退出 start
+			// this.lastTime = new Date();
+
+			// window.addEventListener('resize', () => {
+			// 	if (timmer) {
+			// 		clearTimeout(timmer);
+			// 	}
+			// 	timmer = setTimeout(() => {
+			// 		if (this.$route.path != '/login') {
+			// 			this.currentTime();
+			// 		}
+			// 	}, 1000)
+			// })
+
+			// window.addEventListener('click', () => {
+			// 	if (timmer) {
+			// 		clearTimeout(timmer);
+			// 	}
+			// 	timmer = setTimeout(() => {
+			// 		if (this.$route.path != '/login') {
+			// 			this.currentTime();
+			// 		}
+			// 	}, 1000)
+			// })
+
+			// window.addEventListener('scroll', () => {
+			// 	if (timmer) {
+			// 		clearTimeout(timmer);
+			// 	}
+			// 	timmer = setTimeout(() => {
+			// 		if (this.$route.path != '/login') {
+			// 			this.currentTime();
+			// 		}
+			// 	}, 1000)
+			// }, true)
+
+			// window.addEventListener('mousemove', () => {
+			// 	if (timmer) {
+			// 		clearTimeout(timmer);
+			// 	}
+			// 	timmer = setTimeout(() => {
+			// 		if (this.$route.path != '/login') {
+			// 			this.currentTime();
+			// 		}
+			// 	}, 1000)
+			// }, true)
+			// // 超时退出 end
+		},
+		methods: {
+			currentTime() { // 超时退出     
+				const currentTime = new Date();
+				if (currentTime - this.lastTime > 1000 * 60 * 60 * 6) {
+					this.lastTime = currentTime
+					if (this.$route.path != '/login') {
+						this.$message({
+							message: '登录超时,将返回登录页',
+							type: 'error',
+							duration: 2000
+						})
+						setTimeout(() => {
+							this.$store.dispatch('user/logout')
+							this.$router.push(`/login?redirect=${this.$route.fullPath}`)
+						}, 2000)
+					}
+				} else {
+					this.lastTime = currentTime;
+				}
+			}
+		}
+	}
+</script>

+ 65 - 0
src/api/accountMgr.js

@@ -0,0 +1,65 @@
+import request from '@/utils/request';
+
+// 获取账号列表数据
+export function getTableData(forData) {
+	let data = new FormData();
+	data.append('page', forData.page);
+	data.append('rows', forData.rows);
+  
+	return request({
+		url: '/adminlist.action',
+		method: 'post',
+		data
+	});
+};
+
+// 新增管理员
+export function addAccount(param) {
+	let data = {
+		admin_name: param.username,
+		phone: param.phone,
+		password: param.password,
+		level: param.checkedRole[0] == '超级管理员' ? 2 : 1,
+		user_name: param.fullname,
+		remark: param.comment,
+    admin_power: '1,2,3,4,5,6',
+		// card_number: param.cardNumber
+	};
+  
+	return request({
+		url: '/admininsert.action',
+		method: 'post',
+		data
+	});
+};
+
+// 修改管理员
+export function modifyAccount(param) {
+	let data = {
+		id: param.id,
+		admin_name: param.username,
+		phone: param.phone,
+		password: param.password,
+		user_name: param.fullname,
+		level: param.checkedRole[0] == '超级管理员' ? 2 : 1,
+		remark: param.comment,
+    admin_power: '1,2,3,4,5,6',
+		// card_number: param.cardNumber
+	};
+	return request({
+		url: '/adminupdate.action',
+		method: 'post',
+		data
+	});
+};
+
+// 删除管理员
+export function deleteAccount(param) {
+	let data = new FormData();
+	data.append('id', param);
+	return request({
+		url: '/admindel.action',
+		method: 'post',
+		data
+	});
+};

+ 93 - 0
src/api/data.js

@@ -0,0 +1,93 @@
+import request from '@/utils/request'
+
+// 获取充值记录,表格数据
+export function getTableData(forData) {
+	let data = new FormData()
+	data.append('page', forData.page)
+	data.append('rows', forData.rows)
+	if (typeof forData.order_name_phone != 'undefined') {
+		data.append('order_name_phone', forData.order_name_phone)
+	}
+	if (typeof forData.start_time != 'undefined') {
+		data.append('start_time', forData.start_time)
+	}
+	if (typeof forData.end_time != 'undefined') {
+		data.append('end_time', forData.end_time)
+	}
+	if (typeof forData.status != 'undefined') {
+		data.append('status', forData.status)
+	}
+	return request({
+		url: '/booklist.action',
+		method: 'post',
+		data
+	})
+}
+
+// 核销
+export function collate(forData) {
+	let data = new FormData()
+	data.append('id', forData.id)
+	data.append('status', forData.status)
+	return request({
+		url: '/bookoperate_live.action',
+		method: 'post',
+		data,
+		headers: {
+			'admin_name': 'admin'
+		}
+	})
+}
+
+// 退款
+export function refund(forData) {
+	let data = new FormData()
+	data.append('id', forData.id)
+	data.append('status', forData.status)
+	return request({
+		url: '/bookrefund_money.action',
+		method: 'post',
+		data,
+		headers: {
+			'admin_name': 'admin'
+		}
+	})
+}
+
+// 退房
+export function checkout(forData) {
+	let data = new FormData()
+	data.append('id', forData.id)
+	data.append('status', forData.status)
+	return request({
+		url: '/bookrefund_room.action',
+		method: 'post',
+		data,
+		headers: {
+			'admin_name': 'admin'
+		}
+	})
+}
+
+// 下载
+export function downloadExcel(forData) {
+	let data = new FormData()
+	if (typeof forData.order_name_phone != 'undefined') {
+		data.append('order_name_phone', forData.order_name_phone)
+	}
+	if (typeof forData.start_time != 'undefined') {
+		data.append('start_time', forData.start_time)
+	}
+	if (typeof forData.end_time != 'undefined') {
+		data.append('end_time', forData.end_time)
+	}
+	if (typeof forData.status != 'undefined') {
+		data.append('status', forData.status)
+	}
+	// console.log(forData);
+	return request({
+		url: '/bookto_excel.action',
+		method: 'post',
+		data
+	})
+}

+ 59 - 0
src/api/house.js

@@ -0,0 +1,59 @@
+import request from '@/utils/request'
+
+// 获取充值记录,表格数据
+export function getTableData(forData) {
+	let data = new FormData()
+	data.append('page', forData.page)
+	data.append('rows', forData.rows)
+	if (typeof forData.h_type != 'undefined') {
+		data.append('h_type', forData.h_type)
+	}
+	return request({
+		url: '/houselist.action',
+		method: 'post',
+		data
+	})
+}
+
+// 添加房型
+export function addHouseType(forData) {
+  let data = {
+  	h_type: forData.h_type,
+  	price: forData.price,
+  	number: forData.number
+  };
+
+	return request({
+		url: '/houseinsert.action',
+		method: 'post',
+		data
+	})
+}
+
+// 修改房型
+export function modifyHouseType(forData) {
+  let data = {
+    id: forData.id,
+  	h_type: forData.h_type,
+  	price: forData.price,
+  	number: forData.number
+  };
+
+	return request({
+		url: '/houseupdate.action',
+		method: 'post',
+		data
+	})
+}
+
+// 修改房型
+export function delHouseType(forData) {
+  let data = new FormData()
+  data.append('id', forData.id)
+
+	return request({
+		url: '/housedel.action',
+		method: 'post',
+		data
+	})
+}

+ 16 - 0
src/api/systemSet.js

@@ -0,0 +1,16 @@
+import request from '@/utils/request'
+
+export function updateSet(data) {
+	return request({
+		url: '/configupdate.action',
+		method: 'post',
+		data
+	})
+}
+
+export function getSet() {
+	return request({
+		url: '/configlist.action',
+		method: 'post'
+	})
+}

+ 30 - 0
src/api/user.js

@@ -0,0 +1,30 @@
+import request from '@/utils/request'
+
+export function login(forData) {
+	let data = new FormData()
+	data.append('admin_name', forData.username.trim())
+	data.append('password', forData.password.trim())
+
+	return request({
+		url: '/adminlogin.action',
+		method: 'post',
+		data
+	})
+}
+
+// export function getInfo(token) {
+// 	return request({
+// 		url: '/airManage/info',
+// 		method: 'get',
+// 		params: {
+// 			token
+// 		}
+// 	})
+// }
+
+// export function logout() {
+// 	return request({
+// 		url: '/airManage/adminloginout.action',
+// 		method: 'post'
+// 	})
+// }

BIN
src/assets/404_images/404.png


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


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

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

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

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

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

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

BIN
src/icons/images/index/title_1.png


BIN
src/icons/images/index/title_icon.png


BIN
src/icons/images/index/user.png


BIN
src/icons/images/index/yewu.png


BIN
src/icons/images/index/yujing.png


BIN
src/icons/images/index/zhuangtai.png


BIN
src/icons/images/index/zoushi.png


BIN
src/icons/images/login/login_bg.png


BIN
src/icons/images/login/login_form_bg.png


BIN
src/icons/images/login/logo_login.png


+ 9 - 0
src/icons/index.js

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

BIN
src/icons/serveAC/del_warning.png


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


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


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


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

@@ -0,0 +1 @@
+<svg t="1690795193988" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12873" width="64" height="64"><path d="M512 256c-168 0-329.6 106.4-384 256 54.4 149.6 216 256 384 256 167.2 0 330.4-106.4 384.8-256-55.2-149.6-217.6-256-384.8-256z m0 416c-88 0-160-72-160-160s72-160 160-160 160 72 160 160-72 160-160 160z m96-160c0 52.8-43.2 96-96 96s-96-43.2-96-96 43.2-96 96-96 96 43.2 96 96z" p-id="12874"></path></svg>

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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

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


+ 22 - 0
src/icons/svgo.yml

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

+ 53 - 0
src/js/common.js

@@ -0,0 +1,53 @@
+export function getDateComponents(date, ...components) {
+	const year = date.getFullYear();
+	const month = String(date.getMonth() + 1).padStart(2, '0');
+	const day = String(date.getDate()).padStart(2, '0');
+	const hour = String(date.getHours()).padStart(2, '0');
+	const minute = String(date.getMinutes()).padStart(2, '0');
+	const second = String(date.getSeconds()).padStart(2, '0');
+	const week_cn = ["日", "一", "二", "三", "四", "五", "六"][date.getDay()];
+	const weekday_cn = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"][date.getDay()];
+	const weekday_cnx = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"][date.getDay()];
+	const week_en = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()];
+
+	const result = {};
+
+	for (const component of components) {
+		switch (component.toLowerCase()) {
+			case 'yyyy-mm-dd':
+			case 'date':
+				result['date'] = `${year}-${month}-${day}`;
+				break;
+			case 'yyyy-mm-dd hh:mm:ss':
+			case 'datetime':
+				result['datetime'] = `${year}-${month}-${day} ${hour}:${minute}:${second}`;
+				break;
+			case 'monthday':
+				result['monthday'] = `${month}-${day}`;
+				break;
+			case 'monthday_cn':
+				result['monthday_cn'] = `${month}月-${day}日`;
+				break;
+			case 'hh:mm:ss':
+			case 'time':
+				result['time'] = `${hour}:${minute}:${second}`;
+				break;
+			case 'week_cn':
+				result['week_cn'] = week_cn;
+				break;
+			case 'weekday_cn':
+				result['weekday_cn'] = weekday_cn;
+				break;
+			case 'weekday_cnx':
+				result['weekday_cnx'] = weekday_cnx;
+				break;
+			case 'week_en':
+				result['week_en'] = week_en;
+				break;
+			default:
+				break;
+		}
+	}
+
+	return result;
+}

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

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

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

@@ -0,0 +1,316 @@
+<template>
+	<div class="navbar">
+		<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
+		<div class="title-container-index">
+			<div class="index-top-title">
+				<!-- <img src="../../icons/images/login/logo_login.png" alt=""> -->
+				<div class="desc">靖安乡村民宿管理平台</div>
+			</div>
+
+			<div class="right-menu">
+				<div class="show-time">
+					<div class="d_show">{{date_show}}</div>
+					<div class="t_show">{{time_show}}</div>
+					<div class="w_show">{{week_show}}</div>
+				</div>
+				<span class="svg-shuxian">
+					<svg-icon icon-class="shuxian" />
+				</span>
+				<span class="svg-user">
+					<!-- <svg-icon icon-class="user" /> -->
+					<img src="../../icons/images/index/user.png" alt="">
+				</span>
+				<div class="right-role">{{role}}</div>
+				<span class="svg-shuxian">
+					<svg-icon icon-class="shuxian" />
+				</span>
+				<div class="exit" @click="logout">
+					<span class="svg-exit">
+						<svg-icon icon-class="exit" />
+					</span>
+					<div class="right-exit">退出</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+	import {
+		mapGetters
+	} from 'vuex';
+	// import Breadcrumb from '@/components/Breadcrumb'
+	import Hamburger from '@/components/Hamburger';
+
+	export default {
+		components: {
+			// Breadcrumb,
+			Hamburger
+		},
+		data() {
+			return {
+				role: this.$store.state.user.name,
+				date_show: '',
+				time_show: '',
+				week_show: ''
+			}
+		},
+		created() {
+			// 获取日期-星期
+			setTimeout(() => {
+				this.getdataTime()
+			}, 1000)
+			// 读秒
+			setInterval(() => {
+				this.getnewTime()
+			}, 1000)
+		},
+		updated() {
+			if (typeof this.role == 'undefined' || this.role == '') {
+				this.autoLogout();
+			}
+		},
+		computed: {
+			...mapGetters([
+				'sidebar',
+				'avatar'
+			])
+		},
+		methods: {
+			// 侧边栏 显示 和 隐藏
+			toggleSideBar() {
+				this.$store.dispatch('app/toggleSideBar')
+			},
+			// 无用户名自动退出
+			autoLogout() {
+				console.log(44444444);
+				return
+				this.$store.dispatch('user/logout')
+				this.$router.push(`/login?redirect=${this.$route.fullPath}`)
+				this.$message({
+					type: 'success',
+					message: '无用户名,已自动退出!请重新登录!'
+				});
+			},
+			// 退出登录
+			async logout() {
+				await this.$confirm('即将退出登录, 是否继续?', '提示', {
+					confirmButtonText: '确定',
+					cancelButtonText: '取消',
+					type: 'warning'
+				}).then(() => {
+					this.$store.dispatch('user/logout')
+					this.$router.push(`/login?redirect=${this.$route.fullPath}`)
+					this.$message({
+						type: 'success',
+						message: '已退出成功!'
+					});
+				}).catch(() => {
+					this.$message({
+						type: 'error',
+						message: '已取消退出!'
+					});
+				});
+			},
+			// 获取当前日期时间星期
+			get_datetime() {
+				this.datetime = new Date().toLocaleString() + ' 星期' + '日一二三四五六'.charAt(new Date().getDay())
+			},
+			// 获取当前系统日期
+			getdataTime() {
+				let wk = new Date().getDay()
+				let yy = new Date().getFullYear();
+				let mm = new Date().getMonth() + 1;
+				let dd = new Date().getDate();
+
+				mm = mm < 10 ? '0' + mm : mm
+				dd = dd < 10 ? '0' + dd : dd
+
+				let weeks = ['日', '一', '二', '三', '四', '五', '六']
+				this.week_show = '星期' + weeks[wk]
+				this.date_show = yy + "-" + mm + "-" + dd + " "
+			},
+			// 获取当前系统的时间
+			getnewTime() {
+				let hh = new Date().getHours();
+				let mf = new Date().getMinutes()
+				let ss = new Date().getSeconds()
+
+				mf = mf < 10 ? "0" + mf : mf;
+				ss = ss < 10 ? "0" + ss : ss;
+
+				this.time_show = hh + ":" + mf + ":" + ss;
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.navbar {
+		// height: 50px;
+		height: 90px;
+		overflow: hidden;
+		position: relative;
+		background: #FFFFFF;
+		box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
+
+		.hamburger-container {
+			line-height: 46px;
+			height: 100%;
+			float: left;
+			cursor: pointer;
+			transition: background .3s;
+			-webkit-tap-highlight-color: transparent;
+
+			&:hover {
+				background: rgba(0, 0, 0, .025)
+			}
+		}
+
+		.breadcrumb-container {
+			float: left;
+		}
+
+		.title-container-index {
+			display: flex;
+			justify-content: space-between;
+			height: 90px;
+
+			.index-top-title {
+				display: flex;
+				align-items: center;
+
+				// img {
+				// 	width: 48px;
+				// 	height: 48px;
+				// 	margin-right: 10px;
+				// }
+
+				.desc {
+					font-size: 30px;
+					font-family: Microsoft YaHei;
+					font-weight: bold;
+					color: #46515E;
+				}
+			}
+
+			.right-menu {
+				display: flex;
+				align-items: center;
+				font-size: 22px;
+				padding-right: 20px;
+
+				.show-time {
+					display: flex;
+					align-items: center;
+					font-family: Microsoft YaHei-3970(82674968);
+					color: #53575A;
+
+					.w_show {
+						margin: 0;
+					}
+
+					.t_show {
+						font-weight: bold;
+						margin: 0 10px;
+					}
+				}
+
+				.svg-shuxian {
+					font-size: 40px;
+					color: #808080;
+				}
+
+				.svg-user {
+					display: flex;
+					justify-content: center;
+					align-items: center;
+					width: 50px;
+					height: 50px;
+					border-radius: 30px;
+					font-size: 32px;
+					background-color: #E0E9FD;
+					color: #4493F8;
+					overflow: hidden;
+					
+					img {
+						width: 50px;
+						height: 50px;
+					}
+				}
+
+				.right-role {
+					margin: auto 0 auto 10px;
+				}
+
+				.exit {
+					display: flex;
+					align-items: center;
+					cursor: pointer;
+				}
+
+				.svg-exit {
+					font-size: 26px;
+					margin-right: 10px;
+				}
+			}
+
+
+			// .right-menu {
+			// 	display: flex;
+			// 	height: 100%;
+			// 	line-height: 90px;
+			// 	margin-right: 20px;
+
+			// 	&:focus {
+			// 		outline: none;
+			// 	}
+
+			// 	.right-menu-item {
+			// 		display: inline-block;
+			// 		padding: 0 8px;
+			// 		height: 100%;
+			// 		font-size: 18px;
+			// 		color: #5a5e66;
+			// 		vertical-align: text-bottom;
+
+			// 		&.hover-effect {
+			// 			cursor: pointer;
+			// 			transition: background .3s;
+
+			// 			&:hover {
+			// 				background: rgba(0, 0, 0, .025)
+			// 			}
+			// 		}
+			// 	}
+
+			// 	.avatar-container {
+			// 		margin-right: 30px;
+
+			// 		.avatar-wrapper {
+			// 			display: flex;
+			// 			align-items: center;
+			// 			height: 90px;
+			// 			margin-top: 5px;
+			// 			position: relative;
+
+			// 			.user-avatar {
+			// 				cursor: pointer;
+			// 				width: 40px;
+			// 				height: 40px;
+			// 				border-radius: 10px;
+			// 			}
+
+			// 			.el-icon-caret-bottom {
+			// 				cursor: pointer;
+			// 				position: absolute;
+			// 				right: -20px;
+			// 				top: 35px;
+			// 				font-size: 14px;
+			// 			}
+			// 		}
+			// 	}
+			// }
+		}
+	}
+</style>

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

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

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

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

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

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

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

@@ -0,0 +1,97 @@
+<template>
+	<div class="sidebar-logo-container" :class="{'collapse':collapse}">
+		<transition name="sidebarLogoFade">
+			<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
+				<img src="../../../icons/images/index/title_icon.png" class="logo" alt="">
+				<img v-if="logo" :src="logo" class="sidebar-logo">
+				<h1 v-else class="sidebar-title">{{ title }} </h1>
+			</router-link>
+			<router-link v-else key="expand" class="sidebar-logo-link" to="/">
+				<img src="../../../icons/images/index/title_1.png" class="logo" alt="">
+				<!-- <img v-if="logo" :src="logo" class="sidebar-logo">
+				<h1 class="sidebar-title">{{ title }} </h1> -->
+			</router-link>
+		</transition>
+	</div>
+</template>
+
+<script>
+	export default {
+		name: 'SidebarLogo',
+		props: {
+			collapse: {
+				type: Boolean,
+				required: true
+			}
+		},
+		data() {
+			return {
+				title: '共享空调运营管理平台',
+				// logo: '../../../icons/images/index/title_icon.png'
+				logo: ''
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.sidebarLogoFade-enter-active {
+		transition: opacity 1.5s;
+	}
+
+	.sidebarLogoFade-enter,
+	.sidebarLogoFade-leave-to {
+		opacity: 0;
+	}
+
+	.sidebar-logo-container {
+		position: relative;
+		width: 100%;
+		height: 90px;
+		line-height: 90px;
+		// background: #2b2f3a;
+		text-align: center;
+		overflow: hidden;
+
+		& .sidebar-logo-link {
+			height: 100%;
+			width: 100%;
+
+			.logo {
+				width: 48px;
+				height: 48px;
+				vertical-align: middle;
+			}
+
+			& .sidebar-logo {
+				width: 48px;
+				height: 48px;
+				vertical-align: middle;
+				margin-right: 12px;
+			}
+
+			& .sidebar-title {
+				display: inline-block;
+				margin: 0;
+				color: #fff;
+				font-weight: 600;
+				line-height: 50px;
+				font-size: 14px;
+				font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
+				vertical-align: middle;
+			}
+		}
+
+		&.collapse {
+			.sidebar-logo {
+				margin-right: 0px;
+			}
+
+			.logo {
+				width: 32px;
+				height: 32px;
+				vertical-align: middle;
+			}
+		}
+	}
+</style>

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

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

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

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

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

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

+ 101 - 0
src/layout/index.vue

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

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

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

+ 42 - 0
src/main.js

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

+ 75 - 0
src/permission.js

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

+ 126 - 0
src/router/index.js

@@ -0,0 +1,126 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+
+Vue.use(Router)
+
+/* Layout */
+import Layout from '@/layout'
+
+/**
+ * Note: sub-menu only appear when route children.length >= 1
+ * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
+ *
+ * hidden: true                   if set true, item will not show in the sidebar(default is false)
+ * alwaysShow: true               if set true, will always show the root menu
+ *                                if not set alwaysShow, when item has more than one children route,
+ *                                it will becomes nested mode, otherwise not show the root menu
+ * redirect: noRedirect           if set noRedirect will no redirect in the breadcrumb
+ * name:'router-name'             the name is used by <keep-alive> (must set!!!)
+ * meta : {
+    roles: ['admin','editor']    control the page roles (you can set multiple roles)
+    title: 'title'               the name show in sidebar and breadcrumb (recommend set)
+    icon: 'svg-name'/'el-icon-x' the icon show in the sidebar
+    breadcrumb: false            if set false, the item will hidden in breadcrumb(default is true)
+    activeMenu: '/example/list'  if set path, the sidebar will highlight the path you set
+  }
+ */
+
+/**
+ * constantRoutes
+ * a base page that does not have permission requirements
+ * all roles can be accessed
+ */
+export const constantRoutes = [{
+		path: '/login',
+		component: () => import('@/views/login/index'),
+		hidden: true
+	},
+
+	{
+		path: '/404',
+		component: () => import('@/views/404'),
+		hidden: true
+	},
+
+	{
+		path: '/',
+		component: Layout,
+		redirect: '/data',
+		children: [{
+			path: 'data',
+			name: 'Data',
+			component: () => import('@/views/data/index'),
+			meta: {
+				title: '数据中台',
+				icon: 'order'
+			}
+		}]
+	},
+	
+	{
+		path: '/stdbookMgr',
+		component: Layout,
+		redirect: '/stdbookMgr',
+		children: [{
+			path: '',
+			name: 'StdbookMgr',
+			component: () => import('@/views/stdbookMgr/index'),
+			meta: {
+				title: '台账管理',
+				icon: 'order'
+			}
+		}]
+	},
+
+	{
+		path: '/systemset',
+		component: Layout,
+		// redirect: '/systemset',
+		name: 'Systemset',
+		meta: {
+			title: '系统设置',
+			icon: 'systemSet'
+		},
+		// component: () => import('@/views/systemSet/index'),
+		children: [{
+			path: 'accountMgr',
+			name: 'AccountMgr',
+			component: () => import('@/views/accountMgr/index'),
+			meta: {
+				title: '账号管理'
+			}
+		}, {
+			path: 'Residentialhostel',
+			name: 'Residentialhostel',
+			component: () => import('@/views/residentialhostelMgr/index'),
+			meta: {
+				title: '民宿管理'
+			}
+		}]
+	},
+	
+	// 404页面 必须放在最后!!
+	{
+		path: '*',
+		redirect: '/404',
+		hidden: true
+	}
+]
+
+const createRouter = () => new Router({
+	mode: 'hash', // require service support
+	scrollBehavior: () => ({
+		y: 0
+	}),
+	routes: constantRoutes
+})
+
+const router = createRouter()
+
+// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
+export function resetRouter() {
+	const newRouter = createRouter()
+	router.matcher = newRouter.matcher // reset router
+}
+
+export default router

+ 16 - 0
src/settings.js

@@ -0,0 +1,16 @@
+module.exports = {
+
+  title: '靖安乡村民宿管理平台',
+
+  /**
+   * @type {boolean} true | false
+   * @description Whether fix the header
+   */
+  fixedHeader: true,
+
+  /**
+   * @type {boolean} true | false
+   * @description Whether show the logo in sidebar
+   */
+  sidebarLogo: true
+}

+ 11 - 0
src/store/getters.js

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

+ 19 - 0
src/store/index.js

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

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

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

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

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

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

@@ -0,0 +1,178 @@
+import {
+	login,
+	logout,
+	getInfo
+} from '@/api/user'
+import {
+	getToken,
+	setToken,
+	removeToken,
+	setName,
+	setLevel
+} from '@/utils/auth'
+import {
+	resetRouter
+} from '@/router'
+
+const getDefaultState = () => {
+	return {
+		token: getToken(),
+		name: '',
+		level: '',
+		avatar: '',
+		user_name: '',
+		phone: ''
+	}
+}
+
+const state = getDefaultState()
+
+const mutations = {
+	RESET_STATE: (state) => {
+		Object.assign(state, getDefaultState())
+	},
+	SET_TOKEN: (state, token) => {
+		state.token = token
+	},
+	SET_NAME: (state, name) => {
+		state.name = name
+	},
+	SET_PHONE: (state, phone) => {
+		state.phone = phone
+	},
+	SET_USER_NAME: (state, user_name) => {
+		state.user_name = user_name
+	},
+	SET_PWD: (state, pwd) => {
+		state.pwd = pwd
+	},
+	SET_LEVEL: (state, level) => {
+		state.level = level
+	},
+	SET_AVATAR: (state, avatar) => {
+		state.avatar = avatar
+	}
+}
+
+const actions = {
+	// user login
+	login({
+		commit
+	}, userInfo) {
+		const {
+			username,
+			password
+		} = userInfo
+		return new Promise((resolve, reject) => {
+			login({
+				username: username.trim(),
+				password: password.trim()
+			}).then(response => {
+				if (response.code == 200) {
+					// 修改:token固定
+					const token = 'admin_token'
+					commit('SET_TOKEN', token)
+					setToken(token)
+					setName(response.data[0].admin_name)
+					setLevel(response.data[0].level)
+					// 添加:
+					commit('SET_LEVEL', response.data[0].level)
+					commit('SET_NAME', response.data[0].admin_name)
+					commit('SET_PWD', password)
+					commit('SET_PHONE', response.data[0].phone)
+					commit('SET_USER_NAME', response.data[0].user_name)
+				} else {
+					commit('SET_TOKEN', '')
+					commit('SET_LEVEL', '')
+					commit('SET_NAME', '')
+					commit('SET_PWD', '')
+					commit('SET_PHONE', '')
+					commit('SET_USER_NAME', '')
+				}
+				// commit('SET_TOKEN', data.token)
+				// setToken(data.token)
+				resolve(response)
+			}).catch(error => {
+				reject(error)
+			})
+		})
+	},
+
+	// get user info
+	// 修改:获取用户信息被注释了
+	// getInfo({
+	// 	commit,
+	// 	state
+	// }) {
+	// 	return new Promise((resolve, reject) => {
+	// 		getInfo(state.token).then(response => {
+	// 			const {
+	// 				data
+	// 			} = response
+
+	// 			if (!data) {
+	// 				return reject('验证失败,请重新登录。')
+	// 			}
+
+	// 			const {
+	// 				name,
+	// 				avatar
+	// 			} = data
+
+	// 			commit('SET_NAME', name)
+	// 			commit('SET_AVATAR', avatar)
+	// 			resolve(data)
+	// 		}).catch(error => {
+	// 			reject(error)
+	// 		})
+	// 	})
+	// },
+
+	// user logout
+	logout({
+		commit,
+		state
+	}) {
+		return new Promise((resolve, reject) => {
+			removeToken() // must remove  token  first
+			resetRouter()
+			commit('RESET_STATE')
+			commit('SET_NAME', '')
+			commit('SET_PWD', '')
+			commit('SET_LEVEL', '')
+			commit('SET_TOKEN', '')
+			commit('SET_PHONE', '')
+			commit('SET_USER_NAME', '')
+			resolve()
+		})
+		// 修改:下面被注释了
+		// return new Promise((resolve, reject) => {
+		//   logout(state.token).then(() => {
+		//     removeToken() // must remove  token  first
+		//     resetRouter()
+		//     commit('RESET_STATE')
+		//     resolve()
+		//   }).catch(error => {
+		//     reject(error)
+		//   })
+		// })
+	},
+
+	// remove token
+	resetToken({
+		commit
+	}) {
+		return new Promise(resolve => {
+			removeToken() // must remove  token  first
+			commit('RESET_STATE')
+			resolve()
+		})
+	}
+}
+
+export default {
+	namespaced: true,
+	state,
+	mutations,
+	actions
+}

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

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

+ 65 - 0
src/styles/index.scss

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

+ 28 - 0
src/styles/mixin.scss

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

+ 273 - 0
src/styles/sidebar.scss

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

+ 48 - 0
src/styles/transition.scss

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

+ 28 - 0
src/styles/variables.scss

@@ -0,0 +1,28 @@
+// sidebar
+$menuText: #FFFFFF;  // 默认文本颜色
+$menuActiveText: rgba(9, 101, 98, 1);  // 当前激活主菜单文本颜色
+$subMenuActiveText: rgba(9, 101, 98, 1); // 当前激活子菜单文本颜色
+
+$menuBg: rgba(9, 101, 98, 1);// 默认背景
+$menuHover: #FFFFFF; // 主菜单悬浮背景
+
+
+$subMenuBg: rgba(9, 101, 98, 1); // 默认子菜单背景
+$subMenuHover: #FFFFFF; // 子菜单悬浮背景
+
+
+$sideBarWidth: 260px;
+
+
+// the :export directive is the magic sauce for webpack
+// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
+:export {
+  menuText: $menuText;
+  menuActiveText: $menuActiveText;
+  subMenuActiveText: $subMenuActiveText;
+  menuBg: $menuBg;
+  menuHover: $menuHover;
+  subMenuBg: $subMenuBg;
+  subMenuHover: $subMenuHover;
+  sideBarWidth: $sideBarWidth;
+}

+ 29 - 0
src/utils/auth.js

@@ -0,0 +1,29 @@
+import Cookies from 'js-cookie'
+
+const TokenKey = 'admin_token'
+const AdminName = 'admin_name'
+const AdminLevel = 'admin_level'
+
+export function getToken() {
+  return Cookies.get(TokenKey)
+}
+
+export function setToken(token) {
+  return Cookies.set(TokenKey, token)
+}
+
+export function removeToken() {
+  return Cookies.remove(TokenKey)
+}
+
+export function setName(name) {
+  return Cookies.set(AdminName, name)
+}
+
+export function setLevel(level) {
+  return Cookies.set(AdminLevel, level)
+}
+
+export function getName() {
+	return Cookies.get(AdminName)
+}

+ 36 - 0
src/utils/common.js

@@ -0,0 +1,36 @@
+/**
+ * 获取当前时间,格式YYYY-MM-DD hh:mm:ss
+ */
+export function getFormatDateTime(dateOrTimeOrDatetime) {
+	var date = new Date();
+	var d = '';
+	var t = '';
+
+	if (dateOrTimeOrDatetime == "date" || dateOrTimeOrDatetime == "datetime") {
+		var seperator1 = "-";
+		var year = date.getFullYear();
+		var month = date.getMonth() + 1;
+		var day = date.getDate();
+		month = month < 9 ? "0" + month : month;
+		day = day < 9 ? "0" + day : day;
+
+		d = year + seperator1 + month + seperator1 + day;
+	}
+
+	if (dateOrTimeOrDatetime == 'time' || dateOrTimeOrDatetime == "datetime") {
+		var seperator2 = ":";
+		var hours = date.getHours();
+		var minutes = date.getMinutes();
+		var seconds = date.getSeconds();
+
+		hours = hours < 9 ? "0" + hours : hours;
+		minutes = minutes < 9 ? "0" + minutes : minutes;
+		seconds = seconds < 9 ? "0" + seconds : seconds;
+
+		t = hours + seperator2 + minutes + seperator2 + seconds;
+	}
+
+	var result = (d + " " + t).trim();
+
+	return result;
+}

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

@@ -0,0 +1,10 @@
+import defaultSettings from '@/settings'
+
+const title = defaultSettings.title || '靖安乡村民宿管理平台'
+
+export default function getPageTitle(pageTitle) {
+  if (pageTitle) {
+    return `${pageTitle} - ${title}`
+  }
+  return `${title}`
+}

+ 117 - 0
src/utils/index.js

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

+ 96 - 0
src/utils/request.js

@@ -0,0 +1,96 @@
+import axios from 'axios'
+import {
+	MessageBox,
+	Message
+} from 'element-ui'
+import store from '@/store'
+import {
+	getToken,
+	getName
+} from '@/utils/auth'
+
+// 创建axios实例
+const service = axios.create({
+	baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
+	// withCredentials: true, // 跨域请求时发送cookie
+	timeout: 20000 // 请求超时
+})
+
+// 请求拦截器
+service.interceptors.request.use(
+	config => {
+		// 在发送请求之前做些什么
+		if (store.getters.token) {
+			// 让每个请求携带令牌
+			// ['X-Token'] 是否为自定义标题键
+			// 请根据实际情况进行修改
+			config.headers['X-Token'] = getToken()
+			config.headers['Admin-Name'] = getName()
+		}
+		return config
+	},
+	error => {
+		// 处理请求错误
+		// console.log(error) // 调试命令
+		return Promise.reject(error)
+	}
+)
+
+// 响应拦截器
+service.interceptors.response.use(
+	/**
+	 * 如果您想要获取http信息,如header或状态
+	 * 请返回  response => response
+	 */
+
+	/**
+	 * 通过自定义代码确定请求状态
+	 * 这只是一个例子
+	 * 您也可以通过“HTTP状态码”来判断状态
+	 */
+	response => {
+		const res = response.data
+
+		// 如果定制代码不是20000,则判定为错误。
+		if (res.code !== 200) {
+			// 返回格式不规范,也只能如此了
+			return res
+
+			Message({
+				message: res.message || '返回信息中无code,无法解析数据',
+				type: 'error',
+				duration: 5 * 1000
+			})
+
+			// 50008: 非法的令牌; 50012: 其他客户端已登录; 50014: 过期令牌;
+			if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
+				// 重新登陆
+				MessageBox.confirm('您已注销,您可以取消留在此页面,或重新登录',
+					'确认注销', {
+						confirmButtonText: '重新登入',
+						cancelButtonText: '取消',
+						type: 'warning'
+					}).then(() => {
+					store.dispatch('user/resetToken').then(() => {
+						location.reload()
+					})
+				})
+			}
+
+			return Promise.reject(new Error(res.message || '未知错误'))
+		} else {
+			return res
+		}
+	},
+	error => {
+		// console.log('错误:' + error) // 用于调试
+		// Message({
+		// 	message: error.message,
+		// 	type: 'error',
+		// 	duration: 5 * 1000
+		// })
+		return Promise.reject(error)
+	}
+)
+
+export default service

+ 0 - 0
src/utils/validate.js


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