Procházet zdrojové kódy

Merge branch 'master' of http://47.100.37.243:10080/wutt/lsappBms

wutt před 5 roky
rodič
revize
316ee035b7
100 změnil soubory, kde provedl 7855 přidání a 0 odebrání
  1. 14 0
      lss_frontend/.editorconfig
  2. 14 0
      lss_frontend/.env.development
  3. 6 0
      lss_frontend/.env.production
  4. 8 0
      lss_frontend/.env.staging
  5. 4 0
      lss_frontend/.eslintignore
  6. 198 0
      lss_frontend/.eslintrc.js
  7. 16 0
      lss_frontend/.gitignore
  8. 5 0
      lss_frontend/.travis.yml
  9. 21 0
      lss_frontend/LICENSE
  10. 98 0
      lss_frontend/README-zh.md
  11. 91 0
      lss_frontend/README.md
  12. 5 0
      lss_frontend/babel.config.js
  13. 35 0
      lss_frontend/build/index.js
  14. 24 0
      lss_frontend/jest.config.js
  15. 9 0
      lss_frontend/jsconfig.json
  16. 66 0
      lss_frontend/mock/index.js
  17. 68 0
      lss_frontend/mock/mock-server.js
  18. 29 0
      lss_frontend/mock/table.js
  19. 84 0
      lss_frontend/mock/user.js
  20. 64 0
      lss_frontend/package.json
  21. 5 0
      lss_frontend/postcss.config.js
  22. binární
      lss_frontend/public/cloud.ico
  23. binární
      lss_frontend/public/favicon.ico
  24. 17 0
      lss_frontend/public/index.html
  25. binární
      lss_frontend/public/lishui.ico
  26. 11 0
      lss_frontend/src/App.vue
  27. 9 0
      lss_frontend/src/api/table.js
  28. 24 0
      lss_frontend/src/api/user.js
  29. binární
      lss_frontend/src/assets/404_images/404.png
  30. binární
      lss_frontend/src/assets/404_images/404_cloud.png
  31. binární
      lss_frontend/src/assets/defaultUser.png
  32. binární
      lss_frontend/src/assets/logo.png
  33. 78 0
      lss_frontend/src/components/Breadcrumb/index.vue
  34. 37 0
      lss_frontend/src/components/Hamburger/index.vue
  35. 62 0
      lss_frontend/src/components/SvgIcon/index.vue
  36. 9 0
      lss_frontend/src/icons/index.js
  37. 1 0
      lss_frontend/src/icons/svg/dashboard.svg
  38. 1 0
      lss_frontend/src/icons/svg/example.svg
  39. 1 0
      lss_frontend/src/icons/svg/eye-open.svg
  40. 1 0
      lss_frontend/src/icons/svg/eye.svg
  41. 1 0
      lss_frontend/src/icons/svg/form.svg
  42. 1 0
      lss_frontend/src/icons/svg/link.svg
  43. 1 0
      lss_frontend/src/icons/svg/nested.svg
  44. 1 0
      lss_frontend/src/icons/svg/password.svg
  45. 1 0
      lss_frontend/src/icons/svg/table.svg
  46. 1 0
      lss_frontend/src/icons/svg/tree.svg
  47. 1 0
      lss_frontend/src/icons/svg/user.svg
  48. 22 0
      lss_frontend/src/icons/svgo.yml
  49. 40 0
      lss_frontend/src/layout/components/AppMain.vue
  50. 131 0
      lss_frontend/src/layout/components/Navbar.vue
  51. 26 0
      lss_frontend/src/layout/components/Sidebar/FixiOSBug.js
  52. 29 0
      lss_frontend/src/layout/components/Sidebar/Item.vue
  53. 36 0
      lss_frontend/src/layout/components/Sidebar/Link.vue
  54. 85 0
      lss_frontend/src/layout/components/Sidebar/Logo.vue
  55. 95 0
      lss_frontend/src/layout/components/Sidebar/SidebarItem.vue
  56. 56 0
      lss_frontend/src/layout/components/Sidebar/index.vue
  57. 3 0
      lss_frontend/src/layout/components/index.js
  58. 93 0
      lss_frontend/src/layout/index.vue
  59. 45 0
      lss_frontend/src/layout/mixin/ResizeHandler.js
  60. 30 0
      lss_frontend/src/main.js
  61. 56 0
      lss_frontend/src/permission.js
  62. 239 0
      lss_frontend/src/router/index.js
  63. 16 0
      lss_frontend/src/settings.js
  64. 9 0
      lss_frontend/src/store/getters.js
  65. 21 0
      lss_frontend/src/store/index.js
  66. 48 0
      lss_frontend/src/store/modules/app.js
  67. 80 0
      lss_frontend/src/store/modules/permission.js
  68. 31 0
      lss_frontend/src/store/modules/settings.js
  69. 90 0
      lss_frontend/src/store/modules/user.js
  70. 49 0
      lss_frontend/src/styles/element-ui.scss
  71. 65 0
      lss_frontend/src/styles/index.scss
  72. 28 0
      lss_frontend/src/styles/mixin.scss
  73. 209 0
      lss_frontend/src/styles/sidebar.scss
  74. 48 0
      lss_frontend/src/styles/transition.scss
  75. 25 0
      lss_frontend/src/styles/variables.scss
  76. 13 0
      lss_frontend/src/utils/auth.js
  77. 10 0
      lss_frontend/src/utils/get-page-title.js
  78. 105 0
      lss_frontend/src/utils/index.js
  79. 55 0
      lss_frontend/src/utils/request.js
  80. 20 0
      lss_frontend/src/utils/validate.js
  81. 250 0
      lss_frontend/src/views/404.vue
  82. 661 0
      lss_frontend/src/views/assets/assetDetail.vue
  83. 664 0
      lss_frontend/src/views/assets/assetManagement.vue
  84. 206 0
      lss_frontend/src/views/assets/assetRecord.vue
  85. 238 0
      lss_frontend/src/views/assets/assetStatistic.vue
  86. 240 0
      lss_frontend/src/views/crew/crewDetail.vue
  87. 202 0
      lss_frontend/src/views/crew/crewList.vue
  88. 82 0
      lss_frontend/src/views/crew/record.vue
  89. 118 0
      lss_frontend/src/views/crew/score.vue
  90. 203 0
      lss_frontend/src/views/login.vue
  91. 16 0
      lss_frontend/src/views/starter.vue
  92. 222 0
      lss_frontend/src/views/system/institution.vue
  93. 323 0
      lss_frontend/src/views/system/institutionDetail.vue
  94. 19 0
      lss_frontend/src/views/system/log.vue
  95. 315 0
      lss_frontend/src/views/system/permission.vue
  96. 474 0
      lss_frontend/src/views/task/taskDetail.vue
  97. 489 0
      lss_frontend/src/views/task/taskList.vue
  98. 5 0
      lss_frontend/tests/unit/.eslintrc.js
  99. 98 0
      lss_frontend/tests/unit/components/Breadcrumb.spec.js
  100. 0 0
      lss_frontend/tests/unit/components/Hamburger.spec.js

+ 14 - 0
lss_frontend/.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

+ 14 - 0
lss_frontend/.env.development

@@ -0,0 +1,14 @@
+# just a flag
+ENV = 'development'
+
+# base api
+VUE_APP_BASE_API = '/dev-api'
+
+# vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable,
+# to control whether the babel-plugin-dynamic-import-node plugin is enabled.
+# It only does one thing by converting all import() to require().
+# This configuration can significantly increase the speed of hot updates,
+# when you have a large number of pages.
+# Detail:  https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js
+
+VUE_CLI_BABEL_TRANSPILE_MODULES = true

+ 6 - 0
lss_frontend/.env.production

@@ -0,0 +1,6 @@
+# just a flag
+ENV = 'production'
+
+# base api
+VUE_APP_BASE_API = '/prod-api'
+

+ 8 - 0
lss_frontend/.env.staging

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

+ 4 - 0
lss_frontend/.eslintignore

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

+ 198 - 0
lss_frontend/.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
lss_frontend/.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
lss_frontend/.travis.yml

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

+ 21 - 0
lss_frontend/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.

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 98 - 0
lss_frontend/README-zh.md


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 91 - 0
lss_frontend/README.md


+ 5 - 0
lss_frontend/babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/app'
+  ]
+}

+ 35 - 0
lss_frontend/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
lss_frontend/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
lss_frontend/jsconfig.json

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

+ 66 - 0
lss_frontend/mock/index.js

@@ -0,0 +1,66 @@
+import Mock from 'mockjs'
+import { param2Obj } from '../src/utils'
+
+import user from './user'
+import table from './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).
+export 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))
+  }
+}
+
+// for mock server
+const responseFake = (url, type, respond) => {
+  return {
+    url: new RegExp(`/mock${url}`),
+    type: type || 'get',
+    response(req, res) {
+      res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
+    }
+  }
+}
+
+export default mocks.map(route => {
+  return responseFake(route.url, route.type, route.response)
+})

+ 68 - 0
lss_frontend/mock/mock-server.js

@@ -0,0 +1,68 @@
+const chokidar = require('chokidar')
+const bodyParser = require('body-parser')
+const chalk = require('chalk')
+const path = require('path')
+
+const mockDir = path.join(process.cwd(), 'mock')
+
+function registerRoutes(app) {
+  let mockLastIndex
+  const { default: mocks } = require('./index.js')
+  for (const mock of mocks) {
+    app[mock.type](mock.url, mock.response)
+    mockLastIndex = app._router.stack.length
+  }
+  const mockRoutesLength = Object.keys(mocks).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)]
+    }
+  })
+}
+
+module.exports = app => {
+  // es6 polyfill
+  require('@babel/register')
+
+  // 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
lss_frontend/mock/table.js

@@ -0,0 +1,29 @@
+import Mock from '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)'
+  }]
+})
+
+export default [
+  {
+    url: '/table/list',
+    type: 'get',
+    response: config => {
+      const items = data.items
+      return {
+        code: 20000,
+        data: {
+          total: items.length,
+          items: items
+        }
+      }
+    }
+  }
+]

+ 84 - 0
lss_frontend/mock/user.js

@@ -0,0 +1,84 @@
+
+const tokens = {
+  admin: {
+    token: 'admin-token'
+  },
+  editor: {
+    token: 'editor-token'
+  }
+}
+
+const users = {
+  'admin-token': {
+    roles: ['admin'],
+    introduction: 'I am a super administrator',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Super Admin'
+  },
+  'editor-token': {
+    roles: ['editor'],
+    introduction: 'I am an editor',
+    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
+    name: 'Normal Editor'
+  }
+}
+
+export default [
+  // user login
+  {
+    url: '/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: '/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: '/user/logout',
+    type: 'post',
+    response: _ => {
+      return {
+        code: 20000,
+        data: 'success'
+      }
+    }
+  }
+]

+ 64 - 0
lss_frontend/package.json

@@ -0,0 +1,64 @@
+{
+    "name": "vue-admin-template",
+    "version": "4.2.1",
+    "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
+    "author": "Pan <panfree23@gmail.com>",
+    "license": "MIT",
+    "scripts": {
+        "dev": "vue-cli-service serve",
+        "build:prod": "vue-cli-service build",
+        "build:stage": "vue-cli-service build --mode staging",
+        "preview": "node build/index.js --preview",
+        "lint": "eslint --ext .js,.vue src",
+        "test:unit": "jest --clearCache && vue-cli-service test:unit",
+        "test:ci": "npm run lint && npm run test:unit",
+        "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
+    },
+    "dependencies": {
+        "axios": "0.18.1",
+        "element-ui": "^2.12.0",
+        "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": {
+        "@babel/core": "7.0.0",
+        "@babel/register": "7.0.0",
+        "@vue/cli-plugin-babel": "3.6.0",
+        "@vue/cli-plugin-eslint": "^3.9.1",
+        "@vue/cli-plugin-unit-jest": "3.6.3",
+        "@vue/cli-service": "3.6.0",
+        "@vue/test-utils": "1.0.0-beta.29",
+        "autoprefixer": "^9.5.1",
+        "babel-core": "7.0.0-bridge.0",
+        "babel-eslint": "10.0.1",
+        "babel-jest": "23.6.0",
+        "chalk": "2.4.2",
+        "connect": "3.6.6",
+        "eslint": "5.15.3",
+        "eslint-plugin-vue": "5.2.2",
+        "html-webpack-plugin": "3.2.0",
+        "mockjs": "1.0.1-beta3",
+        "node-sass": "^4.9.0",
+        "runjs": "^4.3.2",
+        "sass-loader": "^7.1.0",
+        "script-ext-html-webpack-plugin": "2.1.3",
+        "script-loader": "0.7.2",
+        "serve-static": "^1.13.2",
+        "svg-sprite-loader": "4.1.3",
+        "svgo": "1.2.2",
+        "vue-template-compiler": "2.6.10"
+    },
+    "engines": {
+        "node": ">=8.9",
+        "npm": ">= 3.0.0"
+    },
+    "browserslist": [
+        "> 1%",
+        "last 2 versions"
+    ]
+}

+ 5 - 0
lss_frontend/postcss.config.js

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

binární
lss_frontend/public/cloud.ico


binární
lss_frontend/public/favicon.ico


+ 17 - 0
lss_frontend/public/index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+    <link rel="icon" href="<%= BASE_URL %>lishui.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>

binární
lss_frontend/public/lishui.ico


+ 11 - 0
lss_frontend/src/App.vue

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

+ 9 - 0
lss_frontend/src/api/table.js

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

+ 24 - 0
lss_frontend/src/api/user.js

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

binární
lss_frontend/src/assets/404_images/404.png


binární
lss_frontend/src/assets/404_images/404_cloud.png


binární
lss_frontend/src/assets/defaultUser.png


binární
lss_frontend/src/assets/logo.png


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

@@ -0,0 +1,78 @@
+<template>
+  <el-breadcrumb class="app-breadcrumb" separator="/">
+    <transition-group name="breadcrumb">
+      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
+        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
+        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
+      </el-breadcrumb-item>
+    </transition-group>
+  </el-breadcrumb>
+</template>
+
+<script>
+import pathToRegexp from 'path-to-regexp'
+
+export default {
+  data() {
+    return {
+      levelList: null
+    }
+  },
+  watch: {
+    $route() {
+      this.getBreadcrumb()
+    }
+  },
+  created() {
+    this.getBreadcrumb()
+  },
+  methods: {
+    getBreadcrumb() {
+      // only show routes with meta.title
+      let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
+      const first = matched[0]
+
+      // if (!this.isDashboard(first)) {
+      //   matched = [{ path: '/dashboard', meta: { title: '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>

+ 37 - 0
lss_frontend/src/components/Hamburger/index.vue

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

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

+ 9 - 0
lss_frontend/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)

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
lss_frontend/src/icons/svg/dashboard.svg


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

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

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
lss_frontend/src/icons/svg/eye-open.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
lss_frontend/src/icons/svg/eye.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
lss_frontend/src/icons/svg/form.svg


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

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

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
lss_frontend/src/icons/svg/nested.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
lss_frontend/src/icons/svg/password.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
lss_frontend/src/icons/svg/table.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
lss_frontend/src/icons/svg/tree.svg


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

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

+ 22 - 0
lss_frontend/src/icons/svgo.yml

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

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

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

+ 131 - 0
lss_frontend/src/layout/components/Navbar.vue

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

+ 26 - 0
lss_frontend/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)
+        }
+      }
+    }
+  }
+}

+ 29 - 0
lss_frontend/src/layout/components/Sidebar/Item.vue

@@ -0,0 +1,29 @@
+<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) {
+      vnodes.push(<svg-icon icon-class={icon}/>)
+    }
+
+    if (title) {
+      vnodes.push(<span slot='title'>{(title)}</span>)
+    }
+    return vnodes
+  }
+}
+</script>

+ 36 - 0
lss_frontend/src/layout/components/Sidebar/Link.vue

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

+ 85 - 0
lss_frontend/src/layout/components/Sidebar/Logo.vue

@@ -0,0 +1,85 @@
+<template>
+    <div class="sidebar-logo-container" :class="{'collapse':collapse}">
+        <transition name="sidebarLogoFade">
+            <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
+                <img v-if="logo" src="@/assets/logo.png" class="sidebar-logo">
+                <h1 v-else class="sidebar-title">{{ title }} </h1>
+            </router-link>
+            <router-link v-else key="expand" class="sidebar-logo-link" to="/">
+                <img v-if="logo" src="@/assets/logo.png" 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: 'https://s2.ax1x.com/2019/10/11/ub3sLd.jpg'
+                logo: true
+            }
+        }
+    }
+</script>
+
+<style lang="scss" scoped>
+    .sidebarLogoFade-enter-active {
+    transition: opacity 1.5s;
+    }
+
+    .sidebarLogoFade-enter,
+    .sidebarLogoFade-leave-to {
+        opacity: 0;
+    }
+
+    .sidebar-logo-container {
+        position: relative;
+        width: 100%;
+        height: 50px;
+        line-height: 50px;
+        background: #2b2f3a;
+        text-align: center;
+        overflow: hidden;
+
+        & .sidebar-logo-link {
+            height: 100%;
+            width: 100%;
+
+            & .sidebar-logo {
+                // width: 32px;
+                // height: 32px;
+                width: 26px;
+                height: 26px;
+                vertical-align: middle;
+                margin-right: 12px;
+            }
+
+            & .sidebar-title {
+                display: inline-block;
+                margin: 0;
+                color: #fff;
+                font-weight: 600;
+                line-height: 50px;
+                font-size: 14px;
+                font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
+                vertical-align: middle;
+            }
+        }
+
+        &.collapse {
+            .sidebar-logo {
+                margin-right: 0px;
+            }
+        }
+    }
+</style>

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

@@ -0,0 +1,95 @@
+<template>
+  <div v-if="!item.hidden" class="menu-wrapper">
+    <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
lss_frontend/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
lss_frontend/src/layout/components/index.js

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

+ 93 - 0
lss_frontend/src/layout/index.vue

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

+ 45 - 0
lss_frontend/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 })
+        }
+      }
+    }
+  }
+}

+ 30 - 0
lss_frontend/src/main.js

@@ -0,0 +1,30 @@
+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/en' // 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
+
+// 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)
+})

+ 56 - 0
lss_frontend/src/permission.js

@@ -0,0 +1,56 @@
+import router, { constantRoutes, staticRoutes, dynamicRoutes } from './router'
+import request from "@/utils/request"
+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'
+import store from './store'
+
+NProgress.configure({ showSpinner: false }) // NProgress Configuration
+
+const whiteList = ['/login'] // no redirect whitelist
+
+router.beforeEach(async (to, from, next) => {
+    NProgress.start()
+    document.title = getPageTitle(to.meta.title)
+    if (store.getters.token != null) {
+        if (store.getters.hasRoute == false) {
+            //获取路由表
+            request({
+                url: "/user/getRoutesById",
+                method: "post",
+                params: {
+                    id: store.getters.token
+                }
+            })
+            .then(response => {
+            //在这里组装动态路由
+                store.commit('permission/SET_HAS_ROUTE', true)
+                store.commit('permission/SET_AUTHORIZATION', true)
+                next()
+            })
+            .catch(error => {
+                console.log(error)
+            })
+        } else {
+            if (to.path === '/login') {
+                next({ path: '/' })
+                NProgress.done()
+            } else {
+                next();
+            }
+        }
+
+    } else {
+        if (whiteList.indexOf(to.path) !== -1) {
+            next()
+        } else {
+            next('/login')
+            NProgress.done()
+        }
+    }
+})
+
+router.afterEach(() => {
+    NProgress.done()
+})

+ 239 - 0
lss_frontend/src/router/index.js

@@ -0,0 +1,239 @@
+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'             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'),
+        hidden: true
+    },
+
+    {
+        path: '/404',
+        component: () => import('@/views/404'),
+        hidden: true
+    },
+
+    {
+        path: '/',
+        component: Layout,
+        redirect: '/management',
+        meta: { title: '资产中心', icon: 'form' },
+        children: [
+            {
+                path: '/management',
+                name: 'Management',
+                component: () => import('@/views/assets/assetManagement'),
+                meta: { title: '资产管理' }
+            },
+            {
+                path: '/statistics',
+                name: 'Statistics',
+                component: () => import('@/views/assets/assetStatistic'),
+                meta: { title: '资产统计' }
+            },
+            {
+                path: '/detail/:id',
+                name: 'Detail',
+                component: () => import('@/views/assets/assetDetail'),
+                hidden: true,
+                meta: { title: '资产详情' }
+            },
+            {
+                path: '/operation/:id',
+                name: 'Operation',
+                component: () => import('@/views/assets/assetRecord'),
+                hidden: true,
+                meta: { title: '操作记录' }
+            }
+        ]
+    },
+
+    {
+        path: '/member',
+        component: Layout,
+        redirect: '/crew',
+        meta: { title: '人员管理', icon: 'form' },
+        children: [
+            {
+                path: '/crew',
+                name: 'CrewList',
+                component: () => import('@/views/crew/crewList'),
+                meta: { title: '通讯录' }
+            },
+            {
+                path: '/crew/:id',
+                name: 'CrewDetail',
+                hidden: true,
+                component: () => import('@/views/crew/crewDetail'),
+                meta: { title: '人员详情' }
+            },
+            {
+                path: '/record',
+                name: 'Record',
+                hidden: true,
+                component: () => import('@/views/crew/record'),
+                meta: { title: '操作记录' }
+            },
+            {
+                path: '/score',
+                name: 'Score',
+                component: () => import('@/views/crew/score'),
+                meta: { title: '打分统计' }
+            }
+        ]
+    },
+
+    {
+        path: '/task',
+        component: Layout,
+        redirect: '/taskList',
+        meta: { title: '任务管理', icon: 'form' },
+        children: [
+            {
+                path: '/taskList',
+                name: 'TaskList',
+                component: () => import('@/views/task/taskList'),
+                meta: { title: '任务列表' }
+            },
+            {
+                path: '/task/:id',
+                name: 'TaskDetail',
+                hidden: true,
+                component: () => import('@/views/task/taskDetail'),
+                meta: { title: '任务详情' }
+            }
+        ]
+    },
+
+    {
+        path: '/system',
+        component: Layout,
+        redirect: '/institution',
+        meta: { title: '系统管理', icon: 'form' },
+        children: [
+            {
+                path: '/institution',
+                name: 'Institution',
+                component: () => import('@/views/system/institution'),
+                meta: { title: '制度管理' }
+            },
+            {
+                path: '/institution/:id',
+                name: 'InstitutionDetail',
+                hidden: true,
+                component: () => import('@/views/system/institutionDetail'),
+                meta: { title: '制度详情' }
+            },
+            {
+                path: '/permission',
+                name: 'Permission',
+                component: () => import('@/views/system/permission'),
+                meta: { title: '权限设置' }
+            },
+            {
+                path: '/log',
+                name: 'Log',
+                component: () => import('@/views/system/log'),
+                meta: { title: '日志管理' }
+            }
+        ]
+    },
+
+    { path: '*', redirect: '/404', hidden: true }
+]
+
+export const staticRoutes = [
+    {
+        path: '/login',
+        component: () => import('@/views/login'),
+        hidden: true
+    },
+
+    {
+        path: '/404',
+        component: () => import('@/views/404'),
+        hidden: true
+    }
+]
+
+export const dynamicRoutes = [
+    {
+        path: '/system',
+        component: Layout,
+        redirect: '/institution',
+        meta: { title: '系统管理', icon: 'form' },
+        children: [
+            {
+                path: '/institution',
+                name: 'Institution',
+                component: () => import('@/views/system/institution'),
+                meta: { title: '制度管理' }
+            },
+            {
+                path: '/institution/:id',
+                name: 'InstitutionDetail',
+                hidden: true,
+                component: () => import('@/views/system/institutionDetail'),
+                meta: { title: '制度详情' }
+            },
+            {
+                path: '/permission',
+                name: 'Permission',
+                component: () => import('@/views/system/permission'),
+                meta: { title: '权限设置' }
+            },
+            {
+                path: '/log',
+                name: 'Log',
+                component: () => import('@/views/system/log'),
+                meta: { title: '日志管理' }
+            }
+        ]
+    }
+]
+
+const createRouter = () => new Router({
+    // mode: 'history', // require service support
+    scrollBehavior: () => ({ y: 0 }),
+    routes: constantRoutes
+    // routes: staticRoutes
+})
+
+const router = createRouter()
+
+export function resetRouter() {
+    const newRouter = createRouter()
+    router.matcher = newRouter.matcher // reset router
+}
+
+export default router

+ 16 - 0
lss_frontend/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
+}

+ 9 - 0
lss_frontend/src/store/getters.js

@@ -0,0 +1,9 @@
+const getters = {
+  sidebar: state => state.app.sidebar,
+  device: state => state.app.device,
+  token: state => state.permission.token,
+  hasRoute: state => state.permission.hasRoute,
+  newRouterList: state => state.permission.newRouterList,
+  authorization: state => state.permission.authorization
+}
+export default getters

+ 21 - 0
lss_frontend/src/store/index.js

@@ -0,0 +1,21 @@
+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'
+import permission from './modules/permission'
+
+Vue.use(Vuex)
+
+const store = new Vuex.Store({
+  modules: {
+    app,
+    settings,
+    user,
+    permission,
+  },
+  getters
+})
+
+export default store

+ 48 - 0
lss_frontend/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
+}

+ 80 - 0
lss_frontend/src/store/modules/permission.js

@@ -0,0 +1,80 @@
+import request from "@/utils/request"
+import router, { dynamicRoutes, resetRouter } from '@/router'
+
+const state = {
+    token: sessionStorage.getItem("LSUserId"), //已储存的userId作为token
+    hasRoute: false, //是否已经生成了路由
+    newRouterList: [],
+    authorization: null
+}
+
+const mutations = {
+    SET_TOKEN: (state, token) => {
+        state.token = token
+    },
+    SET_HAS_ROUTE: (state, hasRoute) => {
+        state.hasRoute = hasRoute
+    },
+    SET_NEW_ROUTER_LIST: (state, newRouterList) => {
+        state.newRouterList = newRouterList
+    },
+    SET_AUTHORIZATION: (state, authorization) => {
+        state.authorization = authorization
+    }
+}
+const actions = {
+    login({ commit, state }, userInfo) {
+        const { username, password } = userInfo
+        return new Promise((resolve, reject) => {
+        request({
+            url: "/user/login",
+            method: "post",
+            params: { username: username.trim(), password: password }
+        })
+            .then(response => {
+            commit('SET_TOKEN', response.data) //这里目前是data 意义为userId
+            window.sessionStorage.setItem("LSUserId", response.data);
+            resolve()
+            })
+            .catch(error => {
+            reject(error)
+            })
+        })
+    },
+
+    loadRouter({ commit, state }) {
+        return new Promise((resolve, reject) => {
+            request({
+                url: "/user/getRoutesById",
+                method: "post"
+            })
+            .then(response => {
+                commit('SET_HAS_ROUTE', true)
+                commit('SET_NEW_ROUTER_LIST', dynamicRoutes)
+                resolve()
+            })
+            .catch(error => {
+                reject(error)
+            })
+        resolve()
+        })
+    },
+
+    // user logout
+    logout({ commit, state }) {
+        return new Promise((resolve, reject) => {
+            commit('SET_TOKEN', null)
+            window.sessionStorage.removeItem("LSUserId");
+            resetRouter()
+            commit('SET_HAS_ROUTE', false)
+            resolve()
+        })
+    },
+}
+
+export default {
+    namespaced: true,
+    state,
+    mutations,
+    actions
+}

+ 31 - 0
lss_frontend/src/store/modules/settings.js

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

+ 90 - 0
lss_frontend/src/store/modules/user.js

@@ -0,0 +1,90 @@
+import { login, logout, getInfo } from '@/api/user'
+import { getToken, setToken, removeToken } from '@/utils/auth'
+import { resetRouter } from '@/router'
+
+const state = {
+  token: getToken(),
+  name: '',
+  avatar: ''
+}
+
+const mutations = {
+  SET_TOKEN: (state, token) => {
+    state.token = token
+  },
+  SET_NAME: (state, name) => {
+    state.name = name
+  },
+  SET_AVATAR: (state, avatar) => {
+    state.avatar = avatar
+  }
+}
+
+const actions = {
+  // user login
+  login({ commit }, userInfo) {
+    const { username, password } = userInfo
+    return new Promise((resolve, reject) => {
+      login({ username: username.trim(), password: password }).then(response => {
+        const { data } = response
+        commit('SET_TOKEN', data.token)
+        setToken(data.token)
+        resolve()
+      }).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) {
+          reject('Verification failed, please Login again.')
+        }
+
+        const { name, avatar } = data
+
+        commit('SET_NAME', name)
+        commit('SET_AVATAR', avatar)
+        resolve(data)
+      }).catch(error => {
+        reject(error)
+      })
+    })
+  },
+
+  // user logout
+  logout({ commit, state }) {
+    return new Promise((resolve, reject) => {
+      logout(state.token).then(() => {
+        commit('SET_TOKEN', '')
+        removeToken()
+        resetRouter()
+        resolve()
+      }).catch(error => {
+        reject(error)
+      })
+    })
+  },
+
+  // remove token
+  resetToken({ commit }) {
+    return new Promise(resolve => {
+      commit('SET_TOKEN', '')
+      removeToken()
+      resolve()
+    })
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}
+

+ 49 - 0
lss_frontend/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
lss_frontend/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
lss_frontend/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%;
+}

+ 209 - 0
lss_frontend/src/styles/sidebar.scss

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

+ 48 - 0
lss_frontend/src/styles/transition.scss

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

+ 25 - 0
lss_frontend/src/styles/variables.scss

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

+ 13 - 0
lss_frontend/src/utils/auth.js

@@ -0,0 +1,13 @@
+const TokenKey = 'LSUserId'
+
+export function getToken() {
+    return sessionStorage.getItem(TokenKey);
+}
+
+export function setToken(token) {
+    return sessionStorage.setItem(TokenKey, token)
+}
+
+export function removeToken() {
+    return sessionStorage.removeItem(TokenKey)
+}

+ 10 - 0
lss_frontend/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}`
+}

+ 105 - 0
lss_frontend/src/utils/index.js

@@ -0,0 +1,105 @@
+/**
+ * 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) {
+        return null
+    }
+    const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
+    let date
+    if (typeof time === 'object') {
+        date = time
+    } else {
+        if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
+        time = parseInt(time)
+        }
+        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]
+        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) {
+        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 = url.split('?')[1]
+    if (!search) {
+        return {}
+    }
+    return JSON.parse(
+        '{"' +
+        decodeURIComponent(search)
+            .replace(/"/g, '\\"')
+            .replace(/&/g, '","')
+            .replace(/=/g, '":"')
+            .replace(/\+/g, ' ') +
+        '"}'
+    )
+}

+ 55 - 0
lss_frontend/src/utils/request.js

@@ -0,0 +1,55 @@
+import axios from 'axios'
+import { Message } from 'element-ui'
+import store from '@/store'
+import { getToken } from '@/utils/auth'
+
+const service = axios.create({
+    // baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
+    baseURL: '/api',
+    timeout: 5000 // request timeout
+})
+
+// request interceptor
+service.interceptors.request.use(
+    config => {
+        if (store.getters.token) {
+            config.headers['X-Token'] = getToken()
+        }
+        return config
+    },
+    error => {
+        Message({
+            message: error.message,
+            type: 'error',
+            duration: 5 * 1000
+        })
+        return Promise.reject(error)
+    }
+)
+
+service.interceptors.response.use(
+    response => {
+        const res = response.data
+
+        if (res.code !== 'ok') {
+            Message({
+                message: res.msg || 'Error',
+                type: 'error',
+                duration: 5 * 1000
+            })
+            return Promise.reject(new Error(res.msg || 'Error'))
+        } else {
+            return res
+        }
+    },
+    error => {
+        Message({
+            message: error.message,
+            type: 'error',
+            duration: 5 * 1000
+        })
+        return Promise.reject(error)
+    }
+)
+
+export default service

+ 20 - 0
lss_frontend/src/utils/validate.js

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

+ 250 - 0
lss_frontend/src/views/404.vue

@@ -0,0 +1,250 @@
+<template>
+    <div class="wscn-http404-container">
+        <div class="wscn-http404">
+            <div class="pic-404">
+                <img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
+                <img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
+                <img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
+                <img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
+            </div>
+            <div class="bullshit">
+                <div class="bullshit__oops">抱歉</div>
+                <div class="bullshit__headline">{{ message }}</div>
+                <div class="bullshit__info">请检查url</div>
+                <a href="" class="bullshit__return-home">返回主页</a>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+    export default {
+        name: 'Page404',
+        computed: {
+            message() {
+                return '没有找到页面'
+            }
+        }
+    }
+</script>
+
+<style lang="scss" scoped>
+    .wscn-http404-container{
+        transform: translate(-50%,-50%);
+        position: absolute;
+        top: 40%;
+        left: 50%;
+    }
+
+    .wscn-http404 {
+        position: relative;
+        width: 1200px;
+        padding: 0 50px;
+        overflow: hidden;
+
+        .pic-404 {
+            position: relative;
+            float: left;
+            width: 600px;
+            overflow: hidden;
+
+            &__parent {
+                width: 100%;
+            }
+
+            &__child {
+                position: absolute;
+
+                &.left {
+                    width: 80px;
+                    top: 17px;
+                    left: 220px;
+                    opacity: 0;
+                    animation-name: cloudLeft;
+                    animation-duration: 2s;
+                    animation-timing-function: linear;
+                    animation-fill-mode: forwards;
+                    animation-delay: 1s;
+                }
+
+                &.mid {
+                    width: 46px;
+                    top: 10px;
+                    left: 420px;
+                    opacity: 0;
+                    animation-name: cloudMid;
+                    animation-duration: 2s;
+                    animation-timing-function: linear;
+                    animation-fill-mode: forwards;
+                    animation-delay: 1.2s;
+                }
+
+                &.right {
+                    width: 62px;
+                    top: 100px;
+                    left: 500px;
+                    opacity: 0;
+                    animation-name: cloudRight;
+                    animation-duration: 2s;
+                    animation-timing-function: linear;
+                    animation-fill-mode: forwards;
+                    animation-delay: 1s;
+                }
+
+                @keyframes cloudLeft {
+                    0% {
+                        top: 17px;
+                        left: 220px;
+                        opacity: 0;
+                    }
+
+                    20% {
+                        top: 33px;
+                        left: 188px;
+                        opacity: 1;
+                    }   
+
+                    80% {
+                        top: 81px;
+                        left: 92px;
+                        opacity: 1;
+                    }
+
+                    100% {
+                        top: 97px;
+                        left: 60px;
+                        opacity: 0;
+                    }
+                }
+
+                @keyframes cloudMid {
+                    0% {
+                        top: 10px;
+                        left: 420px;
+                        opacity: 0;
+                    }
+
+                    20% {
+                        top: 40px;
+                        left: 360px;
+                        opacity: 1;
+                    }
+
+                    70% {
+                        top: 130px;
+                        left: 180px;
+                        opacity: 1;
+                    }
+
+                    100% {
+                        top: 160px;
+                        left: 120px;
+                        opacity: 0;
+                    }
+                }
+
+                @keyframes cloudRight {
+                    0% {
+                        top: 100px;
+                        left: 500px;
+                        opacity: 0;
+                    }
+
+                    20% {
+                        top: 120px;
+                        left: 460px;
+                        opacity: 1;
+                    }
+
+                    80% {
+                        top: 180px;
+                        left: 340px;
+                        opacity: 1;
+                    }
+
+                    100% {
+                        top: 200px;
+                        left: 300px;
+                        opacity: 0;
+                    }
+                }
+            }
+        }
+
+        .bullshit {
+            position: relative;
+            float: left;
+            width: 300px;
+            padding: 30px 0;
+            overflow: hidden;
+
+            &__oops {
+                font-size: 32px;
+                font-weight: bold;
+                line-height: 40px;
+                color: #1482f0;
+                opacity: 0;
+                margin-bottom: 20px;
+                animation-name: slideUp;
+                animation-duration: 0.5s;
+                animation-fill-mode: forwards;
+            }
+
+            &__headline {
+                font-size: 20px;
+                line-height: 24px;
+                color: #222;
+                font-weight: bold;
+                opacity: 0;
+                margin-bottom: 10px;
+                animation-name: slideUp;
+                animation-duration: 0.5s;
+                animation-delay: 0.1s;
+                animation-fill-mode: forwards;
+            }
+
+            &__info {
+                font-size: 13px;
+                line-height: 21px;
+                color: grey;
+                opacity: 0;
+                margin-bottom: 30px;
+                animation-name: slideUp;
+                animation-duration: 0.5s;
+                animation-delay: 0.2s;
+                animation-fill-mode: forwards;
+            }
+
+            &__return-home {
+                display: block;
+                float: left;
+                width: 110px;
+                height: 36px;
+                background: #1482f0;
+                border-radius: 100px;
+                text-align: center;
+                color: #ffffff;
+                opacity: 0;
+                font-size: 14px;
+                line-height: 36px;
+                cursor: pointer;
+                animation-name: slideUp;
+                animation-duration: 0.5s;
+                animation-delay: 0.3s;
+                animation-fill-mode: forwards;
+            }
+
+            @keyframes slideUp {
+                0% {
+                    transform: translateY(60px);
+                    opacity: 0;
+                }
+                
+                100% {
+                    transform: translateY(0);
+                    opacity: 1;
+                }
+            }
+        }
+    }
+</style>

+ 661 - 0
lss_frontend/src/views/assets/assetDetail.vue

@@ -0,0 +1,661 @@
+<template>
+  <div class="app-container">
+    <!-- 上部分区域 资产详情 -->
+    <el-row style="margin-bottom: 20px">
+      <el-col :span="3">
+        <el-image
+          style="width: 100px; height: 100px"
+          :src="'http://localhost:9102/img' + assetData.pic"
+        ></el-image>
+      </el-col>
+      <el-col class="detail" :span="6">
+        <span class="tips">{{assetData.name}}</span>
+        <span class="number">{{assetData.modelNumber}}</span>
+      </el-col>
+      <el-col class="detail" :span="3">
+        <span>所属分类</span>
+      </el-col>
+      <el-col class="detail" :span="3">
+        <span>生产厂家</span>
+      </el-col>
+      <el-col class="detail" :span="3">
+        <span>位置</span>
+      </el-col>
+      <el-col class="detail" :span="2">
+        <span>服务期限</span>
+      </el-col>
+      <el-col class="detail" :span="3">
+        <span>下次维护日期</span>
+      </el-col>
+      <el-col class="detail" :span="1">
+        <el-button type="primary" size="small" @click="backToList" style="float:right">返回</el-button>
+      </el-col>
+      <el-col class="detail" :span="6">
+        <span class="tips">{{assetData.amount}}{{assetData.unit}}</span>
+      </el-col>
+      <el-col class="detail" :span="3">
+        <span class="tips">{{assetData.tagName}}</span>
+      </el-col>
+      <el-col class="detail" :span="3">
+        <span class="tips">{{assetData.address}}</span>
+      </el-col>
+      <el-col class="detail" :span="3">
+        <span class="tips">{{assetData.factory}}</span>
+      </el-col>
+      <el-col class="detail" :span="2">
+        <span class="tips">{{assetData.serviceLife}}天</span>
+      </el-col>
+      <el-col class="detail" :span="4">
+        <span class="tips">{{assetData.nextIndate}}</span>
+      </el-col>
+    </el-row>
+
+    <!-- 下部分区域 详细分配情况 -->
+    <el-row style="margin-bottom: 20px">
+      <el-col :span="24">
+        <span class="title">搜索</span>
+        <el-input
+          placeholder="输入编号搜索"
+          v-model="searchNumber"
+          clearable
+          @keyup.enter.native="getItem(searchNumber, currentSearchState, currentSearchOrder)"
+          @blur="getItem(searchNumber, currentSearchState, currentSearchOrder)"
+          style="width:150px"
+          size="small"
+        ></el-input>
+        <!-- 筛选 -->
+        <span class="title">筛选</span>
+        <el-tag
+          class="tag"
+          :type="currentSearchState == -1? null: 'info'"
+          @click="getItem(searchNumber, -1, currentSearchOrder)"
+        >全部</el-tag>
+        <el-tag
+          class="tag"
+          :type="currentSearchState == 0? null: 'info'"
+          @click="getItem(searchNumber, 0, currentSearchOrder)"
+        >未用</el-tag>
+        <el-tag
+          class="tag"
+          :type="currentSearchState == 1? null: 'info'"
+          @click="getItem(searchNumber, 1, currentSearchOrder)"
+        >已用</el-tag>
+        <el-tag
+          class="tag"
+          :type="currentSearchState == 2? null: 'info'"
+          @click="getItem(searchNumber, 2, currentSearchOrder)"
+        >待维护</el-tag>
+        <el-tag
+          class="tag"
+          :type="currentSearchState == 3? null: 'info'"
+          @click="getItem(searchNumber, 3, currentSearchOrder)"
+        >报废</el-tag>
+        <!-- 排序 -->
+        <span class="title">排序</span>
+        <el-tag
+          class="tag"
+          :type="currentSearchOrder == 0? null: 'info'"
+          @click="getItem(searchNumber, currentSearchState, 0)"
+        >资产编号顺序</el-tag>
+        <el-tag
+          class="tag"
+          :type="currentSearchOrder == 1? null: 'info'"
+          @click="getItem(searchNumber, currentSearchState, 1)"
+        >资产编号倒序</el-tag>
+        <el-tag
+          class="tag"
+          :type="currentSearchOrder == 2? null: 'info'"
+          @click="getItem(searchNumber, currentSearchState, 2)"
+        >更新时间顺序</el-tag>
+        <el-tag
+          class="tag"
+          :type="currentSearchOrder == 3? null: 'info'"
+          @click="getItem(searchNumber, currentSearchState, 3)"
+        >更新时间倒序</el-tag>
+        <el-button class="bottom-button" type="text" @click="toRecord">操作记录</el-button>
+        <el-button class="bottom-button" type="text" @click="openEditCodeDialog">修改编号</el-button>
+        <el-button class="bottom-button" type="text" @click="openAddDialog">添加资产</el-button>
+        <el-button class="bottom-button" type="text" @click="openMaintainDialog">维护资产</el-button>
+      </el-col>
+    </el-row>
+    <div v-loading="loading">
+      <div class="card" v-for="item in itemData" :key="item.id">
+        <div class="number" style="margin-bottom: 20px">
+          <span class="number-title">{{item.modelNo}}</span>
+          <span style="float:right">
+            {{item.state == 0? "未使用": item.state == 1?
+            "已使用":item.state == 2? "维护中": "已报废"}}
+          </span>
+        </div>
+        <div class="number">
+          <span v-if="item.state == 1">{{item.username}} / {{item.phone}}</span>
+          <el-popover placement="bottom" trigger="click">
+            <el-button
+              class="single-item"
+              type="text"
+              @click="openDealDialog(item.id, item.modelNo, item.state)"
+            >处置资产</el-button>
+            <el-button
+              class="single-item"
+              type="text"
+              @click="deleteItem(item.id, item.modelNo)"
+            >删除资产</el-button>
+            <span slot="reference" class="options">· · ·</span>
+          </el-popover>
+        </div>
+      </div>
+    </div>
+
+    <!-- 添加资产的dialog -->
+    <el-dialog title="添加资产" :visible.sync="addDialogVisible" width="400px">
+      <el-form ref="addForm" :model="addItemForm" :rules="rules1" label-width="70px">
+        <el-form-item label="数量" prop="amount">
+          <el-input v-model="addItemForm.amount" placeholder="请输入数量" clearable></el-input>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="addDialogVisible=false">取消</el-button>
+        <el-button type="primary" @click="addItem">提交</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 修改资产编号的dialog -->
+    <el-dialog title="修改编号" :visible.sync="editCodeDialogVisible" width="600px">
+      <el-form ref="editCodeForm" :model="editCodeForm" :rules="rules2" label-width="100px">
+        <el-form-item label="部门编号" prop="divisionCode">
+          <el-input v-model="editCodeForm.divisionCode" placeholder="请输入部门编号" clearable></el-input>
+        </el-form-item>
+        <el-form-item label="字母前缀" prop="suffixCode">
+          <el-input v-model="editCodeForm.suffixCode" placeholder="请输入字母前缀" clearable></el-input>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="editCodeDialogVisible=false">取消</el-button>
+        <el-button type="primary" @click="editCode">提交</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 处置资产的dialog -->
+    <el-dialog title="处置资产" :visible.sync="dealDialogVisible" width="400px">
+      <el-form ref="dealForm" :model="dealForm" :rules="rules3" label-width="80px">
+        <el-form-item label="资产编号" prop="modelNo">
+          <el-input v-model="dealForm.modelNo" disabled></el-input>
+        </el-form-item>
+        <el-form-item label="资产状态" prop="state">
+          <el-select v-model="dealForm.state" placeholder="请选择资产状态" style="width: 100%">
+            <el-option label="未用" value="0" v-if="dealForm.currentState != 0"></el-option>
+            <el-option label="已用" value="1" v-if="dealForm.currentState != 1"></el-option>
+            <el-option label="报废" value="3" v-if="dealForm.currentState == 0"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="选择人员" prop="userId" v-if="dealForm.state == 1">
+          <el-select
+            v-model="dealForm.userId"
+            filterable
+            placeholder="请选择要分配给的人员"
+            style="width: 100%"
+          >
+            <el-option v-for="user in userData" :key="user.id" :label="user.name" :value="user.id"></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="dealDialogVisible=false">取消</el-button>
+        <el-button
+          type="primary"
+          @click="manageItem(dealForm.id, dealForm.state, dealForm.userId)"
+        >提交</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 维护资产的dialog -->
+    <el-dialog title="维护资产" :visible.sync="maintainDialogVisible" width="800px">
+      <el-form ref="maintainForm" :model="maintainForm" :rules="rules4" label-width="140px">
+        <el-form-item label="资产编号" prop="ids">
+          <el-select
+            v-model="maintainForm.ids"
+            multiple
+            placeholder="请选择要维护的资产"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="item in itemData"
+              v-if="item.state == 2"
+              :key="item.id"
+              :label="item.modelNo"
+              :value="item.id"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="维护人" prop="maintain">
+          <el-input v-model="maintainForm.maintain" placeholder="请输入维护人" clearable></el-input>
+        </el-form-item>
+        <el-form-item label="维护人联系方式" prop="maintainPhone">
+          <el-input v-model="maintainForm.maintainPhone" placeholder="请输入联系方式" clearable></el-input>
+        </el-form-item>
+        <el-form-item label="维护厂家" prop="maintainCompany">
+          <el-input v-model="maintainForm.maintainCompany" placeholder="请输入维护厂家" clearable></el-input>
+        </el-form-item>
+        <el-form-item label="维护厂家联系方式" prop="maintainCompanyPhone">
+          <el-input v-model="maintainForm.maintainCompanyPhone" placeholder="请输入联系方式" clearable></el-input>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="maintainDialogVisible=false">取消</el-button>
+        <el-button type="primary" @click="maintainItem">提交</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import request from "@/utils/request";
+export default {
+  data() {
+    return {
+      loading: false,
+      assetId: null,
+      assetData: [],
+      itemData: [],
+      userData: [],
+      divisionCode: "",
+      suffixCode: "",
+      searchNumber: "",
+      currentSearchState: -1,
+      currentSearchOrder: 0,
+      addDialogVisible: false,
+      editCodeDialogVisible: false,
+      dealDialogVisible: false,
+      maintainDialogVisible: false,
+      addItemForm: {
+        amount: null
+      },
+      editCodeForm: {
+        divisionCode: null,
+        suffixCode: null
+      },
+      dealForm: {
+        id: null,
+        modelNo: "",
+        state: 0,
+        userId: null,
+        currentState: 0
+      },
+      maintainForm: {
+        ids: [],
+        maintain: "",
+        maintainPhone: "",
+        maintainCompany: "",
+        maintainCompanyPhone: ""
+      },
+      rules1: {
+        amount: [
+          { required: true, message: "请输入要增加的数量", trigger: "blur" }
+        ]
+      },
+      rules2: {
+        divisionCode: [
+          { required: true, message: "请输入部门编码", trigger: "blur" }
+        ],
+        suffixCode: [
+          { required: true, message: "请输入前缀编码", trigger: "blur" }
+        ]
+      },
+      rules3: {
+        state: [{ required: true, message: "请选择状态", trigger: "blur" }]
+      },
+      rules4: {
+        ids: [
+          { required: true, message: "请选择要维护的资产", trigger: "blur" }
+        ],
+        maintain: [{ required: true, message: "请输入", trigger: "blur" }],
+        maintainPhone: [{ required: true, message: "请输入", trigger: "blur" }],
+        maintainCompany: [
+          { required: true, message: "请输入", trigger: "blur" }
+        ],
+        maintainCompanyPhone: [
+          { required: true, message: "请输入", trigger: "blur" }
+        ]
+      }
+    };
+  },
+  methods: {
+    //获取资产详情
+    getAsset() {
+      this.loading = true;
+      request({
+        url: "/goods/getAssetById",
+        method: "post",
+        params: { id: this.assetId }
+      })
+        .then(response => {
+          this.assetData = response.data;
+          //顺便再获取一下物品列表
+          this.getItem("", -1, 0);
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+    //获取物品
+    getItem(searchNumber, currentSearchState, currentSearchOrder) {
+      this.loading = true;
+      this.searchNumber = searchNumber;
+      this.currentSearchOrder = currentSearchOrder;
+      this.currentSearchState = currentSearchState;
+      request({
+        url: "/goods-no/getItems",
+        method: "post",
+        params: {
+          goodsId: this.assetData.id,
+          modelNo: this.searchNumber,
+          order: this.currentSearchOrder,
+          state: this.currentSearchState
+        }
+      })
+        .then(response => {
+          this.itemData = response.data;
+          this.loading = false;
+          //重新单独获取数量
+          this.resetAmount();
+          //如果数组有长度 获取一下当前的divisionCode和suffixCode
+          if (this.itemData.length > 0) {
+            this.divisionCode = this.itemData[0].divisionCode;
+            this.suffixCode = this.itemData[0].suffixCode;
+          }
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+    //重新获取数量
+    resetAmount() {
+      request({
+        url: "/goods-no/getItemAmount",
+        method: "post",
+        params: {
+          id: this.assetData.id
+        }
+      })
+        .then(response => {
+          this.assetData.amount = response.data;
+        })
+        .catch(error => {});
+    },
+    //打开新建的dialog
+    openAddDialog() {
+      this.addItemForm.amount = null;
+      this.addDialogVisible = true;
+    },
+    //打开编辑前缀的dialog
+    openEditCodeDialog() {
+      this.editCodeForm.divisionCode = this.divisionCode;
+      this.editCodeForm.suffixCode = this.suffixCode;
+      this.editCodeDialogVisible = true;
+    },
+    //打开处置资产的dialog
+    openDealDialog(id, modelNo, state) {
+      //在此处获取下拉列表的用户
+      this.getUser();
+      this.dealForm.id = id;
+      this.dealForm.modelNo = modelNo;
+      this.dealForm.state = null;
+      this.dealForm.userId = null;
+      this.dealForm.currentState = state;
+      this.dealDialogVisible = true;
+    },
+    //打开维护资产的dialog
+    openMaintainDialog() {
+      this.maintainForm.ids = [];
+      this.maintainForm.maintain = "";
+      this.maintainForm.maintainPhone = "";
+      this.maintainForm.maintainCompany = "";
+      this.maintainForm.maintainCompanyPhone = "";
+      this.maintainDialogVisible = true;
+    },
+    //维护资产
+    maintainItem() {
+      this.$refs.maintainForm.validate(valid => {
+        if (valid) {
+          this.loading = true;
+          request({
+            url: "/goods-no/maintainItems",
+            method: "post",
+            params: {
+              ids: this.maintainForm.ids.join(","),
+              operator: this.maintainForm.maintain,
+              operatorPhone: this.maintainForm.maintainPhone,
+              company: this.maintainForm.maintainCompany,
+              companyPhone: this.maintainForm.maintainCompanyPhone
+            }
+          })
+            .then(response => {
+              this.$message({
+                message: "维护完毕",
+                type: "success"
+              });
+              this.maintainDialogVisible = false;
+              this.getItem("", -1, 0);
+            })
+            .catch(error => {
+              this.loading = false;
+            });
+        }
+      });
+    },
+    //增加数量
+    addItem() {
+      this.$refs.addForm.validate(valid => {
+        if (valid) {
+          this.loading = true;
+          request({
+            url: "/goods-no/addItems",
+            method: "post",
+            params: {
+              goodsId: this.assetData.id,
+              amount: this.addItemForm.amount
+            }
+          })
+            .then(response => {
+              this.$message({
+                message: "添加成功",
+                type: "success"
+              });
+              this.addDialogVisible = false;
+              this.getItem("", -1, 0);
+            })
+            .catch(error => {
+              this.loading = false;
+            });
+        }
+      });
+    },
+    //修改资产编号
+    editCode() {
+      this.$refs.editCodeForm.validate(valid => {
+        if (valid) {
+          if (
+            this.editCodeForm.divisionCode == this.divisionCode &&
+            this.editCodeForm.suffixCode == this.suffixCode
+          ) {
+            this.$message({
+              message: "尚未修改",
+              type: "error"
+            });
+          } else {
+            this.loading = true;
+            request({
+              url: "/goods-no/editItemCode",
+              method: "post",
+              params: {
+                goodsId: this.assetData.id,
+                divisionCode: this.editCodeForm.divisionCode,
+                suffixCode: this.editCodeForm.suffixCode
+              }
+            })
+              .then(response => {
+                this.$message({
+                  message: "修改编号成功",
+                  type: "success"
+                });
+                this.editCodeDialogVisible = false;
+                this.getItem("", -1, 0);
+              })
+              .catch(error => {
+                this.loading = false;
+              });
+          }
+        }
+      });
+    },
+    //删除物品
+    deleteItem(id, name) {
+      this.$confirm("确定要删除资产" + name + "吗", "添加标签", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "info"
+      })
+        .then(() => {
+          this.loading = true;
+          request({
+            url: "/goods-no/deleteItemById",
+            method: "post",
+            params: { id: id }
+          })
+            .then(response => {
+              this.$message({
+                type: "success",
+                message: "删除成功!"
+              });
+              this.getItem("", -1, 0);
+            })
+            .catch(error => {
+              this.loading = false;
+            });
+        })
+        .catch(() => {});
+    },
+    //处置资产
+    manageItem(id, state, userId) {
+      this.$refs.dealForm.validate(valid => {
+        if (valid) {
+          if (state == 1 && userId == null) {
+            this.$message({
+              message: "请选择要分配的用户",
+              type: "error"
+            });
+          } else {
+            this.loading = true;
+            request({
+              url: "/goods-no/manageItem",
+              method: "post",
+              params: {
+                id: id,
+                state: state,
+                userId: state == 1 ? userId : null,
+                operatorId: window.sessionStorage.getItem("LSUserId")
+              }
+            })
+              .then(response => {
+                this.$message({
+                  message: "资产处置完毕",
+                  type: "success"
+                });
+                this.getItem("", -1, 0);
+                this.dealDialogVisible = false;
+              })
+              .catch(error => {
+                this.loading = false;
+              });
+          }
+        }
+      });
+    },
+    //获取用户信息
+    getUser() {
+      request({
+        url: "/user/getUser",
+        method: "post"
+      })
+        .then(response => {
+          this.userData = response.data;
+        })
+        .catch(error => {});
+    },
+    //回到前一个页面
+    backToList() {
+      this.$router.go(-1);
+    },
+    //到操作记录
+    toRecord() {
+      this.$router.push("/operation/" + this.assetId);
+    }
+  },
+  mounted() {
+    //为本页的assetId赋值
+    this.assetId = this.$route.params.id;
+    if (this.assetId == null) {
+      this.$router.go("/404");
+    } else {
+      this.getAsset();
+    }
+  }
+};
+</script>
+
+<style scoped>
+.tag {
+  cursor: pointer;
+}
+.detail {
+  line-height: 50px;
+  height: 50px;
+}
+.detail span {
+  color: #444;
+  font-size: 14px;
+}
+.detail .tips {
+  color: #777;
+}
+.detail .number {
+  font-size: 12px;
+  color: #999;
+}
+.title {
+  font-size: 14px;
+  line-height: 40px;
+  margin: 0 15px 0 15px;
+}
+.bottom-button {
+  float: right;
+  margin-left: 5px;
+}
+.card {
+  float: left;
+  width: 200px;
+  height: 67px;
+  background-color: #eee;
+  margin: 0 10px 10px 0;
+  padding: 10px;
+}
+.card .number {
+  width: 100%;
+  font-size: 12px;
+  color: #555;
+}
+.card .number-title {
+  font-size: 13px;
+  font-weight: bold;
+}
+.options {
+  float: right;
+  font-weight: bold;
+  cursor: pointer;
+}
+.single-item {
+  margin: 0 5px;
+  padding: 0;
+}
+</style>
+

+ 664 - 0
lss_frontend/src/views/assets/assetManagement.vue

@@ -0,0 +1,664 @@
+<template>
+    <div class="app-container">
+        <!-- 头部区域 -->
+        <el-row>
+            <el-col class="item" :span="24">
+                <span class="title">检索类型</span>
+                <el-tag :type="currentType == null? '': 'info'" @click="getAssets(null, currentOrder, keyword)" style="cursor: pointer">
+                    全部
+                </el-tag>
+                <el-tag class="tag" v-for="tag in tags" :key="tag.id" :type="currentType == tag.id? '': 'info'" @click="getAssets(tag.id, currentOrder, keyword)">
+                    {{tag.name}}
+                </el-tag>
+            </el-col>
+            <el-col class="item" :span="24">
+                <span class="title">排序方式</span>
+                <el-tag class="tag" :type="currentOrder == 0 || currentOrder == null? '': 'info'" @click="getAssets(currentType, 0, keyword)">
+                    登记时间倒序
+                </el-tag>
+                <el-tag class="tag" :type="currentOrder == 1? '': 'info'" @click="getAssets(currentType, 1, keyword)">
+                    登记时间顺序
+                </el-tag>
+            </el-col>
+            <el-col class="item" :span="24">
+                <span class="title">搜索名称</span>
+                <el-input v-model="keyword" placeholder="输入名称检索" clearable style="width: 200px" @keyup.enter.native="getAssets(currentType, currentOrder, keyword)" size="small"></el-input>
+                <el-button @click="getAssets(currentType, currentOrder, keyword)" size="small">搜索</el-button>
+                <el-button type="primary" style="float: right" @click="openDialog1" size="small">登记资产</el-button>
+            </el-col>
+        </el-row>
+
+        <!-- 列表区域 -->
+        <el-table :data="assets" style="width: 100%" v-loading="loading">
+            <el-table-column type="index" width="40"></el-table-column>
+            <el-table-column label="资产图片" width="110">
+                <template slot-scope="scope">
+                    <el-image style="width: 100px; height: 100px" :src="'http://localhost:9102/img' + scope.row.pic"></el-image>
+                </template>
+            </el-table-column>
+            <el-table-column prop="name" label="资产名称"></el-table-column>
+            <el-table-column prop="modelNumber" label="型号" width="100"></el-table-column>
+            <el-table-column prop="amount" label="数量" width="50">
+                <template slot-scope="scope">{{assets2[scope.$index].amount}}{{scope.row.unit}}</template>
+            </el-table-column>
+            <el-table-column prop="tagName" label="分类" width="100">
+                <template slot-scope="scope">{{assets2[scope.$index].tagName}}</template>
+            </el-table-column>
+            <el-table-column prop="address" label="位置" width="130"></el-table-column>
+            <el-table-column prop="factory" label="生产厂家" width="130"></el-table-column>
+            <el-table-column prop="serviceLife" label="服务期限" width="80"></el-table-column>
+            <el-table-column prop="nextIndate" label="下次维护日期" width="110"></el-table-column>
+            <el-table-column label="操作" width="180" fixed="right">
+                <template slot-scope="scope">
+                    <el-button type="primary" @click="toDetail(scope.row.id)">详情</el-button>
+                    <el-button @click="openDialog3(scope.$index)">编辑</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <!-- 页码区域 -->
+        <el-pagination
+            v-if="total > 0"
+            @size-change="handleSizeChange"
+            @current-change="handleCurrentChange"
+            :current-page="pageIndex"
+            :page-sizes="[20, 50, 100]"
+            :page-size="20"
+            layout="total, sizes, prev, pager, next, jumper"
+            :total="total"
+            background
+            style="float: right"
+        ></el-pagination>
+
+        <!-- 添加的dialog -->
+        <el-dialog title="登记资产" :visible.sync="dialog1Visible" width="600px">
+        <el-form ref="form1" :model="assetForm" :rules="rules1" label-width="110px">
+            <el-form-item label="物品名称" prop="name">
+            <el-input v-model="assetForm.name" placeholder="请输入物品名称" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="型号" prop="modelNumber">
+            <el-input v-model="assetForm.modelNumber" placeholder="请输入型号" clearable></el-input>
+            </el-form-item>
+            <el-row>
+            <el-col :span="12">
+                <el-form-item label="数量" prop="amount">
+                <el-input v-model="assetForm.amount" placeholder="请输入数量" clearable></el-input>
+                </el-form-item>
+            </el-col>
+            <el-col :span="12">
+                <el-form-item label="单位" prop="unit">
+                <el-input v-model="assetForm.unit" placeholder="请输入单位" clearable></el-input>
+                </el-form-item>
+            </el-col>
+            </el-row>
+            <el-form-item label="物品图片" prop="image">
+            <el-upload
+                ref="upload"
+                action="customize"
+                :http-request="uploadDiscardFile"
+                :limit="1"
+                :before-remove="beforeRemove"
+            >
+                <!-- 这里添加和编辑的上传图片都没有检查类型 -->
+                <!-- 并且所有的rules都没有详细调整触发方式以及数据类型限制 -->
+                <el-button type="primary" :loading="loading">上传一张图片</el-button>
+            </el-upload>
+            </el-form-item>
+            <el-form-item label="位置" prop="position">
+            <el-input v-model="assetForm.position" placeholder="请输入位置" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="生产厂家" prop="manufacturer">
+            <el-input v-model="assetForm.manufacturer" placeholder="请输入生产厂家" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="服务期限" prop="lifetime">
+            <el-input v-model="assetForm.lifetime" placeholder="请输入服务期限" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="下次维护日期" prop="nextDate">
+            <el-date-picker
+                v-model="assetForm.nextDate"
+                type="date"
+                format="yyyy 年 MM 月 dd 日"
+                value-format="yyyy-MM-dd"
+                placeholder="请选择下次维护日期"
+            ></el-date-picker>
+            </el-form-item>
+            <el-form-item label="所属分类" prop="belongTag">
+            <el-tag
+                class="tag"
+                v-for="tag in tags"
+                :key="tag.id"
+                :type="assetForm.belongTag == tag.id? '': 'info'"
+                @click="chooseTag(tag.id)"
+                :closable="tag.id>7"
+                @close="handleClose(tag.id)"
+            >{{tag.name}}</el-tag>
+            <!-- 添加和删除标签的地方 修改的话别忘了把下面那里也同步一下 -->
+            <el-input
+                class="input-new-tag"
+                v-if="newTagVisible"
+                v-model="newTagValue"
+                ref="saveTagInput"
+                size="small"
+                style="width:80px"
+                @keyup.enter.native="handleInputConfirm"
+                @blur="newTagVisible=false"
+            ></el-input>
+            <el-button v-else class="button-new-tag" size="small" @click="showInput">新建分类</el-button>
+            </el-form-item>
+            <p class="tips">新增标签按回车键确认</p>
+        </el-form>
+        <span slot="footer" class="dialog-footer">
+            <el-button @click="dialog1Visible = false">取消</el-button>
+            <el-button type="primary" @click="openDialog2">下一步</el-button>
+        </span>
+        </el-dialog>
+
+        <!-- 编辑的dialog -->
+        <el-dialog title="编辑资产" :visible.sync="dialog3Visible" width="600px">
+        <el-form ref="form3" :model="assetForm" :rules="rules3" label-width="110px">
+            <el-form-item label="物品名称" prop="name">
+            <el-input v-model="assetForm.name" placeholder="请输入物品名称" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="型号" prop="modelNumber">
+            <el-input v-model="assetForm.modelNumber" placeholder="请输入型号" clearable></el-input>
+            </el-form-item>
+            <el-row>
+            <el-col :span="12">
+                <el-form-item label="单位" prop="unit">
+                <el-input v-model="assetForm.unit" placeholder="请输入单位" clearable></el-input>
+                </el-form-item>
+            </el-col>
+            <el-col :span="12">
+                <el-form-item label="物品图片" prop="image">
+                <el-upload
+                    ref="upload"
+                    action="customize"
+                    :http-request="uploadDiscardFile"
+                    :limit="1"
+                    :before-remove="beforeRemove"
+                >
+                    <el-button type="primary" :loading="loading">上传一张其他图片</el-button>
+                </el-upload>
+                </el-form-item>
+            </el-col>
+            </el-row>
+            <el-form-item label="位置" prop="position">
+            <el-input v-model="assetForm.position" placeholder="请输入位置" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="生产厂家" prop="manufacturer">
+            <el-input v-model="assetForm.manufacturer" placeholder="请输入生产厂家" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="服务期限" prop="lifetime">
+            <el-input v-model="assetForm.lifetime" placeholder="请输入服务期限" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="下次维护日期" prop="nextDate">
+            <el-date-picker
+                v-model="assetForm.nextDate"
+                type="date"
+                format="yyyy 年 MM 月 dd 日"
+                value-format="yyyy-MM-dd"
+                placeholder="请选择下次维护日期"
+            ></el-date-picker>
+            </el-form-item>
+            <el-form-item label="所属分类" prop="belongTag">
+            <el-tag
+                class="tag"
+                v-for="tag in tags"
+                :key="tag.id"
+                :type="assetForm.belongTag == tag.id? '': 'info'"
+                @click="chooseTag(tag.id)"
+                :closable="tag.id>7"
+                @close="handleClose(tag.id)"
+            >{{tag.name}}</el-tag>
+            <!-- 添加和删除标签的地方 -->
+            <el-input
+                class="input-new-tag"
+                v-if="newTagVisible"
+                v-model="newTagValue"
+                ref="saveTagInput"
+                size="small"
+                style="width:80px"
+                @keyup.enter.native="handleInputConfirm"
+                @blur="newTagVisible=false"
+            ></el-input>
+            <el-button v-else class="button-new-tag" size="small" @click="showInput">新建分类</el-button>
+            </el-form-item>
+            <p class="tips">新增标签按回车键确认 编号可以在详情中修改 图片不上传即默认为不修改</p>
+        </el-form>
+        <span slot="footer" class="dialog-footer">
+            <el-button @click="dialog3Visible = false">取消</el-button>
+            <el-button type="primary" @click="editSubmit">更新</el-button>
+        </span>
+        </el-dialog>
+
+        <!-- 添加的dialog -->
+        <el-dialog title="分配资产编号" :visible.sync="dialog2Visible" width="600px">
+        <el-form ref="form2" :model="assetForm2" :rules="rules2" label-width="110px">
+            <el-form-item label="部门编号" prop="divisionCode">
+            <el-input v-model="assetForm2.divisionCode" placeholder="请输入部门编号" clearable></el-input>
+            </el-form-item>
+            <el-form-item label="字母前缀" prop="suffix">
+            <el-input v-model="assetForm2.suffix" placeholder="请输入字母前缀" clearable></el-input>
+            </el-form-item>
+            <p class="tips">编号构成:部门编号+字母前缀+四位数字(由系统自动生成)</p>
+            <p class="tips">示例:ABCD-EFG0001</p>
+        </el-form>
+        <span slot="footer" class="dialog-footer">
+            <el-button @click="goBack">上一步</el-button>
+            <el-button type="primary" @click="submit">提交</el-button>
+        </span>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import request from "@/utils/request";
+export default {
+  data() {
+    return {
+      tags: [], //标签
+      currentType: null, //当前标签
+      currentOrder: 0, //当前排序方式
+      assets: [], //资产
+      assets2: [], //资产额外信息
+      loading: false, //加载状态
+      dialog1Visible: false, //添加的第一个dialog的状态
+      dialog2Visible: false, //添加的第二个dialog的状态
+      dialog3Visible: false, //编辑的dialog的状态
+      newTagVisible: false, //新增种类的可见性
+      newTagValue: "", //新增种类的名字
+      keyword: "", //检索关键字
+      assetForm: {
+        id: null,
+        name: "",
+        modelNumber: "",
+        amount: "",
+        unit: "",
+        image: null,
+        position: "",
+        manufacturer: "",
+        lifetime: "",
+        nextDate: "",
+        belongTag: null
+      }, //dialog1中的表单
+      assetForm2: {
+        divisionCode: "",
+        suffix: ""
+      }, //dialog2中的表单
+      rules1: {
+        name: [{ required: true, message: "请输入名字", trigger: "blur" }],
+        modelNumber: [
+          { required: true, message: "请输入编号", trigger: "blur" }
+        ],
+        amount: [{ required: true, message: "请输入数量", trigger: "blur" }],
+        unit: [{ required: true, message: "请输入单位", trigger: "blur" }],
+        position: [{ required: true, message: "请输入地点", trigger: "blur" }],
+        manufacturer: [
+          { required: true, message: "请输入生产商", trigger: "blur" }
+        ],
+        lifetime: [
+          { required: true, message: "请输入使用时间", trigger: "blur" }
+        ],
+        nextDate: [
+          { required: true, message: "请输入下次维护时间", trigger: "blur" }
+        ],
+        belongTag: [{ required: true, message: "请选择标签", trigger: "blur" }]
+      }, //验证规则1
+      rules2: {
+        divisionCode: [
+          { required: true, message: "请输入部门编码", trigger: "blur" }
+        ],
+        suffix: [{ required: true, message: "请输入前缀编码", trigger: "blur" }]
+      }, //验证规则2
+      rules3: {
+        name: [{ required: true, message: "请输入名字", trigger: "blur" }],
+        modelNumber: [
+          { required: true, message: "请输入编号", trigger: "blur" }
+        ],
+        unit: [{ required: true, message: "请输入单位", trigger: "blur" }],
+        position: [{ required: true, message: "请输入地点", trigger: "blur" }],
+        manufacturer: [
+          { required: true, message: "请输入生产商", trigger: "blur" }
+        ],
+        lifetime: [
+          { required: true, message: "请输入使用时间", trigger: "blur" }
+        ],
+        nextDate: [
+          { required: true, message: "请输入下次维护时间", trigger: "blur" }
+        ],
+        belongTag: [{ required: true, message: "请选择标签", trigger: "blur" }]
+      }, //验证规则3
+      //分页相关
+      pageIndex: 1,
+      pageSize: 20,
+      total: 0
+    };
+  },
+  methods: {
+    //页码规格变更
+    handleSizeChange(val) {
+      this.pageSize = val;
+      //重新获取
+      this.getAssets(this.currentType, this.currentOrder, this.keyword);
+    },
+
+    //页码页数变更
+    handleCurrentChange(val) {
+      this.pageIndex = val;
+      //重新获取
+      this.getAssets(this.currentType, this.currentOrder, this.keyword);
+    },
+
+    //到详情页面
+    toDetail(id) {
+      if (id != null) {
+        this.$router.push("/detail/" + id);
+      } else {
+        this.$message({
+          message: "发生错误",
+          type: "error"
+        });
+      }
+    },
+
+    //打开dialog1新建第一部分
+    openDialog1() {
+      this.assetForm.id = null;
+      this.assetForm.name = "";
+      this.assetForm.modelNumber = "";
+      this.assetForm.amount = "";
+      this.assetForm.unit = "";
+      this.assetForm.image = null;
+      this.assetForm.position = "";
+      this.assetForm.manufacturer = "";
+      this.assetForm.lifetime = "";
+      this.assetForm.nextDate = "";
+      this.assetForm.belongTag = null;
+      this.dialog1Visible = true;
+    },
+
+    //打开dialog3编辑
+    openDialog3(index) {
+      this.assetForm.id = this.assets[index].id;
+      this.assetForm.name = this.assets[index].name;
+      this.assetForm.modelNumber = this.assets[index].modelNumber;
+      this.assetForm.unit = this.assets[index].unit;
+      this.assetForm.image = null;
+      this.assetForm.position = this.assets[index].address;
+      this.assetForm.manufacturer = this.assets[index].factory;
+      this.assetForm.lifetime = this.assets[index].serviceLife;
+      this.assetForm.nextDate = this.assets[index].nextIndate;
+      this.assetForm.belongTag = this.assets[index].tagId;
+      this.dialog3Visible = true;
+    },
+
+    //打开dialog2新建第二部分
+    openDialog2() {
+      this.$refs.form1.validate(valid => {
+        if (!valid) {
+          return;
+        } else if (this.assetForm.belongTag == null) {
+          this.$message({
+            message: "尚未选择种类",
+            type: "error"
+          });
+        } else if (this.assetForm.image == null) {
+          this.$message({
+            message: "尚未上传图片",
+            type: "error"
+          });
+        } else {
+          this.dialog1Visible = false;
+          this.dialog2Visible = true;
+          this.assetForm2.divisionCode = "";
+          this.assetForm2.suffix = "";
+        }
+      });
+    },
+
+    //dialog2时返回上一步
+    goBack() {
+      this.dialog2Visible = false;
+      this.dialog1Visible = true;
+    },
+
+    //新建提交
+    submit() {
+      this.$refs.form2.validate(valid => {
+        if (valid) {
+          this.loading = true;
+          var form = new FormData();
+          form.append("name", this.assetForm.name);
+          form.append("modelNumber", this.assetForm.modelNumber);
+          form.append("amount", this.assetForm.amount);
+          form.append("unit", this.assetForm.unit);
+          form.append("address", this.assetForm.position);
+          form.append("factory", this.assetForm.manufacturer);
+          form.append("serviceLife", this.assetForm.lifetime);
+          form.append("nextIndate", this.assetForm.nextDate);
+          form.append("tagId", this.assetForm.belongTag);
+          form.append("divisionCode", this.assetForm2.divisionCode);
+          form.append("suffix", this.assetForm2.suffix);
+          form.append("multipartFile", this.assetForm.image); //添加照片
+          request({
+            url: "/goods/insertAssets",
+            method: "post",
+            data: form
+          })
+            .then(response => {
+              //清除文件并获取信息
+              this.$refs.upload.clearFiles();
+              this.getTags();
+              this.getAssets(null, null, null);
+              this.dialog2Visible = false;
+              this.loading = false;
+              this.$message({
+                message: "添加成功",
+                type: "success"
+              });
+            })
+            .catch(error => {
+              this.loading = false;
+            });
+        }
+      });
+    },
+
+    //编辑提交
+    editSubmit() {
+      this.$refs.form3.validate(valid => {
+        if (valid) {
+          this.loading = true;
+          var form = new FormData();
+          form.append("id", this.assetForm.id);
+          form.append("name", this.assetForm.name);
+          form.append("modelNumber", this.assetForm.modelNumber);
+          form.append("unit", this.assetForm.unit);
+          form.append("address", this.assetForm.position);
+          form.append("factory", this.assetForm.manufacturer);
+          form.append("serviceLife", this.assetForm.lifetime);
+          form.append("nextIndate", this.assetForm.nextDate);
+          form.append("tagId", this.assetForm.belongTag);
+          form.append("multipartFile", this.assetForm.image); //添加照片
+          request({
+            url: "/goods/updateAsset",
+            method: "post",
+            data: form
+          })
+            .then(response => {
+              //清除文件并获取信息
+              this.$refs.upload.clearFiles();
+              this.getTags();
+              this.getAssets(null, null, null);
+              this.dialog3Visible = false;
+              this.loading = false;
+              this.$message({
+                message: "编辑成功",
+                type: "success"
+              });
+            })
+            .catch(error => {
+              this.loading = false;
+            });
+        }
+      });
+    },
+
+    //获取标签列表
+    getTags() {
+      request({
+        url: "/goods-tags/getTags",
+        method: "post"
+      })
+        .then(response => {
+          this.tags = response.data;
+        })
+        .catch(error => {});
+    },
+
+    //分页获取资产列表
+    getAssets(type, order, keyword) {
+      this.currentType = type;
+      this.currentOrder = order;
+      this.loading = true;
+      request({
+        url: "/goods/getAssetsByPage",
+        method: "post",
+        params: {
+          pageIndex: this.pageIndex,
+          pageSize: this.pageSize,
+          type: type,
+          order: order,
+          name: keyword
+        }
+      })
+        .then(response => {
+          this.assets = response.data.page.records;
+          this.assets2 = response.data.additional;
+          this.total = response.data.page.total;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //获取上传的文件
+    uploadDiscardFile(params) {
+      this.assetForm.image = params.file;
+      return false;
+    },
+
+    //文件上传的移除操作
+    beforeRemove() {
+      this.assetForm.image = null;
+    },
+
+    //表单内选择标签
+    chooseTag(id) {
+      this.assetForm.belongTag = id;
+    },
+
+    //点击删除标签
+    handleClose(id) {
+      this.$confirm("确定要删除分类吗", "删除分类", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      })
+        .then(() => {
+          request({
+            url: "/goods-tags/deleteTags",
+            method: "post",
+            params: { id: id }
+          })
+            .then(response => {
+              this.$message({
+                type: "success",
+                message: "删除成功!"
+              });
+              //这里要清除选中的和表单中的tag
+              this.currentType = null;
+              this.assetForm.belongTag = null;
+              this.getTags();
+            })
+            .catch(error => {});
+        })
+        .catch(() => {});
+    },
+
+    //点击开始新增标签
+    showInput() {
+      this.newTagVisible = true;
+      this.$nextTick(_ => {
+        this.$refs.saveTagInput.$refs.input.focus();
+      });
+    },
+
+    //确认新增标签
+    handleInputConfirm() {
+      if (this.newTagValue != "") {
+        this.$confirm(
+          "确定要添加新分类" + this.newTagValue + "吗",
+          "添加标签",
+          {
+            confirmButtonText: "确定",
+            cancelButtonText: "取消",
+            type: "info"
+          }
+        )
+          .then(() => {
+            request({
+              url: "/goods-tags/insertTags",
+              method: "post",
+              params: { name: this.newTagValue }
+            })
+              .then(response => {
+                this.$message({
+                  type: "success",
+                  message: "添加成功!"
+                });
+                this.getTags();
+                this.newTagVisible = false;
+                this.newTagValue = "";
+              })
+              .catch(error => {
+                this.newTagVisible = false;
+                this.newTagValue = "";
+              });
+          })
+          .catch(() => {
+            this.newTagVisible = false;
+            this.newTagValue = "";
+          });
+      }
+    }
+  },
+  mounted() {
+    this.getTags();
+    this.getAssets(null, null, null);
+  }
+};
+</script>
+
+<style scoped>
+.title {
+  line-height: 32px;
+  font-size: 14px;
+}
+.tag {
+  margin-right: 5px;
+  margin-bottom: 5px;
+  cursor: pointer;
+}
+.item {
+  margin-bottom: 5px;
+}
+.tips {
+  width: 100%;
+  text-align: center;
+  font-size: 12px;
+  color: #777;
+}
+</style>
+

+ 206 - 0
lss_frontend/src/views/assets/assetRecord.vue

@@ -0,0 +1,206 @@
+<template>
+  <div class="app-container">
+    <el-row style="margin-bottom: 20px">
+      <el-col :span="24">
+        <el-button type="primary" @click="backToDetail">返回</el-button>
+        <span style="margin: 0 20px 0 50px">搜索</span>
+        <el-input
+          placeholder="输入资产编号搜索"
+          v-model="keyword"
+          clearable
+          @keyup.enter.native="changeKeyword"
+          style="width:200px"
+        ></el-input>
+        <!-- @blur="changeKeyword" -->
+        <el-button style="float:right" @click="exportRecord">导出</el-button>
+      </el-col>
+    </el-row>
+
+    <!-- 标签页 -->
+    <el-tabs v-model="tabsName">
+      <el-tab-pane label="处置记录" name="0">
+        <!-- 处置记录列表 -->
+        <el-table :data="operationData" v-loading="loading" width="100%">
+          <el-table-column type="index" width="40"></el-table-column>
+          <el-table-column prop="modelNo" label="资产编号" width="160"></el-table-column>
+          <el-table-column prop="userName" label="处置人" width="80"></el-table-column>
+          <el-table-column prop="indate" label="处置时间" width="100"></el-table-column>
+          <el-table-column prop="content" label="处置详情"></el-table-column>
+        </el-table>
+        <!-- 页码区域 -->
+        <el-pagination
+          @size-change="handleSizeChange1"
+          @current-change="handleCurrentChange1"
+          :current-page="pageIndex1"
+          :page-sizes="[20, 50, 100]"
+          :page-size="20"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total1"
+          background
+          style="float:right"
+        ></el-pagination>
+      </el-tab-pane>
+
+      <!-- 维护记录 -->
+      <el-tab-pane label="维护记录" name="1">
+        <!-- 维护记录列表 -->
+        <el-table :data="maintainData" v-loading="loading" width="100%">
+          <el-table-column type="index" width="40"></el-table-column>
+          <el-table-column prop="modelNo" label="资产编号"></el-table-column>
+          <el-table-column prop="operator" label="维护人" width="80"></el-table-column>
+          <el-table-column prop="operator" label="维护人联系方式" width="120"></el-table-column>
+          <el-table-column prop="operator" label="维护厂家" width="100"></el-table-column>
+          <el-table-column prop="operator" label="维护厂家联系方式" width="140"></el-table-column>
+          <el-table-column prop="indate" label="处置时间" width="100"></el-table-column>
+        </el-table>
+        <!-- 页码区域 -->
+        <el-pagination
+          @size-change="handleSizeChange2"
+          @current-change="handleCurrentChange2"
+          :current-page="pageIndex2"
+          :page-sizes="[20, 50, 100]"
+          :page-size="20"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total2"
+          background
+          style="float:right"
+        ></el-pagination>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script>
+import request from "@/utils/request";
+export default {
+  data() {
+    return {
+      assetId: this.$route.params.id,
+      maintainData: [],
+      operationData: [],
+      keyword: "",
+      loading: false,
+      tabsName: 0,
+      //分页相关
+      pageIndex1: 1,
+      pageSize1: 20,
+      total1: 0,
+      pageIndex2: 1,
+      pageSize2: 20,
+      total2: 0
+    };
+  },
+  methods: {
+    //获取处置记录
+    getManagementRecord() {
+      this.loading = true;
+      request({
+        url: "/maintain-record/getOperationRecordByPage",
+        method: "post",
+        params: {
+          id: this.assetId,
+          pageIndex: this.pageIndex1,
+          pageSize: this.pageSize1,
+          keyword: this.keyword
+        }
+      })
+        .then(response => {
+          this.operationData = response.data.records;
+          this.total1 = response.data.total;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //获取维修记录
+    getMaintainRecord() {
+      this.loading = true;
+      request({
+        url: "/maintain-record/getMaintainRecordByPage",
+        method: "post",
+        params: {
+          id: this.assetId,
+          pageIndex: this.pageIndex2,
+          pageSize: this.pageSize2,
+          keyword: this.keyword
+        }
+      })
+        .then(response => {
+          this.maintainData = response.data.records;
+          this.total2 = response.data.total;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //导出记录
+    exportRecord() {
+      this.loading = true;
+      request({
+        url: "/maintain-record/exportExcel",
+        method: "post",
+        params: {
+          id: this.assetId,
+          type: this.tabsName
+        }
+      })
+        .then(response => {
+          location.href = "http://localhost:9102/img" + response.data;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    downloadFile() {},
+
+    //改变关键字
+    changeKeyword() {
+      this.getManagementRecord();
+      this.getMaintainRecord();
+    },
+
+    //页码规格变更1
+    handleSizeChange1(val) {
+      this.pageSize1 = val;
+      this.getManagementRecord();
+    },
+
+    //页码页数变更1
+    handleCurrentChange1(val) {
+      this.pageIndex1 = val;
+      this.getManagementRecord();
+    },
+
+    //页码规格变更2
+    handleSizeChange2(val) {
+      this.pageSize2 = val;
+      this.getMaintainRecord();
+    },
+
+    //页码页数变更2
+    handleCurrentChange2(val) {
+      this.pageIndex2 = val;
+      this.getMaintainRecord();
+    },
+
+    //回到前一个页面
+    backToDetail() {
+      this.$router.go(-1);
+    }
+  },
+  mounted() {
+    this.getManagementRecord();
+    this.getMaintainRecord();
+  }
+};
+</script>
+
+<style scoped>
+</style>
+

+ 238 - 0
lss_frontend/src/views/assets/assetStatistic.vue

@@ -0,0 +1,238 @@
+<template>
+  <div class="app-container">
+    <!-- 头部区域 -->
+    <el-row>
+      <!-- 按类型检索 -->
+      <el-col class="item" :span="24">
+        <span class="title">检索类型</span>
+        <el-tag
+          :type="selectedType == -1? '': 'info'"
+          @click="getItems(-1, selectedState, selectedOrder, keyword, keywordType)"
+          style="cursor: pointer"
+        >全部</el-tag>
+        <el-tag
+          class="tag"
+          v-for="tag in tags"
+          :key="tag.id"
+          :type="selectedType == tag.id? '': 'info'"
+          @click="getItems(tag.id, selectedState, selectedOrder, keyword, keywordType)"
+        >{{tag.name}}</el-tag>
+      </el-col>
+      <!-- 按状态检索 -->
+      <el-col class="item" :span="24">
+        <span class="title">状态检索</span>
+        <el-tag
+          class="tag"
+          :type="selectedState == -1? null: 'info'"
+          @click="getItems(selectedType, -1, selectedOrder, keyword, keywordType)"
+        >全部</el-tag>
+        <el-tag
+          class="tag"
+          :type="selectedState == 0? null: 'info'"
+          @click="getItems(selectedType, 0, selectedOrder, keyword, keywordType)"
+        >未用</el-tag>
+        <el-tag
+          class="tag"
+          :type="selectedState == 1? null: 'info'"
+          @click="getItems(selectedType, 1, selectedOrder, keyword, keywordType)"
+        >已用</el-tag>
+        <el-tag
+          class="tag"
+          :type="selectedState == 2? null: 'info'"
+          @click="getItems(selectedType, 2, selectedOrder, keyword, keywordType)"
+        >待维护</el-tag>
+        <el-tag
+          class="tag"
+          :type="selectedState == 3? null: 'info'"
+          @click="getItems(selectedType, 3, selectedOrder, keyword, keywordType)"
+        >报废</el-tag>
+      </el-col>
+      <!-- 排序方式 -->
+      <el-col class="item" :span="24">
+        <span class="title">排序方式</span>
+        <el-tag
+          class="tag"
+          :type="selectedOrder == 0? '': 'info'"
+          @click="getItems(selectedType, selectedState, 0, keyword, keywordType)"
+        >登记时间倒序</el-tag>
+        <el-tag
+          class="tag"
+          :type="selectedOrder == 1? '': 'info'"
+          @click="getItems(selectedType, selectedState, 1, keyword, keywordType)"
+        >登记时间顺序</el-tag>
+      </el-col>
+      <!-- 关键字检索 -->
+      <el-col class="item" :span="24">
+        <span class="title">搜索名称</span>
+        <el-select v-model="keywordType" placeholder="请选择" size="small" style="width: 150px">
+          <el-option label="物品编号" value="1"></el-option>
+          <el-option label="使用人姓名" value="2"></el-option>
+        </el-select>
+        <el-input
+          v-model="keyword"
+          placeholder="输入关键字"
+          clearable
+          style="width: 250px"
+          @keyup.enter.native="getItems(selectedType, selectedState, selectedOrder, keyword, keywordType)"
+          size="small"
+          :disabled="keywordType == null"
+        ></el-input>
+        <el-button
+          @click="getItems(selectedType, selectedState, selectedOrder, keyword, keywordType)"
+          :disabled="keywordType == null"
+          size="small"
+        >搜索</el-button>
+      </el-col>
+    </el-row>
+
+    <!-- 列表区域 -->
+    <el-table :data="items" style="width: 100%" v-loading="loading">
+      <el-table-column type="index" width="40"></el-table-column>
+      <el-table-column label="资产图片" width="110">
+        <template slot-scope="scope">
+          <el-image
+            style="width: 100px; height: 100px"
+            :src="'http://localhost:9102/img' + scope.row.pic"
+          ></el-image>
+        </template>
+      </el-table-column>
+      <el-table-column prop="number" label="资产编号" width="130"></el-table-column>
+      <el-table-column prop="name" label="资产名称"></el-table-column>
+      <el-table-column prop="model_number" label="型号" width="100"></el-table-column>
+      <el-table-column prop="indate" label="登记时间" width="100"></el-table-column>
+      <el-table-column prop="tag_name" label="分类" width="80"></el-table-column>
+      <el-table-column prop="address" label="位置"></el-table-column>
+      <el-table-column prop="factory" label="生产厂家"></el-table-column>
+      <el-table-column prop="service_life" label="服务期限" width="80"></el-table-column>
+      <el-table-column prop="next_indate" label="下次维护日期" width="110"></el-table-column>
+      <el-table-column label="所属人员" width="80">
+        <template slot-scope="scope">
+          <span v-if="scope.row.username != null && scope.row.state == 1">{{scope.row.username}}</span>
+          <span v-else>无</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" width="60" fixed="right">
+        <template slot-scope="scope">
+          <span v-if="scope.row.state == 0">未用</span>
+          <span v-else-if="scope.row.state == 1">已用</span>
+          <span v-else-if="scope.row.state == 1">待维护</span>
+          <span v-else>报废</span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 页码区域 -->
+    <el-pagination
+      v-if="total > 0"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      :current-page="pageIndex"
+      :page-sizes="[20, 50, 100]"
+      :page-size="20"
+      layout="total, sizes, prev, pager, next, jumper"
+      :total="total"
+      background
+      style="float: right"
+    ></el-pagination>
+  </div>
+</template>
+
+<script>
+import request from "@/utils/request";
+export default {
+  data() {
+    return {
+      loading: false,
+      tags: null,
+      selectedType: -1,
+      selectedState: -1,
+      selectedOrder: 0,
+      keyword: "",
+      keywordType: 1,
+      items: [],
+      //分页相关
+      pageIndex: 1,
+      pageSize: 20,
+      total: 0
+    };
+  },
+  methods: {
+    //页码规格变更
+    handleSizeChange(val) {
+      this.pageSize = val;
+      //重新获取
+      this.getItems(
+        this.selectedType,
+        this.selectedState,
+        this.selectedOrder,
+        this.keyword,
+        this.keywordType
+      );
+    },
+
+    //页码页数变更
+    handleCurrentChange(val) {
+      this.pageIndex = val;
+      //重新获取
+      this.getItems(
+        this.selectedType,
+        this.selectedState,
+        this.selectedOrder,
+        this.keyword,
+        this.keywordType
+      );
+    },
+    //获取标签 其他全部数据
+    getItems(type, state, order, keyword, keywordType) {
+      //先给全局变量赋值
+      this.selectedType = type;
+      this.selectedState = state;
+      this.selectedOrder = order;
+      this.keyword = keyword;
+      this.keywordType = keywordType;
+      this.loading = true;
+      request({
+        url: "/goods-no/getAllItems",
+        method: "post",
+        params: {
+          pageIndex: this.pageIndex,
+          pageSize: this.pageSize,
+          type: type,
+          state: state,
+          order: order,
+          keyword: keyword,
+          keywordType: keyword == "" || keyword == null ? 0 : keywordType
+        }
+      })
+        .then(response => {
+          this.tags = response.data.tag;
+          this.items = response.data.item;
+          this.total = response.data.total;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    }
+  },
+  mounted() {
+    this.getItems(-1, -1, 0, "", 1);
+    this.keywordType = null;
+  }
+};
+</script>
+
+<style scoped>
+.title {
+  line-height: 32px;
+  font-size: 14px;
+}
+.tag {
+  margin: 0 5px 5px 0;
+  cursor: pointer;
+}
+.item {
+  margin-bottom: 5px;
+}
+</style>
+

+ 240 - 0
lss_frontend/src/views/crew/crewDetail.vue

@@ -0,0 +1,240 @@
+<template>
+  <div class="app-container">
+    <el-row style="margin-bottom: 20px">
+      <el-col :span="3">
+        <el-image
+          style="width: 100px; height: 100px; border-radius: 50px;"
+          :src="'http://localhost:9102/img' + user.headUrl"
+        ></el-image>
+      </el-col>
+      <el-col class="detail" :span="2">
+        <span>{{user.name}}</span>
+      </el-col>
+      <el-col class="detail" :span="4">
+        <span>{{user.phone}}</span>
+      </el-col>
+      <el-col class="detail" :span="4">
+        <span>本月得分</span>
+      </el-col>
+      <el-col class="detail" :span="4">
+        <span>所属资产</span>
+      </el-col>
+      <el-col class="detail" :span="7">
+        <el-button type="primary" @click="backToList" style="float:right">返回</el-button>
+      </el-col>
+      <el-col class="detail" :span="6">
+        <span>第{{user.deptId}}部门</span>
+      </el-col>
+      <el-col class="detail" :span="4">
+        <span>暂无</span>
+      </el-col>
+      <el-col class="detail" :span="11">
+        <span class="asset" v-for="item in asset" :key="item">{{item}}</span>
+      </el-col>
+    </el-row>
+
+    <!-- 标签页 -->
+    <el-tabs v-model="tabsName">
+      <el-tab-pane label="评分记录" name="0">
+        <!-- 评分记录列表 -->
+        <el-table :data="score" v-loading="loading" width="100%">
+          <el-table-column type="index" width="40"></el-table-column>
+          <el-table-column prop="scoringYearMonth" label="时间" width="100"></el-table-column>
+          <el-table-column prop="personalScore" label="个人得分" width="80"></el-table-column>
+          <el-table-column prop="ideaScore" label="建议得分" width="80"></el-table-column>
+          <el-table-column prop="total" label="总分" width="80"></el-table-column>
+        </el-table>
+        <!-- 页码区域 -->
+        <el-pagination
+          @size-change="handleSizeChange1"
+          @current-change="handleCurrentChange1"
+          :current-page="pageIndex1"
+          :page-sizes="[20, 50, 100]"
+          :page-size="20"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total1"
+          background
+          style="float:right"
+        ></el-pagination>
+      </el-tab-pane>
+
+      <!-- 建议记录列表 -->
+      <el-tab-pane label="建议记录" name="1">
+        <!-- 评分记录列表 -->
+        <el-table :data="advise" v-loading="loading">
+          <el-table-column type="index" width="40"></el-table-column>
+          <el-table-column prop="indate" label="建议时间" width="100"></el-table-column>
+          <el-table-column prop="content" label="建议内容"></el-table-column>
+          <el-table-column prop="ideaScore" label="评分人" width="80">
+            <template slot-scope="scope">
+              <span>{{reply[scope.$index].responder}}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="total" label="回复内容">
+            <template slot-scope="scope">
+              <span>{{reply[scope.$index].content}}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="score" label="得分" width="80"></el-table-column>
+          <el-table-column prop="indate" label="打分时间" width="80"></el-table-column>
+        </el-table>
+        <!-- 页码区域 -->
+        <el-pagination
+          @size-change="handleSizeChange2"
+          @current-change="handleCurrentChange2"
+          :current-page="pageIndex2"
+          :page-sizes="[20, 50, 100]"
+          :page-size="20"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total1"
+          background
+          style="float:right"
+        ></el-pagination>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script>
+import request from "@/utils/request";
+export default {
+  data() {
+    return {
+      user: {},
+      asset: [],
+      score: [],
+      advise: [],
+      reply: [],
+      userId: this.$route.params.id,
+      loading: false,
+      tabsName: 0,
+      //分页相关
+      pageIndex1: 1,
+      pageSize1: 20,
+      total1: 0,
+      pageIndex2: 1,
+      pageSize2: 20,
+      total2: 0
+    };
+  },
+  methods: {
+    //获取用户信息
+    getUser() {
+      this.loading = true;
+      request({
+        url: "/user/getUserById",
+        method: "post",
+        params: {
+          id: this.userId
+        }
+      })
+        .then(response => {
+          this.user = response.data.user;
+          this.asset = response.data.asset;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //获取评分
+    getScore() {
+      this.loading = true;
+      request({
+        url: "/score/getScoreByPage",
+        method: "post",
+        params: {
+          id: this.userId,
+          pageIndex: this.pageIndex1,
+          pageSize: this.pageSize1
+        }
+      })
+        .then(response => {
+          this.score = response.data.records;
+          this.total1 = response.data.total;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //获取评价
+    getAdvise() {
+      this.loading = true;
+      request({
+        url: "/score/getAdviseByPage",
+        method: "post",
+        params: {
+          id: this.userId,
+          pageIndex: this.pageIndex2,
+          pageSize: this.pageSize2
+        }
+      })
+        .then(response => {
+          this.advise = response.data.advise.records;
+          this.total2 = response.data.advise.total;
+          this.reply = response.data.reply;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //页码规格变更1
+    handleSizeChange1(val) {
+      this.pageSize1 = val;
+      this.getScore();
+    },
+
+    //页码页数变更1
+    handleCurrentChange1(val) {
+      this.pageIndex1 = val;
+      this.getScore();
+    },
+
+    //页码规格变更2
+    handleSizeChange2(val) {
+      this.pageSize2 = val;
+      this.getAdvise();
+    },
+
+    //页码页数变更2
+    handleCurrentChange2(val) {
+      this.pageIndex2 = val;
+      this.getAdvise();
+    },
+
+    //回到前一个页面
+    backToList() {
+      this.$router.go(-1);
+    }
+  },
+  mounted() {
+    if (this.userId == null) {
+      this.$router.go("/404");
+    } else {
+      this.getUser();
+      this.getScore();
+      this.getAdvise();
+    }
+  }
+};
+</script>
+
+<style scoped>
+.detail {
+  line-height: 50px;
+  height: 50px;
+}
+.detail span {
+  color: #444;
+  font-size: 14px;
+}
+.detail asset {
+  margin-right: 5px;
+}
+</style>
+

+ 202 - 0
lss_frontend/src/views/crew/crewList.vue

@@ -0,0 +1,202 @@
+<template>
+  <div class="app-container">
+    <el-row>
+      <el-col :span="24">
+        <el-button @click="openDialog" :loading="loading">分值统计日设置</el-button>
+        <el-button :loading="loading">导出通讯录N/A</el-button>
+      </el-col>
+    </el-row>
+
+    <!-- 列表区域 -->
+    <el-table :data="crews" style="width: 100%" v-loading="loading">
+      <el-table-column type="index" width="40"></el-table-column>
+      <el-table-column label="头像" width="110">
+        <template slot-scope="scope">
+          <el-image
+            style="width: 100px; height: 100px"
+            :src="'http://localhost:9102/img' + scope.row.headUrl"
+          ></el-image>
+        </template>
+      </el-table-column>
+      <el-table-column prop="name" label="姓名" width="70"></el-table-column>
+      <el-table-column prop="phone" label="手机号" width="110"></el-table-column>
+      <el-table-column prop="deptId" label="部门" width="80"></el-table-column>
+      <el-table-column prop="roleName" label="角色" width="80"></el-table-column>
+      <el-table-column label="对应资产">
+        <template slot-scope="scope">
+          <span
+            v-for="asset in assets[scope.$index]"
+            :key="asset"
+            style="margin-right: 10px"
+          >{{asset}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="80">
+        <template slot-scope="scope">
+          <el-button type="primary" @click="toDetail(scope.row.id)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div>
+      <!-- 打分提示 -->
+      <span class="tips" v-if="currentDate!=null">请于本月{{currentDate}}日前完成上月的打分</span>
+      <span class="tips" v-else>请设置打分截止日期</span>
+
+      <!-- 页码区域 -->
+      <el-pagination
+        v-if="total > 0"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+        :current-page="pageIndex"
+        :page-sizes="[20, 50, 100]"
+        :page-size="20"
+        layout="total, sizes, prev, pager, next, jumper"
+        :total="total"
+        background
+        style="float: right"
+      ></el-pagination>
+    </div>
+
+    <!-- 设置打分截止日期的dialog -->
+    <el-dialog title="设置打分截止日期" :visible.sync="dialogVisible" width="330px">
+      <el-form ref="form" :model="settingForm" :rules="rules" label-width="120px">
+        <el-form-item label="打分截止日期" prop="date">
+          <!-- 这里要限制数字类型 并且要小于等于28 -->
+          <el-input v-model="settingForm.date" placeholder="请输入截止日期" clearable></el-input>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="dialogVisible=false">取消</el-button>
+        <el-button type="primary" @click="editDeadline">提交</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import request from "@/utils/request";
+export default {
+  data() {
+    return {
+      crews: [],
+      assets: [],
+      settingForm: {
+        date: ""
+      },
+      currentDate: 0,
+      loading: false,
+      dialogVisible: false,
+      //分页相关
+      pageIndex: 1,
+      pageSize: 20,
+      total: 0,
+      rules: {
+        date: [{ required: true, message: "请输入截止时间", trigger: "blur" }]
+      }
+    };
+  },
+  methods: {
+    getCrews() {
+      this.loading = true;
+      request({
+        url: "/user/getUserByPage",
+        method: "post",
+        params: {
+          pageIndex: this.pageIndex,
+          pageSize: this.pageSize
+        }
+      })
+        .then(response => {
+          this.crews = response.data.user.records;
+          this.total = response.data.user.total;
+          this.assets = response.data.asset;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //获取截止日期
+    getDeadline() {
+      request({
+        url: "/user/getScoringDeadline",
+        method: "post"
+      })
+        .then(response => {
+          this.currentDate = response.data;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //修改截止日期
+    editDeadline() {
+      this.$refs.form.validate(valid => {
+        if (valid) {
+          this.loading = true;
+          request({
+            url: "/user/setScoringDeadline",
+            method: "post",
+            params: {
+              date: this.settingForm.date
+            }
+          })
+            .then(response => {
+              this.getDeadline();
+              this.loading = false;
+              this.dialogVisible = false;
+            })
+            .catch(error => {
+              this.loading = false;
+            });
+        }
+      });
+    },
+
+    //打开设置截止日期的dialog
+    openDialog() {
+      this.settingForm.date = this.currentDate;
+      this.dialogVisible = true;
+    },
+
+    //到详情页面
+    toDetail(id) {
+      if (id != null) {
+        this.$router.push("/crew/" + id);
+      } else {
+        this.$message({
+          message: "发生错误",
+          type: "error"
+        });
+      }
+    },
+
+    //页码规格变更
+    handleSizeChange(val) {
+      this.pageSize = val;
+      this.getCrews();
+    },
+
+    //页码页数变更
+    handleCurrentChange(val) {
+      this.pageIndex = val;
+      this.getCrews();
+    }
+  },
+  mounted() {
+    this.getCrews();
+    this.getDeadline();
+  }
+};
+</script>
+
+<style scoped>
+.tips {
+  line-height: 32px;
+}
+</style>
+

+ 82 - 0
lss_frontend/src/views/crew/record.vue

@@ -0,0 +1,82 @@
+<template>
+  <div class="app-container">
+    <!-- 列表区域 -->
+    <el-table :data="record" style="width: 100%" v-loading="loading">
+      <el-table-column type="index" width="40"></el-table-column>
+      <el-table-column prop="content" label="操作"></el-table-column>
+      <el-table-column prop="operator" label="操作人" width="80"></el-table-column>
+      <el-table-column prop="uname" label="操作对象" width="80"></el-table-column>
+      <el-table-column prop="indate" label="时间" width="100"></el-table-column>
+    </el-table>
+
+    <!-- 页码区域 -->
+    <el-pagination
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      :current-page="pageIndex"
+      :page-sizes="[20, 50, 100]"
+      :page-size="20"
+      layout="total, sizes, prev, pager, next, jumper"
+      :total="total"
+      background
+      style="float: right"
+    ></el-pagination>
+  </div>
+</template>
+
+<script>
+import request from "@/utils/request";
+export default {
+  data() {
+    return {
+      record: [],
+      loading: false,
+      //分页相关
+      pageIndex: 1,
+      pageSize: 20,
+      total: 0
+    };
+  },
+  methods: {
+    //获取记录
+    getRecord() {
+      this.loading = true;
+      request({
+        url: "/scoring-operate-record/getScoreRecordByPage",
+        method: "post",
+        params: {
+          pageIndex: this.pageIndex,
+          pageSize: this.pageSize
+        }
+      })
+        .then(response => {
+          this.record = response.data.records;
+          this.total = response.data.total;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //页码规格变更
+    handleSizeChange(val) {
+      this.pageSize = val;
+      this.getRecord();
+    },
+
+    //页码页数变更
+    handleCurrentChange(val) {
+      this.pageIndex = val;
+      this.getRecord();
+    }
+  },
+  mounted() {
+    this.getRecord();
+  }
+};
+</script>
+
+<style scoped>
+</style>
+

+ 118 - 0
lss_frontend/src/views/crew/score.vue

@@ -0,0 +1,118 @@
+<template>
+  <div class="app-container">
+    <el-row>
+      <el-col :span="24">
+        <el-button @click="toRecord">操作记录</el-button>
+        <el-button>导出得分N/A</el-button>
+      </el-col>
+    </el-row>
+
+    <!-- 列表区域 -->
+    <el-table :data="user" style="width: 100%" v-loading="loading">
+      <el-table-column type="index" width="40"></el-table-column>
+      <el-table-column prop="name" label="姓名"></el-table-column>
+      <el-table-column prop="phone" label="手机号" width="110"></el-table-column>
+      <el-table-column prop="deptId" label="部门" width="80"></el-table-column>
+      <el-table-column label="个人得分" width="80">
+        <template slot-scope="scope">
+          <span>{{score[scope.$index] == null? null: score[scope.$index].personalScore}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="建议得分" width="80">
+        <template slot-scope="scope">
+          <span>{{score[scope.$index] == null? null: score[scope.$index].ideaScore}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="总得分" width="80">
+        <template slot-scope="scope">
+          <span>{{score[scope.$index] == null? null: score[scope.$index].total}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="评分人" width="100">
+        <template slot-scope="scope">
+          <span>{{score[scope.$index] == null? null: score[scope.$index].rater}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="indate" label="时间" width="100"></el-table-column>
+    </el-table>
+
+    <!-- 页码区域 -->
+    <el-pagination
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      :current-page="pageIndex"
+      :page-sizes="[20, 50, 100]"
+      :page-size="20"
+      layout="total, sizes, prev, pager, next, jumper"
+      :total="total"
+      background
+      style="float: right"
+    ></el-pagination>
+  </div>
+</template>
+
+<script>
+import request from "@/utils/request";
+export default {
+  data() {
+    return {
+      idea: [],
+      score: [],
+      user: [],
+      loading: false,
+      //分页相关
+      pageIndex: 1,
+      pageSize: 20,
+      total: 0
+    };
+  },
+  methods: {
+    //获取全部分数统计信息
+    getAllScore() {
+      this.loading = true;
+      request({
+        url: "/score/getAllScore",
+        method: "post",
+        params: {
+          pageIndex: this.pageIndex,
+          pageSize: this.pageSize
+        }
+      })
+        .then(response => {
+          this.idea = response.data.idea.records;
+          this.score = response.data.score;
+          this.user = response.data.user;
+          this.total = response.data.idea.total;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //到记录页面
+    toRecord() {
+      this.$router.push("/record");
+    },
+
+    //页码规格变更
+    handleSizeChange(val) {
+      this.pageSize = val;
+      this.getAllScore();
+    },
+
+    //页码页数变更
+    handleCurrentChange(val) {
+      this.pageIndex = val;
+      this.getAllScore();
+    }
+  },
+  mounted() {
+    this.getAllScore();
+  }
+};
+</script>
+
+<style scoped>
+</style>
+

+ 203 - 0
lss_frontend/src/views/login.vue

@@ -0,0 +1,203 @@
+<template>
+    <div class="login-container">
+        <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">
+            <div class="title-container">
+                <h3 class="title">机柜管理系统</h3>
+            </div>
+
+            <el-form-item prop="username">
+                <span class="svg-container">
+                    <svg-icon icon-class="user" />
+                </span>
+                <el-input ref="username" v-model="loginForm.username" 
+                    placeholder="请输入用户名" name="username" type="text" tabindex="1" auto-complete="on"/>
+            </el-form-item>
+
+            <el-form-item prop="password">
+                <span class="svg-container">
+                    <svg-icon icon-class="password" />
+                </span>
+                <el-input :key="passwordType" ref="password" v-model="loginForm.password" :type="passwordType" 
+                    placeholder="请输入密码" name="password" tabindex="2" auto-complete="on" @keyup.enter.native="handleLogin"/>
+                <span class="show-pwd" @click="showPwd">
+                    <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
+                </span>
+            </el-form-item>
+
+            <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">
+                登录
+            </el-button>
+        </el-form>
+    </div>
+</template>
+
+<script>
+    import request from "@/utils/request";
+
+    export default {
+        name: "Login",
+        data() {
+            return {
+                loginForm: {
+                    username: "",
+                    password: ""
+                },
+                loginRules: {
+                    username: [
+                        { required: true, message: "请输入用户名", trigger: "blur" }
+                    ],
+                    password: [
+                        { required: true, message: "请输入密码", trigger: "blur" }
+                    ]
+                },
+                loading: false,
+                passwordType: "password",
+                redirect: undefined,
+            };
+        },
+        watch: {
+            $route: {
+                handler: function(route) {
+                    this.redirect = route.query && route.query.redirect;
+                },
+                immediate: true
+            }
+        },
+        methods: {
+            showPwd() {
+                if (this.passwordType === "password") {
+                    this.passwordType = "";
+                } else {
+                    this.passwordType = "password";
+                }
+                this.$nextTick(() => {
+                    this.$refs.password.focus();
+                });
+            },
+            handleLogin() {
+                this.$refs.loginForm.validate(valid => {
+                    if (valid) {
+                        this.loading = true;
+                        this.$store
+                        .dispatch("permission/login", this.loginForm)
+                        .then(() => {
+                            this.$router.push("/");
+                            this.loading = false;
+                        })
+                        .catch(() => {
+                            this.loading = false;
+                        });
+                    }
+                });
+            }
+        }
+    };
+</script>
+
+<style lang="scss">
+    $bg: #283443;
+    $light_gray: #fff;
+    $cursor: #fff;
+
+    @supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
+        .login-container .el-input input {
+            color: $cursor;
+        }
+    }
+
+    /* reset element-ui css */
+    .login-container {
+        .el-input {
+            display: inline-block;
+            height: 47px;
+            width: 85%;
+
+            input {
+                background: transparent;
+                border: 0px;
+                -webkit-appearance: none;
+                border-radius: 0px;
+                padding: 12px 5px 12px 15px;
+                color: $light_gray;
+                height: 47px;
+                caret-color: $cursor;
+
+                &:-webkit-autofill {
+                    box-shadow: 0 0 0px 1000px $bg inset !important;
+                    -webkit-text-fill-color: $cursor !important;
+                }
+            }
+        }
+
+        .el-form-item {
+            border: 1px solid rgba(255, 255, 255, 0.1);
+            background: rgba(0, 0, 0, 0.1);
+            border-radius: 5px;
+            color: #454545;
+        }
+    }
+</style>
+
+<style lang="scss" scoped>
+    $bg: #2d3a4b;
+    $dark_gray: #889aa4;
+    $light_gray: #eee;
+
+    .login-container {
+        min-height: 100%;
+        width: 100%;
+        background-color: $bg;
+        overflow: hidden;
+
+        .login-form {
+            position: relative;
+            width: 520px;
+            max-width: 100%;
+            padding: 160px 35px 0;
+            margin: 0 auto;
+            overflow: hidden;
+        }
+
+        .tips {
+            font-size: 14px;
+            color: #fff;
+            margin-bottom: 10px;
+
+            span {
+                &:first-of-type {
+                    margin-right: 16px;
+                }
+            }
+        }
+
+        .svg-container {
+            padding: 6px 5px 6px 15px;
+            color: $dark_gray;
+            vertical-align: middle;
+            width: 30px;
+            display: inline-block;
+        }
+
+        .title-container {
+            position: relative;
+
+            .title {
+                font-size: 26px;
+                color: $light_gray;
+                margin: 0px auto 40px auto;
+                text-align: center;
+                font-weight: bold;
+            }
+        }
+
+        .show-pwd {
+            position: absolute;
+            right: 10px;
+            top: 7px;
+            font-size: 16px;
+            color: $dark_gray;
+            cursor: pointer;
+            user-select: none;
+        }
+    }
+</style>

+ 16 - 0
lss_frontend/src/views/starter.vue

@@ -0,0 +1,16 @@
+<template>
+    <div class="app-container">
+        <el-button type="primary">按钮</el-button>
+    </div>
+</template>
+
+<script>
+    export default {
+        data() {
+            return {};
+        },
+        methods: {},
+        mounted() {}
+    };
+</script>
+

+ 222 - 0
lss_frontend/src/views/system/institution.vue

@@ -0,0 +1,222 @@
+<template>
+  <div class="app-container">
+    <el-button class="add-button" @click="openDialog">添加制度</el-button>
+
+    <el-row>
+      <el-col :span="24" v-loading="loading">
+        <div class="card" v-for="institution in institutions" :key="institution.id">
+          <div class="title">
+            <span style="cursor: pointer;" @click="toDetail(institution.id)">{{institution.title}}</span>
+            <span class="button" @click="toDetail(institution.id)">查看详情</span>
+            <span
+              class="button"
+              @click="deleteInstitution(institution.id, institution.title)"
+              style="margin-right: 15px"
+            >删除制度</span>
+          </div>
+          <div class="content" @click="toDetail(institution.id)">{{institution.content}}</div>
+        </div>
+      </el-col>
+    </el-row>
+
+    <!-- 页码区域 -->
+    <el-pagination
+      v-if="total > 0"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      :current-page="pageIndex"
+      :page-sizes="[20, 50, 100]"
+      :page-size="20"
+      layout="total, sizes, prev, pager, next, jumper"
+      :total="total"
+      background
+      style="float: right"
+    ></el-pagination>
+
+    <!-- 添加的dialog -->
+    <el-dialog title="添加制度" :visible.sync="dialogVisible" width="600px">
+      <el-form ref="form1" :model="institutionForm" :rules="rules" label-width="110px">
+        <el-form-item label="制度标题" prop="title">
+          <el-input v-model="institutionForm.title" placeholder="请输入制度标题" clearable></el-input>
+        </el-form-item>
+        <el-form-item label="制度内容" prop="content">
+          <el-input v-model="institutionForm.content" placeholder="请输入制度内容" clearable></el-input>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="dialogVisible=false">取消</el-button>
+        <el-button type="primary" @click="addInstitution">提交</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import request from "@/utils/request";
+export default {
+  data() {
+    return {
+      institutions: [],
+      loading: false,
+      institutionForm: {
+        title: "",
+        content: ""
+      },
+      dialogVisible: false,
+      rules: {
+        title: [{ required: true, message: "请输入制度标题", trigger: "blur" }],
+        content: [
+          { required: true, message: "请输入制度内容", trigger: "blur" }
+        ]
+      },
+      //分页相关
+      pageIndex: 1,
+      pageSize: 20,
+      total: 0
+    };
+  },
+  methods: {
+    //页码规格变更
+    handleSizeChange(val) {
+      this.pageSize = val;
+      //重新获取
+      this.getInstitution();
+    },
+
+    //页码页数变更
+    handleCurrentChange(val) {
+      this.pageIndex = val;
+      //重新获取
+      this.getInstitution();
+    },
+
+    //增加制度
+    addInstitution() {
+      this.$refs.form1.validate(valid => {
+        if (valid) {
+          this.loading = true;
+          request({
+            url: "/institution/insertInstitution",
+            method: "post",
+            params: {
+              title: this.institutionForm.title,
+              content: this.institutionForm.content
+            }
+          })
+            .then(response => {
+              this.$message({
+                message: "添加制度成功",
+                type: "success"
+              });
+              this.loading = false;
+              this.dialogVisible = false;
+              this.getInstitution();
+            })
+            .catch(error => {
+              this.loading = false;
+            });
+        }
+      });
+    },
+
+    //删除制度
+    deleteInstitution(id, title) {
+      this.$confirm("是否删除" + title, "删除", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      })
+        .then(() => {
+          this.loading = true;
+          request({
+            url: "/institution/deleteInstitution",
+            method: "post",
+            params: {
+              id: id
+            }
+          })
+            .then(response => {
+              this.$message({
+                message: "删除成功",
+                type: "success"
+              });
+              this.loading = false;
+              this.getInstitution();
+            })
+            .catch(error => {
+              this.loading = false;
+            });
+        })
+        .catch(() => {});
+    },
+
+    //获取制度
+    getInstitution() {
+      this.loading = true;
+      request({
+        url: "/institution/getInstitutionsByPage",
+        method: "post",
+        params: {
+          pageIndex: this.pageIndex,
+          pageSize: this.pageSize
+        }
+      })
+        .then(response => {
+          this.institutions = response.data.records;
+          this.total = response.data.total;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //打开添加的dialog
+    openDialog() {
+      this.institutionForm.title = "";
+      this.institutionForm.content = "";
+      this.dialogVisible = true;
+    },
+
+    //到详情
+    toDetail(id) {
+      if (id != null) {
+        this.$router.push("/institution/" + id);
+      } else {
+        this.$message({
+          message: "发生错误",
+          type: "error"
+        });
+      }
+    }
+  },
+  mounted() {
+    this.getInstitution();
+  }
+};
+</script>
+
+<style scoped>
+.card {
+  margin-bottom: 20px;
+}
+.card .title {
+  margin-bottom: 10px;
+  font-size: 18px;
+  cursor: pointer;
+}
+.card .title .button {
+  float: right;
+  color: #409eff;
+  font-size: 16px;
+  cursor: pointer;
+}
+.card .content {
+  font-size: 15px;
+  color: #aaa;
+  cursor: pointer;
+}
+.add-button {
+  margin-bottom: 15px;
+}
+</style>

+ 323 - 0
lss_frontend/src/views/system/institutionDetail.vue

@@ -0,0 +1,323 @@
+<template>
+  <div class="app-container">
+    <el-row>
+      <el-col :span="24" v-loading="loading">
+        <div class="title">
+          {{institution.title}}
+          <el-button class="button" type="primary" @click="goBack" size="small">返回</el-button>
+          <el-button class="button" @click="openDialog" size="small">修改</el-button>
+          <el-button class="button" type="danger" @click="deleteInstitution" size="small">删除</el-button>
+        </div>
+        <div class="content">{{institution.content}}</div>
+      </el-col>
+    </el-row>
+    <el-divider></el-divider>
+
+    <!-- 上传文件 -->
+    <el-button type="primary" @click="openFileDialog" size="small">添加附件</el-button>
+
+    <!-- 文件显示 -->
+    <p class="file" v-if="files.length==0">暂无文件</p>
+    <p class="file" v-for="file in files" :key="file.id">
+      <a
+        class="name"
+        :href="'http://localhost:9102/img' + file.url"
+        :download="file.name"
+      >{{file.name}}</a>
+      <el-button
+        type="danger"
+        size="small"
+        @click="deleteFile(file.id, file.name)"
+        :loading="loading"
+      >删除</el-button>
+    </p>
+
+    <!-- 上传文件的dialog -->
+    <el-dialog title="上传附件" :visible.sync="fileDialogVisible" @closed="clearFile" width="400px">
+      <el-upload
+        ref="upload"
+        action="customize"
+        :http-request="uploadDiscardFile"
+        :limit="10"
+        multiple
+        :before-remove="beforeRemove"
+      >
+        <el-button :loading="loading">添加文件</el-button>
+      </el-upload>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="fileDialogVisible=false">取消</el-button>
+        <el-button type="primary" @click="uploadFile" :loading="loading">上传</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 修改的dialog -->
+    <el-dialog title="修改制度" :visible.sync="dialogVisible" width="600px">
+      <el-form ref="form1" :model="institutionForm" :rules="rules" label-width="110px">
+        <el-form-item label="制度标题" prop="title">
+          <el-input v-model="institutionForm.title" placeholder="请输入制度标题" clearable></el-input>
+        </el-form-item>
+        <el-form-item label="制度内容" prop="content">
+          <el-input v-model="institutionForm.content" placeholder="请输入制度内容" clearable></el-input>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="dialogVisible=false">取消</el-button>
+        <el-button type="primary" @click="editInstitution">提交</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import request from "@/utils/request";
+export default {
+  data() {
+    return {
+      institutionId: this.$route.params.id,
+      institution: {}, //制度信息
+      files: [], //制度附件
+      uploadingFiles: [], //要上传的文件
+      loading: false,
+      institutionForm: {
+        title: "",
+        content: ""
+      },
+      dialogVisible: false,
+      fileDialogVisible: false,
+      rules: {
+        title: [{ required: true, message: "请输入制度标题", trigger: "blur" }],
+        content: [
+          { required: true, message: "请输入制度内容", trigger: "blur" }
+        ]
+      }
+    };
+  },
+  methods: {
+    //根据本页的id获取制度详情
+    getInstitution() {
+      this.loading = true;
+      request({
+        url: "/institution/getInstitutionById",
+        method: "post",
+        params: {
+          id: this.institutionId
+        }
+      })
+        .then(response => {
+          this.institution = response.data.institution;
+          this.files = response.data.files;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //单独获取文件
+    getFiles() {
+      this.loading = true;
+      request({
+        url: "/institution/getFiles",
+        method: "post",
+        params: {
+          id: this.institutionId
+        }
+      })
+        .then(response => {
+          this.files = response.data;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //删除文件
+    deleteFile(id, name) {
+      this.$confirm("是否删除文件" + name, "删除", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      })
+        .then(() => {
+          this.loading = true;
+          request({
+            url: "/institution/deleteFileById",
+            method: "post",
+            params: {
+              id: id
+            }
+          })
+            .then(response => {
+              this.$message({
+                message: "删除成功",
+                type: "success"
+              });
+              this.loading = false;
+              this.getFiles();
+            })
+            .catch(error => {
+              this.loading = false;
+            });
+        })
+        .catch(() => {});
+    },
+
+    //上传文件
+    uploadFile() {
+      if (this.uploadingFiles != []) {
+        this.loading = true;
+        var form = new FormData();
+        form.append("id", this.institutionId);
+        this.uploadingFiles.forEach(file => {
+          form.append("files", file);
+        });
+        request({
+          url: "/institution/uploadFiles",
+          method: "post",
+          data: form
+        })
+          .then(response => {
+            this.$message({
+              message: "上传成功",
+              type: "success"
+            });
+            this.loading = false;
+            this.getFiles();
+            this.fileDialogVisible = false;
+          })
+          .catch(error => {
+            this.loading = false;
+          });
+      } else {
+        this.$message({
+          message: "请选择要上传的文件",
+          type: "error"
+        });
+      }
+    },
+
+    //清除文件
+    clearFile() {
+      this.$refs.upload.clearFiles();
+    },
+
+    //删除制度
+    deleteInstitution() {
+      this.$confirm("是否删除本篇制度", "删除", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      })
+        .then(() => {
+          request({
+            url: "/institution/deleteInstitution",
+            method: "post",
+            params: {
+              id: this.institutionId
+            }
+          })
+            .then(response => {
+              this.$message({
+                message: "删除成功",
+                type: "success"
+              });
+              this.$router.go(-1);
+            })
+            .catch(error => {});
+        })
+        .catch(() => {});
+    },
+
+    //打开dialog
+    openDialog() {
+      this.institutionForm.title = this.institution.title;
+      this.institutionForm.content = this.institution.content;
+      this.dialogVisible = true;
+    },
+
+    //打开上传文件Dialog
+    openFileDialog() {
+      this.uploadingFiles = [];
+      this.fileDialogVisible = true;
+    },
+
+    //编辑制度
+    editInstitution() {
+      this.$refs.form1.validate(valid => {
+        if (valid) {
+          this.loading = true;
+          request({
+            url: "/institution/insertInstitution",
+            method: "post",
+            params: {
+              id: this.institutionId,
+              title: this.institutionForm.title,
+              content: this.institutionForm.content
+            }
+          })
+            .then(response => {
+              this.$message({
+                message: "编辑制度成功",
+                type: "success"
+              });
+              this.loading = false;
+              this.dialogVisible = false;
+              this.getInstitution();
+            })
+            .catch(error => {
+              this.loading = false;
+            });
+        }
+      });
+    },
+
+    //获取上传的文件
+    uploadDiscardFile(params) {
+      this.uploadingFiles.push(params.file);
+      console.log(this.uploadingFiles);
+      return false;
+    },
+
+    //文件上传的移除操作
+    beforeRemove() {
+      this.uploadingFiles = [];
+    },
+
+    //回到列表页
+    goBack() {
+      this.$router.go(-1);
+    }
+  },
+  mounted() {
+    //如果没有传进来id的话
+    if (this.institutionId == null) {
+      this.$router.go(-1);
+    } else {
+      this.getInstitution();
+    }
+  }
+};
+</script>
+
+<style scoped>
+.button {
+  float: right;
+  margin: 0 5px 0 0;
+}
+.title {
+  font-size: 22px;
+  margin-bottom: 20px;
+}
+.content {
+  font-size: 18px;
+  color: #aaa;
+}
+.file .name {
+  font-size: 18px;
+  color: #409eff;
+  cursor: pointer;
+  margin-right: 15px;
+}
+</style>
+

+ 19 - 0
lss_frontend/src/views/system/log.vue

@@ -0,0 +1,19 @@
+<template>
+  <div class="app-container">
+    <el-button type="primary">按钮</el-button>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {};
+  },
+  methods: {},
+  mounted() {}
+};
+</script>
+
+<style scoped>
+</style>
+

+ 315 - 0
lss_frontend/src/views/system/permission.vue

@@ -0,0 +1,315 @@
+<template>
+  <div class="app-container">
+    <!-- 权限 -->
+    <el-tabs tab-position="left" style="height: 100%;" v-model="tabsIndex">
+      <el-button style="float:right" @click="openDialog(null)">添加人员</el-button>
+      <el-tab-pane label="普通员工" name="0">
+        <!-- 列表 -->
+        <el-table :data="crew1" v-loading="loading" width="100%">
+          <el-table-column type="index" width="40"></el-table-column>
+          <el-table-column label="头像" width="110">
+            <template slot-scope="scope">
+              <el-image
+                style="width: 100px; height: 100px"
+                :src="'http://localhost:9102/img' + scope.row.headUrl"
+              ></el-image>
+            </template>
+          </el-table-column>
+          <el-table-column prop="name" label="姓名"></el-table-column>
+          <el-table-column prop="phone" label="手机号" width="110"></el-table-column>
+          <el-table-column prop="deptId" label="部门" width="80"></el-table-column>
+          <el-table-column prop="roleName" label="角色" width="80"></el-table-column>
+          <el-table-column label="操作" width="100">
+            <template slot-scope="scope">
+              <el-button @click="openDialog(scope.row.id)">转移</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <!-- 页码区域 -->
+        <el-pagination
+          @size-change="handleSizeChange1"
+          @current-change="handleCurrentChange1"
+          :current-page="pageIndex1"
+          :page-sizes="[20, 50, 100]"
+          :page-size="20"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total1"
+          background
+          style="float:right"
+        ></el-pagination>
+      </el-tab-pane>
+      <el-tab-pane label="领导" name="1">
+        <!-- 列表 -->
+        <el-table :data="crew2" v-loading="loading" width="100%">
+          <el-table-column type="index" width="40"></el-table-column>
+          <el-table-column label="头像" width="110">
+            <template slot-scope="scope">
+              <el-image
+                style="width: 100px; height: 100px"
+                :src="'http://localhost:9102/img' + scope.row.headUrl"
+              ></el-image>
+            </template>
+          </el-table-column>
+          <el-table-column prop="name" label="姓名"></el-table-column>
+          <el-table-column prop="phone" label="手机号" width="110"></el-table-column>
+          <el-table-column prop="deptId" label="部门" width="80"></el-table-column>
+          <el-table-column prop="roleName" label="角色" width="80"></el-table-column>
+          <el-table-column label="操作" width="100">
+            <template slot-scope="scope">
+              <el-button @click="openDialog(scope.row.id)">转移</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <!-- 页码区域 -->
+        <el-pagination
+          @size-change="handleSizeChange2"
+          @current-change="handleCurrentChange2"
+          :current-page="pageIndex2"
+          :page-sizes="[20, 50, 100]"
+          :page-size="20"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total2"
+          background
+          style="float:right"
+        ></el-pagination>
+      </el-tab-pane>
+      <el-tab-pane label="操作员" name="2">
+        <!-- 列表 -->
+        <el-table :data="crew3" v-loading="loading" width="100%">
+          <el-table-column type="index" width="40"></el-table-column>
+          <el-table-column label="头像" width="110">
+            <template slot-scope="scope">
+              <el-image
+                style="width: 100px; height: 100px"
+                :src="'http://localhost:9102/img' + scope.row.headUrl"
+              ></el-image>
+            </template>
+          </el-table-column>
+          <el-table-column prop="name" label="姓名"></el-table-column>
+          <el-table-column prop="phone" label="手机号" width="110"></el-table-column>
+          <el-table-column prop="deptId" label="部门" width="80"></el-table-column>
+          <el-table-column prop="roleName" label="角色" width="80"></el-table-column>
+          <el-table-column label="操作" width="100">
+            <template slot-scope="scope">
+              <el-button @click="openDialog(scope.row.id)">转移</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <!-- 页码区域 -->
+        <el-pagination
+          @size-change="handleSizeChange3"
+          @current-change="handleCurrentChange3"
+          :current-page="pageIndex3"
+          :page-sizes="[20, 50, 100]"
+          :page-size="20"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total3"
+          background
+          style="float:right"
+        ></el-pagination>
+      </el-tab-pane>
+    </el-tabs>
+
+    <!-- 转移的dialog -->
+    <el-dialog title="权限设置" :visible.sync="dialogVisible" width="600px">
+      <el-form ref="form1" :model="switchForm" :rules="rules" label-width="110px">
+        <el-form-item v-if="switchForm.single == false" label="选择人员" prop="ids">
+          <el-select
+            v-model="switchForm.ids"
+            multiple
+            filterable
+            placeholder="请选择人员"
+            style="width: 100%"
+          >
+            <el-option v-for="item in user" :key="item.id" :label="item.name" :value="item.id"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="角色" prop="roleId">
+          <el-select v-model="switchForm.roleId" placeholder="请选择角色" style="width: 100%">
+            <el-option label="普通员工" value="1"></el-option>
+            <el-option label="领导" value="2"></el-option>
+            <el-option label="操作员" value="3"></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="dialogVisible=false">取消</el-button>
+        <el-button type="primary" @click="switchRole">提交</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import request from "@/utils/request";
+export default {
+  data() {
+    return {
+      crew1: [],
+      crew2: [],
+      crew3: [],
+      user: [],
+      switchForm: {
+        ids: [],
+        roleId: 1,
+        single: false
+      },
+      dialogVisible: false,
+      loading: false,
+      tabsIndex: 0,
+      //分页相关
+      pageIndex1: 1,
+      pageSize1: 20,
+      total1: 0,
+      pageIndex2: 1,
+      pageSize2: 20,
+      total2: 0,
+      pageIndex3: 1,
+      pageSize3: 20,
+      total3: 0,
+      rules: {
+        ids: [{ required: true, message: "请选择人员", trigger: "blur" }],
+        roleId: [{ required: true, message: "请选择角色", trigger: "blur" }]
+      }
+    };
+  },
+  methods: {
+    //获取列表
+    getCrew(roleId) {
+      this.loading = true;
+      request({
+        url: "/permission/getCrewByPage",
+        method: "post",
+        params: {
+          pageIndex: this.pageIndex1,
+          pageSize: this.pageSize1,
+          type: roleId
+        }
+      })
+        .then(response => {
+          switch (roleId) {
+            case 1:
+              this.crew1 = response.data.user;
+              this.total1 = response.data.page.total;
+              break;
+            case 2:
+              this.crew2 = response.data.user;
+              this.total2 = response.data.page.total;
+              break;
+            case 3:
+              this.crew3 = response.data.user;
+              this.total3 = response.data.page.total;
+              break;
+          }
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //变更角色
+    switchRole() {
+      this.$refs.form1.validate(valid => {
+        if (valid) {
+          this.loading = true;
+          var form = new FormData();
+          form.append("roleId", this.switchForm.roleId);
+          this.switchForm.ids.forEach(id => {
+            form.append("ids", id);
+          });
+          request({
+            url: "/permission/switchRole",
+            method: "post",
+            data: form
+          })
+            .then(response => {
+              this.loading = false;
+              this.refresh();
+              this.dialogVisible = false;
+            })
+            .catch(error => {
+              this.loading = false;
+              this.dialogVisible = false;
+            });
+        }
+      });
+    },
+
+    //打开dailog
+    openDialog(id) {
+      this.getUser();
+      this.switchForm.ids = id == null ? [] : [id];
+      this.switchForm.single = id != null;
+      this.switchForm.roleId = null;
+      this.dialogVisible = true;
+    },
+
+    //获取下拉列表的全部人员
+    getUser(user) {
+      this.loading = true;
+      request({
+        url: "/permission/getSimpleUserList",
+        method: "post"
+      })
+        .then(response => {
+          this.user = response.data;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //页码规格变更1
+    handleSizeChange1(val) {
+      this.pageSize1 = val;
+      this.getCrew(1);
+    },
+
+    //页码页数变更1
+    handleCurrentChange1(val) {
+      this.pageIndex1 = val;
+      this.getCrew(1);
+    },
+
+    //页码规格变更2
+    handleSizeChange2(val) {
+      this.pageSize2 = val;
+      this.getCrew(2);
+    },
+
+    //页码页数变更2
+    handleCurrentChange2(val) {
+      this.pageIndex2 = val;
+      this.getCrew(2);
+    },
+
+    //页码规格变更3
+    handleSizeChange3(val) {
+      this.pageSize3 = val;
+      this.getCrew(3);
+    },
+
+    //页码页数变更3
+    handleCurrentChange3(val) {
+      this.pageIndex3 = val;
+      this.getCrew(3);
+    },
+
+    //全员恶人
+    refresh() {
+      this.getCrew(1);
+      this.getCrew(2);
+      this.getCrew(3);
+    }
+  },
+  mounted() {
+    this.refresh();
+  }
+};
+</script>
+
+<style scoped>
+</style>
+

+ 474 - 0
lss_frontend/src/views/task/taskDetail.vue

@@ -0,0 +1,474 @@
+<template>
+  <div class="app-container">
+    <el-row class="top">
+      <el-col :span="24">
+        <el-button type="primary" @click="backToList">返回</el-button>
+        <el-button
+          class="button"
+          type="danger"
+          v-if="task.state != 8 && task.publishId == currentUserId"
+          @click="deleteTask"
+        >删除</el-button>
+        <el-button class="button" @click="openDialog3">审核</el-button>
+        <el-button class="button" @click="openDialog2">提交</el-button>
+        <el-button
+          class="button"
+          @click="openDialog1"
+          v-if="task.state == 0 ||task.state == 1 ||task.state == 2 ||task.state == 4 && task.publishId == currentUserId"
+        >延期</el-button>
+        <el-button
+          class="button"
+          v-if="task.state == 0 && task.recipientId == currentUserId"
+          @click="receiveTask"
+        >接收</el-button>
+      </el-col>
+    </el-row>
+
+    <div class="block" style="line-height: 35px">
+      <span class="title">{{task.content}}</span>
+      <span class="state" v-if="task.state == 0">待派发</span>
+      <span class="state" v-else-if="task.state == 1">已派发</span>
+      <span class="state" v-else-if="task.state == 2">已接收</span>
+      <span class="state" v-else-if="task.state == 3">待审核</span>
+      <span class="state" v-else-if="task.state == 4">未通过</span>
+      <span class="state" v-else-if="task.state == 5">已完成</span>
+      <span class="state" v-else-if="task.state == 6">已延期</span>
+      <span class="state" v-else-if="task.state == 7">已失效</span>
+      <span class="state" v-else-if="task.state == 8">已删除</span>
+      <span class="state" v-else>不明</span>
+    </div>
+    <div class="block">
+      <span class="item">任务编码:{{task.code}}</span>
+      <span class="item">任务类型:{{task.tagName}}</span>
+      <span class="item">工作量:{{task.workLoad}}</span>
+    </div>
+    <div class="block">
+      <span class="item">发起人:{{task.publisherName}}</span>
+      <span class="item">接收人:{{task.recipientName}}</span>
+      <span class="item">发起时间:{{task.indate}}</span>
+      <span class="item">计划时间:{{task.planTime}}</span>
+      <span class="item">延迟时间:{{task.delayTime}}</span>
+      <span class="item">收款方:{{task.payee}}</span>
+      <span class="item">付款方:{{task.payer}}</span>
+      <span class="item">费用:{{task.fee}}</span>
+    </div>
+    <div class="block">
+      <span
+        v-if="task.participant != null && task.participant.length > 0"
+        class="item"
+      >参与人:{{task.participant.join("、")}}</span>
+      <span v-else class="item">参与人:无</span>
+    </div>
+    <div class="block">
+      <span class="item">任务内容:{{task.content}}</span>
+    </div>
+    <!-- 下半部分 -->
+    <el-tabs v-model="activeTab" @tab-click="handleTabClick">
+      <!-- 任务动态 -->
+      <el-tab-pane label="任务动态" name="0">
+        <span v-if="dynamic.length == 0">暂无动态</span>
+        <el-timeline>
+          <el-timeline-item v-for="item in dynamic" :key="item.id" :timestamp="item.content">
+            <span style="position: relative; top: 3px;">{{item.stateContent}}</span>
+          </el-timeline-item>
+        </el-timeline>
+      </el-tab-pane>
+      <!-- 审核结果 -->
+      <el-tab-pane label="审核结果" name="1">
+        <span v-if="examination.length == 0">暂无审核</span>
+        <el-timeline>
+          <el-timeline-item v-for="item in examination" :key="item.id">
+            <div style="position: relative; top: 3px;">
+              <div>
+                <el-image
+                  class="head"
+                  :src="'http://localhost:9102/img' + publisherUrl[item.$index]"
+                ></el-image>
+                <div class="publisher-name">{{item.publisher}}</div>
+                <div class="publisher-date">{{item.publishDate}}</div>
+              </div>
+              <div class="publish-content">{{item.publishContent}}</div>
+              <div class="publish-reply" v-if="item.isPass != 0">
+                <div>
+                  <el-image
+                    class="head"
+                    :src="'http://localhost:9102/img' + responderUrl[item.$index]"
+                  ></el-image>
+                  <span class="publish-state" v-if="item.isPass == 1">未通过</span>
+                  <span class="publish-state" v-else>已通过</span>
+                  <div class="publisher-name">{{item.responder}}</div>
+                  <div class="publisher-date">{{item.respondDate}}</div>
+                </div>
+                <div class="publish-content">{{item.respondContent}}</div>
+                <el-rate
+                  v-if="item.isPass == 2"
+                  class="publish-star"
+                  v-model="item.score"
+                  disabled
+                  show-score
+                  text-color="#ff9900"
+                  score-template="{value}"
+                ></el-rate>
+              </div>
+            </div>
+          </el-timeline-item>
+        </el-timeline>
+      </el-tab-pane>
+      <!-- 资料上传下载 -->
+      <el-tab-pane label="任务资料" name="2" v-if="true">
+        <el-button>上传资料N/A</el-button>
+        <el-table :data="file" style="width: 100%" v-loading="loading">
+          <el-table-column type="index" width="40"></el-table-column>
+          <el-table-column prop="name" label="文件名"></el-table-column>
+          <el-table-column prop="indate" label="时间"></el-table-column>
+          <el-table-column label="操作" width="100">
+            <template slot-scope="scope">
+              <el-button type="primary" @click="downloadFile(scope.$index)">下载</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-tab-pane>
+    </el-tabs>
+
+    <!-- 延期的dialog -->
+    <el-dialog title="延期" :visible.sync="form1Visible" width="600px">
+      <el-form ref="form1" label-width="110px">
+        <el-form-item label="延期时间">
+          <el-date-picker
+            v-model="extendDate"
+            type="date"
+            format="yyyy 年 MM 月 dd 日"
+            value-format="yyyy-MM-dd"
+            placeholder="请选择延期时间"
+            style="width: 100%"
+          ></el-date-picker>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="form1Visible = false">取消</el-button>
+        <el-button @click="extendTask" type="primary">提交</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 提交的dialog -->
+    <!-- <el-dialog title="提交" :visible.sync="form2Visible" width="600px">
+      <el-form ref="form2" :model="taskForm" label-width="110px">
+        <el-form-item label="任务名称" prop="name">
+          <el-input v-model="taskForm.name" placeholder="请输入任务名称" clearable></el-input>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="taskFormVisible = false">取消</el-button>
+        <el-button type="primary" @click="submit">提交</el-button>
+      </span>
+    </el-dialog>-->
+
+    <!-- 审核的dialog -->
+    <!-- <el-dialog title="审核" :visible.sync="form3Visible" width="600px">
+      <el-form ref="form3" :model="taskForm" label-width="110px">
+        <el-form-item label="任务名称" prop="name">
+          <el-input v-model="taskForm.name" placeholder="请输入任务名称" clearable></el-input>
+        </el-form-item>
+        <el-form-item label="任务编码" prop="code">
+          <el-input v-model="taskForm.code" placeholder="请输入任务编码" clearable></el-input>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="taskFormVisible = false">取消</el-button>
+        <el-button type="primary" @click="submit">提交</el-button>
+      </span>
+    </el-dialog>-->
+  </div>
+</template>
+
+<script>
+import store from "@/store";
+import request from "@/utils/request";
+export default {
+  data() {
+    return {
+      taskId: this.$route.params.id,
+      currentUserId: store.getters.token,
+      task: [],
+      dynamic: [],
+      examination: [],
+      publisherUrl: [],
+      responderUrl: [],
+      file: [],
+      activeTab: "0",
+      loading: false,
+      form1Visible: false,
+      extendDate: null,
+      form2Visible: false,
+      form3Visible: false
+    };
+  },
+  methods: {
+    //接收任务
+    receiveTask() {
+      this.loading = true;
+      request({
+        url: "/task/receiveTask",
+        method: "post",
+        params: {
+          userId: this.currentUserId,
+          taskId: this.taskId
+        }
+      })
+        .then(response => {
+          this.$message({
+            message: "任务已接收",
+            type: "success"
+          });
+          this.getTask();
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //延期任务
+    extendTask() {
+      this.loading = true;
+      if (this.extendDate == null) {
+        this.$message({
+          message: "请填写延期时间",
+          type: "error"
+        });
+      } else {
+        request({
+          url: "/task/extendTask",
+          method: "post",
+          params: {
+            taskId: this.taskId,
+            date: this.extendDate
+          }
+        })
+          .then(response => {
+            this.$message({
+              message: "任务已延期",
+              type: "success"
+            });
+            this.getTask();
+            this.form1Visible = false;
+            this.loading = false;
+          })
+          .catch(error => {
+            this.loading = false;
+          });
+      }
+    },
+
+    //删除任务
+    deleteTask() {
+      this.loading = true;
+      request({
+        url: "/task/deleteTaskById",
+        method: "post",
+        params: {
+          taskId: this.taskId
+        }
+      })
+        .then(response => {
+          this.$message({
+            message: "任务已删除",
+            type: "success"
+          });
+          this.getTask();
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //获取记录
+    getTask() {
+      this.loading = true;
+      request({
+        url: "/task/getTaskById",
+        method: "post",
+        params: {
+          id: this.taskId
+        }
+      })
+        .then(response => {
+          this.task = response.data.task;
+          this.task.participant = response.data.participant;
+          this.task.publisherName = response.data.publisherName;
+          this.task.recipientName = response.data.recipientName;
+          this.task.tagName = response.data.tagName;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //获取任务动态
+    getDynamic() {
+      this.loading = true;
+      request({
+        url: "/task/getDynamicById",
+        method: "post",
+        params: {
+          id: this.taskId
+        }
+      })
+        .then(response => {
+          this.dynamic = response.data;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //获取任务审核信息
+    getExamination() {
+      this.loading = true;
+      request({
+        url: "/task/getExaminationById",
+        method: "post",
+        params: {
+          id: this.taskId
+        }
+      })
+        .then(response => {
+          this.examination = response.data.examination;
+          this.publisherUrl = response.data.publisherUrl;
+          this.responderUrl = response.data.responderUrl;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //获取资料文件
+    getFile() {
+      this.loading = true;
+      request({
+        url: "/task/getFileById",
+        method: "post",
+        params: {
+          id: this.taskId
+        }
+      })
+        .then(response => {
+          this.file = response.data;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //下载
+    downloadFile(index) {
+      if (this.file[index].url == null) {
+        location.href = "http://localhost:9102" + this.file[index].url;
+      } else {
+        this.$message({
+          message: "下载失败",
+          type: "error"
+        });
+      }
+    },
+
+    //打开延期窗口
+    openDialog1() {
+      this.form1Visible = true;
+      this.extendDate = null;
+    },
+
+    //打开提交窗口
+    openDialog2() {
+      this.form2Visible = true;
+      this.extendDate = null;
+    },
+
+    //打开审核窗口
+    openDialog3() {
+      this.form3Visible = true;
+      this.extendDate = null;
+    },
+
+    //回到前一个页面
+    backToList() {
+      this.$router.go(-1);
+    },
+
+    //标签页切换
+    handleTabClick(tab, event) {}
+  },
+  mounted() {
+    this.getTask();
+    this.getDynamic();
+    this.getExamination();
+    this.getFile();
+  }
+};
+</script>
+
+<style scoped>
+.button {
+  float: right;
+  margin-left: 15px;
+}
+.top {
+  margin-bottom: 15px;
+}
+.block {
+  margin-bottom: 10px;
+}
+.title {
+  font-size: 18px;
+}
+.state {
+  margin-left: 30px;
+  font-size: 16px;
+  color: #409eff;
+}
+.item {
+  font-size: 16px;
+  line-height: 30px;
+  margin-right: 20px;
+}
+.head {
+  width: 60px;
+  height: 60px;
+  border-radius: 60px;
+  float: left;
+}
+.publisher-name {
+  line-height: 30px;
+  font-size: 16px;
+  margin-left: 70px;
+}
+.publisher-date {
+  line-height: 30px;
+  font-size: 16px;
+  color: #aaa;
+  margin-left: 70px;
+}
+.publish-content {
+  font-size: 16px;
+  margin-top: 15px;
+}
+.publish-reply {
+  background-color: #eee;
+  padding: 15px;
+  margin-top: 15px;
+  font-size: 16px;
+}
+.publish-state {
+  font-size: 16px;
+  float: right;
+}
+.publish-star {
+  margin-top: 15px;
+}
+</style>
+

+ 489 - 0
lss_frontend/src/views/task/taskList.vue

@@ -0,0 +1,489 @@
+<template>
+  <div class="app-container">
+    <el-row>
+      <el-col :span="24">
+        <!-- 现在什么筛选条件也没有 -->
+        <el-button style="float: right">导出报表N/A</el-button>
+        <el-button @click="openInsertDialog" style="float: right; margin-right: 15px">添加任务</el-button>
+      </el-col>
+    </el-row>
+
+    <!-- 列表区域 -->
+    <el-table :data="task" style="width: 100%" v-loading="loading">
+      <el-table-column type="index" width="40"></el-table-column>
+      <el-table-column prop="name" label="任务名称"></el-table-column>
+      <el-table-column prop="code" label="任务编码" width="120"></el-table-column>
+      <el-table-column label="任务类型" width="80">
+        <template slot-scope="scope">{{info[scope.$index].tagName}}</template>
+      </el-table-column>
+      <el-table-column label="发起人" width="80">
+        <template slot-scope="scope">{{info[scope.$index].publisherName}}</template>
+      </el-table-column>
+      <el-table-column prop="indate" label="发起时间" width="100"></el-table-column>
+      <el-table-column prop="planTime" label="计划时间" width="100"></el-table-column>
+      <el-table-column label="接收人" width="80">
+        <template slot-scope="scope">{{info[scope.$index].recipientName}}</template>
+      </el-table-column>
+      <el-table-column label="参与人">
+        <template slot-scope="scope">{{info[scope.$index].participant.join("、")}}</template>
+      </el-table-column>
+      <el-table-column prop="workLoad" label="工作量" width="80"></el-table-column>
+      <el-table-column prop="payee" label="收款方" width="80"></el-table-column>
+      <el-table-column prop="payer" label="付款方" width="80"></el-table-column>
+      <el-table-column prop="fee" label="费用" width="80"></el-table-column>
+      <el-table-column label="状态" width="80">
+        <template slot-scope="scope">
+          <span v-if="scope.row.state == 0">待派发</span>
+          <span v-else-if="scope.row.state == 1">已派发</span>
+          <span v-else-if="scope.row.state == 2">已接收</span>
+          <span v-else-if="scope.row.state == 3">待审核</span>
+          <span v-else-if="scope.row.state == 4">未通过</span>
+          <span v-else-if="scope.row.state == 5">已完成</span>
+          <span v-else-if="scope.row.state == 6">已延期</span>
+          <span v-else-if="scope.row.state == 7">已失效</span>
+          <span v-else-if="scope.row.state == 8">已删除</span>
+          <span v-else>不明</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="180" fixed="right">
+        <template slot-scope="scope">
+          <el-button type="primary" @click="toDetail(scope.row.id)">详情</el-button>
+          <el-button v-if="scope.row.state == 0" @click="openUpdateDialog(scope.$index)">编辑</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 页码区域 -->
+    <el-pagination
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      :current-page="pageIndex"
+      :page-sizes="[20, 50, 100]"
+      :page-size="20"
+      layout="total, sizes, prev, pager, next, jumper"
+      :total="total"
+      background
+      style="float: right"
+    ></el-pagination>
+
+    <!-- 添加和修改任务的dialog -->
+    <el-dialog title="任务" :visible.sync="taskFormVisible" width="600px">
+      <el-form ref="form1" :model="taskForm" :rules="rules1" label-width="110px">
+        <el-form-item label="任务名称" prop="name">
+          <el-input v-model="taskForm.name" placeholder="请输入任务名称" clearable></el-input>
+        </el-form-item>
+        <el-form-item label="任务编码" prop="code">
+          <el-input v-model="taskForm.code" placeholder="请输入任务编码" clearable></el-input>
+        </el-form-item>
+        <el-form-item label="任务类型" prop="tagId">
+          <!-- 标签 -->
+          <el-tag
+            class="tag"
+            v-for="tag in tags"
+            :key="tag.id"
+            :type="taskForm.tagId == tag.id? '': 'info'"
+            @click="chooseTag(tag.id)"
+            :closable="tag.id>7"
+            @close="handleClose(tag.id)"
+            style="cursor: pointer;"
+          >{{tag.name}}</el-tag>
+          <!-- 添加和删除标签的地方 -->
+          <el-input
+            class="input-new-tag"
+            v-if="newTagVisible"
+            v-model="newTagValue"
+            ref="saveTagInput"
+            size="small"
+            style="width:80px"
+            @keyup.enter.native="handleInputConfirm"
+            @blur="newTagVisible=false"
+          ></el-input>
+          <el-button v-else class="button-new-tag" size="small" @click="showInput">新建分类</el-button>
+        </el-form-item>
+        <el-form-item label="计划时间" prop="planTime">
+          <el-date-picker
+            v-model="taskForm.planTime"
+            type="date"
+            format="yyyy 年 MM 月 dd 日"
+            value-format="yyyy-MM-dd"
+            placeholder="请选择计划时间"
+            style="width: 100%"
+          ></el-date-picker>
+        </el-form-item>
+        <el-form-item label="工作量" prop="workload">
+          <el-input v-model="taskForm.workload" placeholder="请输入工作量" clearable></el-input>
+        </el-form-item>
+        <el-form-item label="接收人" prop="recipientId">
+          <el-select
+            v-model="taskForm.recipientId"
+            filterable
+            placeholder="请输入接收人"
+            style="width: 100%"
+          >
+            <el-option v-for="item in user" :key="item.id" :label="item.name" :value="item.id"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="参与人" prop="participantIds">
+          <el-select
+            v-model="taskForm.participantIds"
+            multiple
+            filterable
+            clearable
+            placeholder="请输入参与人"
+            style="width: 100%"
+          >
+            <el-option v-for="item in user" :key="item.id" :label="item.name" :value="item.id"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="收款方" prop="payee">
+          <el-input v-model="taskForm.payee" placeholder="请输入收款方" clearable></el-input>
+        </el-form-item>
+        <el-form-item label="付款方" prop="payer">
+          <el-input v-model="taskForm.payer" placeholder="请输入付款方" clearable></el-input>
+        </el-form-item>
+        <el-form-item label="费用" prop="fee">
+          <el-input v-model="taskForm.fee" placeholder="请输入费用" clearable></el-input>
+        </el-form-item>
+        <el-form-item label="任务内容" prop="content">
+          <el-input v-model="taskForm.content" placeholder="请输入任务内容" clearable></el-input>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="taskFormVisible = false">取消</el-button>
+        <el-button type="primary" @click="submit">提交</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import store from '@/store'
+import request from "@/utils/request";
+export default {
+  data() {
+    return {
+      task: [],
+      info: [],
+      taskForm: {
+        id: null,
+        name: null,
+        code: null,
+        tagId: null,
+        planTime: null,
+        workload: null,
+        publishId: null,
+        recipientId: null,
+        participantIds: null,
+        payee: null,
+        payer: null,
+        fee: null,
+        content: null,
+        state: null,
+        delayTime: null
+      },
+      tags: [],
+      user: [],
+      taskFormVisible: false,
+      newTagVisible: false,
+      newTagValue: "",
+      loading: false,
+      //分页相关
+      pageIndex: 1,
+      pageSize: 20,
+      total: 0,
+      rules1: {
+        name: [{ required: true, message: "请输入名称", trigger: "blur" }],
+        code: [{ required: true, message: "请输入编码", trigger: "blur" }],
+        tagId: [{ required: true, message: "请选择类型", trigger: "blur" }],
+        planTime: [
+          { required: true, message: "请选择计划时间", trigger: "blur" }
+        ],
+        workload: [
+          { required: true, message: "请输入工作量", trigger: "blur" }
+        ],
+        recipientId: [
+          { required: true, message: "请选择接收人", trigger: "blur" }
+        ],
+        payee: [{ required: true, message: "请输入收款方", trigger: "blur" }],
+        payer: [{ required: true, message: "请输入付款方", trigger: "blur" }],
+        fee: [{ required: true, message: "请输入费用", trigger: "blur" }],
+        content: [
+          { required: true, message: "请输入任务内容", trigger: "blur" }
+        ]
+      }
+    };
+  },
+  methods: {
+    //获取记录
+    getTask() {
+      this.loading = true;
+      request({
+        url: "/task/getTaskListByPage",
+        method: "post",
+        params: {
+          pageIndex: this.pageIndex,
+          pageSize: this.pageSize
+        }
+      })
+        .then(response => {
+          this.task = response.data.page.records;
+          this.total = response.data.page.total;
+          this.info = response.data.other;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //提交创建和更新
+    submit() {
+      this.$refs.form1.validate(valid => {
+        if (valid) {
+          this.loading = true;
+          var form = new FormData();
+          form.append("name", this.taskForm.name);
+          form.append("code", this.taskForm.code);
+          form.append("planTime", this.taskForm.planTime);
+          form.append("workLoad", this.taskForm.workload);
+          form.append("payer", this.taskForm.payer);
+          form.append("payee", this.taskForm.payee);
+          form.append("fee", this.taskForm.fee);
+          form.append("tagId", this.taskForm.tagId);
+          form.append("recipientId", this.taskForm.recipientId);
+          form.append("content", this.taskForm.content);
+          form.append("publishId", store.getters.token);
+          if (this.taskForm.delayTime != null) {
+            form.append("delayTime", this.taskForm.delayTime);
+          }
+          if (this.taskForm.id != null) {
+            //新增的情况
+            form.append("id", this.taskForm.id);
+          }
+          this.taskForm.participantIds.forEach(item => {
+            form.append("participantIds", item);
+          });
+          request({
+            url: "/task/editTask",
+            method: "post",
+            data: form
+          })
+            .then(response => {
+              this.$message({
+                message: "操作成功",
+                type: "success"
+              });
+              this.getTask();
+              this.loading = false;
+              this.taskFormVisible = false;
+            })
+            .catch(error => {
+              this.loading = false;
+              this.taskFormVisible = false;
+            });
+        }
+      });
+    },
+
+    //打开创建窗口
+    openInsertDialog() {
+      this.taskForm.id = null;
+      this.taskForm.name = "";
+      this.taskForm.code = "";
+      this.taskForm.tagId = null;
+      this.taskForm.planTime = null;
+      this.taskForm.workload = "";
+      this.taskForm.publishId = store.getters.token;
+      this.taskForm.recipientId = null;
+      this.taskForm.participantIds = [];
+      this.taskForm.payee = "";
+      this.taskForm.payer = "";
+      this.taskForm.fee = "";
+      this.taskForm.content = "";
+      this.taskForm.state = 0;
+      this.taskForm.delayTime = null;
+      this.taskFormVisible = true;
+    },
+
+    //打开编辑窗口
+    openUpdateDialog(index) {
+      this.taskForm.id = this.task[index].id;
+      this.taskForm.name = this.task[index].name;
+      this.taskForm.code = this.task[index].code;
+      this.taskForm.tagId = this.task[index].tagId;
+      this.taskForm.planTime = this.task[index].planTime;
+      this.taskForm.workload = this.task[index].workLoad;
+      this.taskForm.publishId = this.task[index].publishId;
+      this.taskForm.recipientId = this.task[index].recipientId;
+      this.taskForm.participantIds = this.info[index].participantId;
+      this.taskForm.payee = this.task[index].payee;
+      this.taskForm.payer = this.task[index].payer;
+      this.taskForm.fee = this.task[index].fee;
+      this.taskForm.content = this.task[index].content;
+      this.taskForm.state = this.task[index].state;
+      this.taskForm.delayTime = this.task[index].delayTime;
+      this.taskFormVisible = true;
+    },
+
+    //获取任务种类标签
+    getTags() {
+      this.loading = true;
+      request({
+        url: "/task-tags/getTaskTags",
+        method: "post",
+        params: {
+          pageIndex: this.pageIndex,
+          pageSize: this.pageSize
+        }
+      })
+        .then(response => {
+          this.tags = response.data;
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+        });
+    },
+
+    //获取用户信息
+    getUser() {
+      request({
+        url: "/user/getUser",
+        method: "post"
+      })
+        .then(response => {
+          this.user = response.data;
+        })
+        .catch(error => {});
+    },
+
+    //表单内选择标签
+    chooseTag(id) {
+      this.taskForm.tagId = id;
+    },
+
+    //页码规格变更
+    handleSizeChange(val) {
+      this.pageSize = val;
+      this.getTask();
+    },
+
+    //页码页数变更
+    handleCurrentChange(val) {
+      this.pageIndex = val;
+      this.getTask();
+    },
+
+    //点击开始新增标签
+    showInput() {
+      this.newTagVisible = true;
+      this.newTagValue = "";
+      this.$nextTick(_ => {
+        this.$refs.saveTagInput.$refs.input.focus();
+      });
+    },
+
+    //确认新增标签
+    handleInputConfirm() {
+      if (this.newTagValue != "") {
+        this.$confirm(
+          "确定要添加新分类" + this.newTagValue + "吗",
+          "添加标签",
+          {
+            confirmButtonText: "确定",
+            cancelButtonText: "取消",
+            type: "info"
+          }
+        )
+          .then(() => {
+            request({
+              url: "/task-tags/insertTaskTag",
+              method: "post",
+              params: { name: this.newTagValue }
+            })
+              .then(response => {
+                this.$message({
+                  type: "success",
+                  message: "添加成功!"
+                });
+                this.getTags();
+                this.newTagVisible = false;
+                this.newTagValue = "";
+              })
+              .catch(error => {
+                this.newTagVisible = false;
+                this.newTagValue = "";
+              });
+          })
+          .catch(() => {
+            this.newTagVisible = false;
+            this.newTagValue = "";
+          });
+      }
+    },
+
+    //点击删除标签
+    handleClose(id) {
+      this.$confirm("确定要删除分类吗", "删除分类", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      })
+        .then(() => {
+          request({
+            url: "/task-tags/deleteTaskTag",
+            method: "post",
+            params: { id: id }
+          })
+            .then(response => {
+              this.$message({
+                type: "success",
+                message: "删除成功!"
+              });
+              //这里要清除选中的和表单中的tag
+              this.taskForm.tagId = null;
+              this.getTags();
+            })
+            .catch(error => {});
+        })
+        .catch(() => {});
+    },
+
+    //到详情页面
+    toDetail(id) {
+      if (id != null) {
+        this.$router.push("/task/" + id);
+      } else {
+        this.$message({
+          message: "发生错误",
+          type: "error"
+        });
+      }
+    }
+  },
+  mounted() {
+    this.getTask();
+    this.getTags();
+    this.getUser();
+  }
+};
+</script>
+
+<style scoped>
+.el-tag + .el-tag {
+  margin-left: 10px;
+}
+.button-new-tag {
+  margin-left: 10px;
+  height: 32px;
+  line-height: 30px;
+  padding-top: 0;
+  padding-bottom: 0;
+}
+.input-new-tag {
+  width: 90px;
+  margin-left: 10px;
+  vertical-align: bottom;
+}
+</style>
+

+ 5 - 0
lss_frontend/tests/unit/.eslintrc.js

@@ -0,0 +1,5 @@
+module.exports = {
+  env: {
+    jest: true
+  }
+}

+ 98 - 0
lss_frontend/tests/unit/components/Breadcrumb.spec.js

@@ -0,0 +1,98 @@
+import { mount, createLocalVue } from '@vue/test-utils'
+import VueRouter from 'vue-router'
+import ElementUI from 'element-ui'
+import Breadcrumb from '@/components/Breadcrumb/index.vue'
+
+const localVue = createLocalVue()
+localVue.use(VueRouter)
+localVue.use(ElementUI)
+
+const routes = [
+  {
+    path: '/',
+    name: 'home',
+    children: [{
+      path: 'dashboard',
+      name: 'dashboard'
+    }]
+  },
+  {
+    path: '/menu',
+    name: 'menu',
+    children: [{
+      path: 'menu1',
+      name: 'menu1',
+      meta: { title: 'menu1' },
+      children: [{
+        path: 'menu1-1',
+        name: 'menu1-1',
+        meta: { title: 'menu1-1' }
+      },
+      {
+        path: 'menu1-2',
+        name: 'menu1-2',
+        redirect: 'noredirect',
+        meta: { title: 'menu1-2' },
+        children: [{
+          path: 'menu1-2-1',
+          name: 'menu1-2-1',
+          meta: { title: 'menu1-2-1' }
+        },
+        {
+          path: 'menu1-2-2',
+          name: 'menu1-2-2'
+        }]
+      }]
+    }]
+  }]
+
+const router = new VueRouter({
+  routes
+})
+
+describe('Breadcrumb.vue', () => {
+  const wrapper = mount(Breadcrumb, {
+    localVue,
+    router
+  })
+  it('dashboard', () => {
+    router.push('/dashboard')
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
+    expect(len).toBe(1)
+  })
+  it('normal route', () => {
+    router.push('/menu/menu1')
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
+    expect(len).toBe(2)
+  })
+  it('nested route', () => {
+    router.push('/menu/menu1/menu1-2/menu1-2-1')
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
+    expect(len).toBe(4)
+  })
+  it('no meta.title', () => {
+    router.push('/menu/menu1/menu1-2/menu1-2-2')
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
+    expect(len).toBe(3)
+  })
+  // it('click link', () => {
+  //   router.push('/menu/menu1/menu1-2/menu1-2-2')
+  //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
+  //   const second = breadcrumbArray.at(1)
+  //   console.log(breadcrumbArray)
+  //   const href = second.find('a').attributes().href
+  //   expect(href).toBe('#/menu/menu1')
+  // })
+  // it('noRedirect', () => {
+  //   router.push('/menu/menu1/menu1-2/menu1-2-1')
+  //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
+  //   const redirectBreadcrumb = breadcrumbArray.at(2)
+  //   expect(redirectBreadcrumb.contains('a')).toBe(false)
+  // })
+  it('last breadcrumb', () => {
+    router.push('/menu/menu1/menu1-2/menu1-2-1')
+    const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
+    const redirectBreadcrumb = breadcrumbArray.at(3)
+    expect(redirectBreadcrumb.contains('a')).toBe(false)
+  })
+})

+ 0 - 0
lss_frontend/tests/unit/components/Hamburger.spec.js


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů