commit 3780ebefadffab898098912b96008a2d81c98623 Author: ival <610568032@qq.com> Date: Tue Jul 2 16:29:52 2019 +0800 增加nclient工程 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..f669cf1f7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.env.development b/.env.development new file mode 100644 index 000000000..2c5c25f12 --- /dev/null +++ b/.env.development @@ -0,0 +1,16 @@ +# just a flag +NODE_ENV = 'development' + +# base api +# VUE_APP_BASE_API = 'https://joylink.club/jlcloud' +# VUE_APP_BASE_API = 'http://192.168.3.4:9000' +VUE_APP_BASE_API = 'http://192.168.3.6:9000' + +# 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 diff --git a/.env.production b/.env.production new file mode 100644 index 000000000..e05ea2043 --- /dev/null +++ b/.env.production @@ -0,0 +1,5 @@ +# just a flag +NODE_ENV = 'production' + +# base api +VUE_APP_BASE_API = 'https://joylink.club/jlcloud' diff --git a/.env.staging b/.env.staging new file mode 100644 index 000000000..c12fe731a --- /dev/null +++ b/.env.staging @@ -0,0 +1,5 @@ +# just a flag +NODE_ENV = 'test' + +# base api +VUE_APP_BASE_API = 'https://joylink.club/jlcloud' diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..e6529fc09 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +build/*.js +src/assets +public +dist diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..bbe07d664 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,196 @@ +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': ["error", "tab"], + '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'] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..9ad28d23d --- /dev/null +++ b/.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 diff --git a/.postcssrc.js b/.postcssrc.js new file mode 100644 index 000000000..10473efcf --- /dev/null +++ b/.postcssrc.js @@ -0,0 +1,8 @@ +// https://github.com/michael-ciniawsky/postcss-load-config + +module.exports = { + 'plugins': { + // to edit target browsers: use "browserslist" field in package.json + 'autoprefixer': {} + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..16574d97a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: stable +script: npm run test +notifications: + email: false diff --git a/Jenkinsfile-prd b/Jenkinsfile-prd new file mode 100644 index 000000000..872713823 --- /dev/null +++ b/Jenkinsfile-prd @@ -0,0 +1,26 @@ +pipeline { + agent { + node { + label 'master' + } + } + + stages { + stage('Package') { + tools { + nodejs 'nodejs-10' + } + steps { + sh 'npm install' + sh 'npm run build' + } + } + stage('Publish') { + steps { + sh 'cp -rf ./dist/* /usr/local/joylink/jlclient' + } + } + } + + +} diff --git a/Jenkinsfile-test b/Jenkinsfile-test new file mode 100644 index 000000000..ad09944fc --- /dev/null +++ b/Jenkinsfile-test @@ -0,0 +1,26 @@ +pipeline { + agent { + node { + label 'master' + } + } + + stages { + stage('Package') { + tools { + nodejs 'nodejs-10' + } + steps { + sh 'npm install' + sh 'npm run test' + } + } + stage('Publish') { + steps { + sh 'cp -rf ./dist/* /usr/local/joylink/jlclient' + } + } + } + + +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..61515750d --- /dev/null +++ b/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. diff --git a/README-zh.md b/README-zh.md new file mode 100644 index 000000000..32243a46d --- /dev/null +++ b/README-zh.md @@ -0,0 +1,96 @@ +# vue-admin-template + +> 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。 + +[线上地址](http://panjiachen.github.io/vue-admin-template) + +[国内访问](https://panjiachen.gitee.io/vue-admin-template) + +目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。 + +## Extra + +如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) + +## 相关项目 + +[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) + +[electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) + +[vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) + +写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目: + +- [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2) +- [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac) +- [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35) +- [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56) +- [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836) + +## Build Setup + +```bash +# 克隆项目 +git clone https://github.com/PanJiaChen/vue-admin-template.git + +# 进入项目目录 +cd vue-admin-template + +# 安装依赖 +npm install + +# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 +npm install --registry=https://registry.npm.taobao.org + +# 启动服务 +npm run dev +``` + +浏览器访问 [http://localhost:9528](http://localhost:9528) + +## 发布 + +```bash +# 构建测试环境 +npm run build:stage + +# 构建生产环境 +npm run build:prod +``` + +## 其它 + +```bash +# 预览发布环境效果 +npm run preview + +# 预览发布环境效果 + 静态资源分析 +npm run preview -- --report + +# 代码格式检查 +npm run lint + +# 代码格式检查并自动修复 +npm run lint -- --fix +``` + +更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/) + +## Demo + +![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif) + +## Browsers support + +Modern browsers and Internet Explorer 10+. + +| [](http://godban.github.io/browsers-support-badges/)IE / Edge | [](http://godban.github.io/browsers-support-badges/)Firefox | [](http://godban.github.io/browsers-support-badges/)Chrome | [](http://godban.github.io/browsers-support-badges/)Safari | +| --------- | --------- | --------- | --------- | +| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions + +## License + +[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license. + +Copyright (c) 2017-present PanJiaChen diff --git a/README.md b/README.md new file mode 100644 index 000000000..bd3dbc87e --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# vue-admin-template + +English | [简体中文](./README-zh.md) + +> A minimal vue admin template with Element UI & axios & iconfont & permission control & lint + +**Live demo:** http://panjiachen.github.io/vue-admin-template + + +**The current version is `v4.0+` build on `vue-cli`. If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0), it does not rely on `vue-cli`** + +## Build Setup + + +```bash +# clone the project +git clone https://github.com/PanJiaChen/vue-element-admin.git + +# enter the project directory +cd vue-element-admin + +# install dependency +npm install + +# develop +npm run dev +``` + +This will automatically open http://localhost:9527 + +## Build + +```bash +# build for test environment +npm run build:stage + +# build for production environment +npm run build:prod +``` + +## Advanced + +```bash +# preview the release environment effect +npm run preview + +# preview the release environment effect + static resource analysis +npm run preview -- --report + +# code format check +npm run lint + +# code format check and auto fix +npm run lint -- --fix +``` + +Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information + +## Demo + +![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif) + +## Extra + +If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) + +For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour)) + +## Related Project + +[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) + +[electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) + +[vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) + +## Browsers support + +Modern browsers and Internet Explorer 10+. + +| [](http://godban.github.io/browsers-support-badges/)IE / Edge | [](http://godban.github.io/browsers-support-badges/)Firefox | [](http://godban.github.io/browsers-support-badges/)Chrome | [](http://godban.github.io/browsers-support-badges/)Safari | +| --------- | --------- | --------- | --------- | +| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions + +## License + +[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license. + +Copyright (c) 2017-present PanJiaChen diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 000000000..5e0f39c22 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/app' + ] +} diff --git a/build/index.js b/build/index.js new file mode 100644 index 000000000..0c57de2aa --- /dev/null +++ b/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}`) +} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..9f8bb7917 --- /dev/null +++ b/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: { + '^@/(.*)$': '/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: '/tests/unit/coverage', + // 'collectCoverage': true, + 'coverageReporters': [ + 'lcov', + 'text-summary' + ], + testURL: 'http://localhost/' +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..c6aa5e19b --- /dev/null +++ b/package.json @@ -0,0 +1,73 @@ +{ + "name": "vue-admin-template", + "version": "4.1.0", + "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint", + "author": "Pan ", + "license": "MIT", + "scripts": { + "dev": "vue-cli-service serve", + "build": "vue-cli-service build", + "test": "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.0", + "echarts": "^4.2.1", + "element-ui": "2.7.2", + "js-cookie": "2.2.0", + "js-md5": "^0.7.3", + "normalize.css": "7.0.0", + "nprogress": "0.2.0", + "path-to-regexp": "2.4.0", + "qrcode.vue": "^1.6.2", + "stompjs": "^2.3.3", + "storejs": "^1.0.25", + "vue": "2.6.10", + "vue-router": "3.0.6", + "vuedraggable": "^2.20.0", + "vuex": "3.1.0", + "xlsx": "^0.14.2" + }, + "devDependencies": { + "@babel/core": "7.0.0", + "@babel/register": "7.0.0", + "@vue/cli-plugin-babel": "3.6.0", + "@vue/cli-plugin-eslint": "3.6.0", + "@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", + "file-loader": "^3.0.1", + "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", + "not ie <= 8" + ] +} diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 000000000..e250916ab Binary files /dev/null and b/public/favicon.png differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 000000000..7a0b35453 --- /dev/null +++ b/public/index.html @@ -0,0 +1,17 @@ + + + + + + + + <%= webpackConfig.name %> + + + + We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue. + + + + + diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 000000000..98b06c41a --- /dev/null +++ b/src/App.vue @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/api/login.js b/src/api/login.js new file mode 100644 index 000000000..ebc5aed91 --- /dev/null +++ b/src/api/login.js @@ -0,0 +1,78 @@ +import request from '@/utils/request' + +// 修改密码 +export function changePassword(userId, data) { + return request({ + url: `/api/login/${userId}/password`, + method: 'put', + data: data + }) +} + +// 账号密码 其他系统 +export function login(params) { + return request({ + url: '/api/login', + method: 'post', + data: params + }); +} + +// 获取登陆url 二维码 其他系统 +export function getLoginUrl(params) { + return request({ + url: '/api/login/url', + method: 'get', + params: params + }); +} + + +// 获取登录信息 +export function getInfo(token) { + return request({ + url: '/api/login/getUserInfo', + method: 'get', + params: { token } + }); +} + +// 登出 +export function logout(token) { + return request({ + url: '/api/login/logout', + method: 'get', + params: { + token + } + }); +} + +// 检查登陆状态 +export function checkLoginStatus(sessionId) { + return new Promise((resolve, reject) => { + request({ + url: '/api/login/checkStatus', + method: 'get', + params: { + sessionId: sessionId + } + }).then(response => { + if (response.data.status === '2') { + resolve(response); + } else { + reject(response); + } + }).catch(error => { + reject(error); + }); + }); +} + +// 检测持续在线 防止掉线在大屏或者仿真系统下 +export function checkLoginLine() { + return request({ + url: '/api/cache/heartBeat', + method: 'get', + }); +} diff --git a/src/api/user.js b/src/api/user.js new file mode 100644 index 000000000..dd4041fee --- /dev/null +++ b/src/api/user.js @@ -0,0 +1,99 @@ +import request from '@/utils/request'; + + +/** 查询用户参数*/ +export function getUserConfigInfo() { + return request({ + url: '/api/user/config', + method: 'get' + }); +} + +// /** 注册用户*/ +// export function createUserInfo(data) { +// return request({ +// url: '/api/userinfo/create', +// method: 'put', +// data: data +// }); +// } +// /** 根据用户Id获取用户信息*/ +// export function getUserInfoByOpenId(params) { +// return request({ +// url: '/api/userinfo/getByOpenId', +// method: 'get', +// params: params +// }); +// } + +// /** 根据姓名或者手机号查询用户*/ +// export function getUserInfoByNameOrMobile(params) { +// return request({ +// url: '/api/userinfo/nameOrMobile', +// method: 'get', +// params: params +// }); +// } + +// /** 设置用户参数*/ +// export function setUserConfigInfo(data) { +// return request({ +// url: '/api/user/config', +// method: 'post', +// data: data +// }); +// } + +// /** 获取销售列表*/ +// export function getSellerList() { +// return request({ +// url: `/api/user/seller`, +// method: 'get' +// }); +// } + +// /** 查询用户列表*/ +// export function getUserList(params) { +// return request({ +// url: `/api/user`, +// method: 'get', +// params: params +// }); +// } + +// /** 模糊查询用户 昵称、名称、手机号*/ +// export function getDimUserList(params) { +// return request({ +// url: `/api/user/fuzzy`, +// method: 'get', +// params: params +// }); +// } + + +// /** 获取用户订阅地图列表*/ +// export function getUserSubscribe(userId) { +// return request({ +// url: `/api/user/subscribe/${userId}`, +// method: 'get' +// }); +// } + +// /** 保存用户订阅地图列表*/ +// export function saveUserSubscribe(data) { +// return request({ +// url: '/api/user/subscribe', +// method: 'post', +// data: data +// }); +// } + + +// // 修改用户权限 +// export function putRoles(data) { +// return request({ +// url: `/api/user/${data.id}/role`, +// method: 'put', +// data: data +// }); +// } diff --git a/src/assets/401_images/401.gif b/src/assets/401_images/401.gif new file mode 100644 index 000000000..cd6e0d943 Binary files /dev/null and b/src/assets/401_images/401.gif differ diff --git a/src/assets/404_images/404.png b/src/assets/404_images/404.png new file mode 100644 index 000000000..3d8e2305c Binary files /dev/null and b/src/assets/404_images/404.png differ diff --git a/src/assets/404_images/404_cloud.png b/src/assets/404_images/404_cloud.png new file mode 100644 index 000000000..c6281d090 Binary files /dev/null and b/src/assets/404_images/404_cloud.png differ diff --git a/src/components/Breadcrumb/index.vue b/src/components/Breadcrumb/index.vue new file mode 100644 index 000000000..2b557bf71 --- /dev/null +++ b/src/components/Breadcrumb/index.vue @@ -0,0 +1,78 @@ + + + + + {{ item.meta.title }} + {{ item.meta.title }} + + + + + + + + diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue new file mode 100644 index 000000000..60719ec9f --- /dev/null +++ b/src/components/Hamburger/index.vue @@ -0,0 +1,44 @@ + + + + + + + + + + + diff --git a/src/components/PopMenu/index.vue b/src/components/PopMenu/index.vue new file mode 100644 index 000000000..f97ec9405 --- /dev/null +++ b/src/components/PopMenu/index.vue @@ -0,0 +1,262 @@ + + + + + + + + + + + + {{ child.label }} + + + + + + + + + {{ item.label }} + + + + + + + + + + + + + + {{ item.label }} + + + {{ item.label }} + + + + + + + + + + + diff --git a/src/components/QrCode/index.vue b/src/components/QrCode/index.vue new file mode 100644 index 000000000..d1ade34eb --- /dev/null +++ b/src/components/QrCode/index.vue @@ -0,0 +1,44 @@ + + + + + + + 关闭 + + + + + \ No newline at end of file diff --git a/src/components/QueryListPage/DataForm.vue b/src/components/QueryListPage/DataForm.vue new file mode 100644 index 000000000..88c9c9c87 --- /dev/null +++ b/src/components/QueryListPage/DataForm.vue @@ -0,0 +1,287 @@ + + + + + + + {{ item.message }} + + + + {{ item.buttontip }} + + + + + + + + + + + + + + + + + + + + {{ item.message }} + + + + + {{ nor.name }} + + + + + 是 + 否 + + + + + + + {{ + option.label }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/QueryListPage/QueryForm.vue b/src/components/QueryListPage/QueryForm.vue new file mode 100644 index 000000000..34c112aeb --- /dev/null +++ b/src/components/QueryListPage/QueryForm.vue @@ -0,0 +1,483 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ + item.label }} + + + + + + + + + + + + + + + + + + 查询 + 重置 + 导出 + + {{ button.text }} + + + + + + + + + diff --git a/src/components/QueryListPage/QueryListPage.vue b/src/components/QueryListPage/QueryListPage.vue new file mode 100644 index 000000000..b6e752c7c --- /dev/null +++ b/src/components/QueryListPage/QueryListPage.vue @@ -0,0 +1,455 @@ + + + + + + + + + + {{ `` }} + + + + + + + {{ column.columnValue(scope.row, + scope.$index) }} + + + + + {{ column.columnValue(scope.row, scope.$index) }} + + + + + + {{ tag }} + + + + + + + + {{ getTableBtnName(button.name, + scope.$index, scope.row) }} + + + + + + + 选中添加 + + + + + + + + + diff --git a/src/components/SvgIcon/index.vue b/src/components/SvgIcon/index.vue new file mode 100644 index 000000000..784a638be --- /dev/null +++ b/src/components/SvgIcon/index.vue @@ -0,0 +1,43 @@ + + + + + + + + + diff --git a/src/components/TurnbackBar/index.vue b/src/components/TurnbackBar/index.vue new file mode 100644 index 000000000..c549e2143 --- /dev/null +++ b/src/components/TurnbackBar/index.vue @@ -0,0 +1,41 @@ + + + 返回 + {{ title }} + + + + + + diff --git a/src/icons/index.js b/src/icons/index.js new file mode 100644 index 000000000..2c6b309c9 --- /dev/null +++ b/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) diff --git a/src/icons/svg/dashboard.svg b/src/icons/svg/dashboard.svg new file mode 100644 index 000000000..5317d3702 --- /dev/null +++ b/src/icons/svg/dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/example.svg b/src/icons/svg/example.svg new file mode 100644 index 000000000..46f42b532 --- /dev/null +++ b/src/icons/svg/example.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/eye-open.svg b/src/icons/svg/eye-open.svg new file mode 100644 index 000000000..88dcc98e6 --- /dev/null +++ b/src/icons/svg/eye-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/eye.svg b/src/icons/svg/eye.svg new file mode 100644 index 000000000..16ed2d872 --- /dev/null +++ b/src/icons/svg/eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/form.svg b/src/icons/svg/form.svg new file mode 100644 index 000000000..dcbaa185a --- /dev/null +++ b/src/icons/svg/form.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/link.svg b/src/icons/svg/link.svg new file mode 100644 index 000000000..48197ba4d --- /dev/null +++ b/src/icons/svg/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/map-mange.svg b/src/icons/svg/map-mange.svg new file mode 100644 index 000000000..7b48a013c --- /dev/null +++ b/src/icons/svg/map-mange.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/nested.svg b/src/icons/svg/nested.svg new file mode 100644 index 000000000..06713a86c --- /dev/null +++ b/src/icons/svg/nested.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/password.svg b/src/icons/svg/password.svg new file mode 100644 index 000000000..e291d85df --- /dev/null +++ b/src/icons/svg/password.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/plan-mange.svg b/src/icons/svg/plan-mange.svg new file mode 100644 index 000000000..7c303e010 --- /dev/null +++ b/src/icons/svg/plan-mange.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/table.svg b/src/icons/svg/table.svg new file mode 100644 index 000000000..0e3dc9dea --- /dev/null +++ b/src/icons/svg/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/tree.svg b/src/icons/svg/tree.svg new file mode 100644 index 000000000..dd4b7dd22 --- /dev/null +++ b/src/icons/svg/tree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/user.svg b/src/icons/svg/user.svg new file mode 100644 index 000000000..0ba0716a6 --- /dev/null +++ b/src/icons/svg/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svgo.yml b/src/icons/svgo.yml new file mode 100644 index 000000000..d11906aec --- /dev/null +++ b/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' diff --git a/src/layout/components/AppMain.vue b/src/layout/components/AppMain.vue new file mode 100644 index 000000000..4aef00e1d --- /dev/null +++ b/src/layout/components/AppMain.vue @@ -0,0 +1,17 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/layout/components/Entry.vue b/src/layout/components/Entry.vue new file mode 100644 index 000000000..47af4fe63 --- /dev/null +++ b/src/layout/components/Entry.vue @@ -0,0 +1,40 @@ + + + + {{item.name}} + + + + + + + \ No newline at end of file diff --git a/src/layout/components/Logout.vue b/src/layout/components/Logout.vue new file mode 100644 index 000000000..b67076ff6 --- /dev/null +++ b/src/layout/components/Logout.vue @@ -0,0 +1,88 @@ + + + + + + {{username}} + + + + + + 退出 + + + + + + + + \ No newline at end of file diff --git a/src/layout/components/Navbar.vue b/src/layout/components/Navbar.vue new file mode 100644 index 000000000..d622071b0 --- /dev/null +++ b/src/layout/components/Navbar.vue @@ -0,0 +1,162 @@ + + + + + + {{item.children[0].meta.title}} + + + + + {{item.children[0].meta.title}} + + + + + {{item.meta.title}} + + + + + + {{child.meta.title}} + + + + + {{grandchild.meta.title}} + + + + + + + + {{child.meta.title}} + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/layout/components/Sidebar/SidebarItem.vue b/src/layout/components/Sidebar/SidebarItem.vue new file mode 100644 index 000000000..aa719d19b --- /dev/null +++ b/src/layout/components/Sidebar/SidebarItem.vue @@ -0,0 +1,59 @@ + + + + + + + + {{item.children[0].meta.title}} + + + + + + + {{item.meta.title}} + + + + + + + + + {{child.meta.title}} + + + + + + + + + + diff --git a/src/layout/components/Sidebar/index.vue b/src/layout/components/Sidebar/index.vue new file mode 100644 index 000000000..294aff67d --- /dev/null +++ b/src/layout/components/Sidebar/index.vue @@ -0,0 +1,29 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/layout/components/index.js b/src/layout/components/index.js new file mode 100644 index 000000000..6744940ba --- /dev/null +++ b/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'; diff --git a/src/layout/index.vue b/src/layout/index.vue new file mode 100644 index 000000000..4f7846db4 --- /dev/null +++ b/src/layout/index.vue @@ -0,0 +1,87 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/layout/mixin/ResizeHandler.js b/src/layout/mixin/ResizeHandler.js new file mode 100644 index 000000000..43dcfbd3e --- /dev/null +++ b/src/layout/mixin/ResizeHandler.js @@ -0,0 +1,41 @@ +import store from '@/store'; + +const { body } = document; +const WIDTH = 1024; +const RATIO = 3; + +export default { + watch: { + $route() { + if (this.device === 'mobile' && this.sidebar.opened) { + store.dispatch('CloseSideBar', { withoutAnimation: false }); + } + } + }, + beforeMount() { + window.addEventListener('resize', this.resizeHandler); + }, + mounted() { + const isMobile = this.isMobile(); + if (isMobile) { + store.dispatch('ToggleDevice', 'mobile'); + store.dispatch('CloseSideBar', { withoutAnimation: true }); + } + }, + methods: { + isMobile() { + const rect = body.getBoundingClientRect(); + return rect.width - RATIO < WIDTH; + }, + resizeHandler() { + if (!document.hidden) { + const isMobile = this.isMobile(); + store.dispatch('ToggleDevice', isMobile ? 'mobile' : 'desktop'); + + if (isMobile) { + store.dispatch('CloseSideBar', { withoutAnimation: true }); + } + } + } + } +}; diff --git a/src/layout1/components/AppMain.vue b/src/layout1/components/AppMain.vue new file mode 100644 index 000000000..5a48b7ba0 --- /dev/null +++ b/src/layout1/components/AppMain.vue @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/src/layout1/components/ChangePassword.vue b/src/layout1/components/ChangePassword.vue new file mode 100644 index 000000000..53de235aa --- /dev/null +++ b/src/layout1/components/ChangePassword.vue @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 取 消 + 确 定 + + + + + + \ No newline at end of file diff --git a/src/layout1/components/Navbar.vue b/src/layout1/components/Navbar.vue new file mode 100644 index 000000000..abc1985f7 --- /dev/null +++ b/src/layout1/components/Navbar.vue @@ -0,0 +1,145 @@ + + + + + + + + + + {{ username }} + + + + + + 首页 + + + + 修改密码 + + + 退出 + + + + + + + + + + + diff --git a/src/layout1/components/Sidebar/FixiOSBug.js b/src/layout1/components/Sidebar/FixiOSBug.js new file mode 100644 index 000000000..d526487d5 --- /dev/null +++ b/src/layout1/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) + } + } + } + } +} diff --git a/src/layout1/components/Sidebar/Item.vue b/src/layout1/components/Sidebar/Item.vue new file mode 100644 index 000000000..166ac5d7e --- /dev/null +++ b/src/layout1/components/Sidebar/Item.vue @@ -0,0 +1,29 @@ + diff --git a/src/layout1/components/Sidebar/Link.vue b/src/layout1/components/Sidebar/Link.vue new file mode 100644 index 000000000..c8531d0af --- /dev/null +++ b/src/layout1/components/Sidebar/Link.vue @@ -0,0 +1,36 @@ + + + + + + + + + diff --git a/src/layout1/components/Sidebar/Logo.vue b/src/layout1/components/Sidebar/Logo.vue new file mode 100644 index 000000000..4eb338a62 --- /dev/null +++ b/src/layout1/components/Sidebar/Logo.vue @@ -0,0 +1,82 @@ + + + + + + {{ title }} + + + + {{ title }} + + + + + + + + diff --git a/src/layout1/components/Sidebar/SidebarItem.vue b/src/layout1/components/Sidebar/SidebarItem.vue new file mode 100644 index 000000000..c9832cc87 --- /dev/null +++ b/src/layout1/components/Sidebar/SidebarItem.vue @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/layout1/components/Sidebar/index.vue b/src/layout1/components/Sidebar/index.vue new file mode 100644 index 000000000..f2eb71681 --- /dev/null +++ b/src/layout1/components/Sidebar/index.vue @@ -0,0 +1,56 @@ + + + + + + + + + + + + diff --git a/src/layout1/components/index.js b/src/layout1/components/index.js new file mode 100644 index 000000000..97ee3cd12 --- /dev/null +++ b/src/layout1/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' diff --git a/src/layout1/index.vue b/src/layout1/index.vue new file mode 100644 index 000000000..0288eaa73 --- /dev/null +++ b/src/layout1/index.vue @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + diff --git a/src/layout1/mixin/ResizeHandler.js b/src/layout1/mixin/ResizeHandler.js new file mode 100644 index 000000000..4c48aed3f --- /dev/null +++ b/src/layout1/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 }) + } + } + } + } +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 000000000..9ccc9d504 --- /dev/null +++ b/src/main.js @@ -0,0 +1,55 @@ +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 +import '@/scripts/GlobalPlugin' + +Vue.use(ElementUI) + +Vue.config.productionTip = false + +new Vue({ + el: '#app', + router, + store, + render: h => h(App) +}) + + +router.beforeEach((to, from, next) => { + let name = to.fullPath; + if (name.includes('/dp/') || name.includes('display/dp')) { + document.title = '琏课堂-大屏系统'; + } else if (name.includes('/plan/') || name.includes('/planEdit/')) { + document.title = '琏计划'; + } else { + document.title = '琏课堂'; + } + next(); +}); + +Vue.prototype.$messageBox = function (msge) { + if (this.$confirm) { + this.$confirm(`${msge || '处理失败'}!`, '提示', { + confirmButtonText: '确定', + type: 'warning', + showCancelButton: false, + center: true + }).then(() => { + }).catch(() => { + }); + } +}; diff --git a/src/mixin/WindowResizeHandler.js b/src/mixin/WindowResizeHandler.js new file mode 100644 index 000000000..4ecc944f3 --- /dev/null +++ b/src/mixin/WindowResizeHandler.js @@ -0,0 +1,35 @@ +import * as throttleUtil from '@/utils/throttle' + +export default { + data() { + return { + _clientWidth: '', + _clientHeight: '' + } + }, + beforeMount() { + // 调用节流函数限制执行频率 + var fn = throttleUtil.createOrUpdate( + this, + '_resizeHandler', + 300, + 'debounce' + ) + window.addEventListener('resize', fn) + }, + mounted() { + this._resizeHandler() + }, + methods: { + _resizeHandler() { + this._clientWidth = document.documentElement.clientWidth + this._clientHeight = document.documentElement.clientHeight + if (this.resizeHandler) { + this.resizeHandler() + } + } + }, + beforeDestroy() { + window.removeEventListener('resize', this._resizeHandler) + } +} diff --git a/src/permission.js b/src/permission.js new file mode 100644 index 000000000..3d21d212b --- /dev/null +++ b/src/permission.js @@ -0,0 +1,110 @@ +import Vue from 'vue'; +import store from '@/store'; +import router from './router'; +import NProgress from 'nprogress'; // Progress 进度条 +import 'nprogress/nprogress.css';// Progress 进度条样式 +import { admin } from './router'; +import { getToken, getScreenToken, getPlanToken } from '@/utils/auth'; // 验权 +import { LoginParams } from '@/utils/login'; + +function hasPermission(roles, permissionRoles) { + if (roles.indexOf(admin) >= 0) return true; + if (!permissionRoles) return true; + return roles.some(role => permissionRoles.indexOf(role) >= 0); +} + +const isDev = process.env.NODE_ENV === 'development'; + +const whiteList = ['/login', '/login1', '/dp/login', '/dp/login1', '/plan/login', '/plan/login1']; // 不重定向白名单 + +const loginPage = isDev ? whiteList[1] : whiteList[0]; + +const loginScreenPage = isDev ? whiteList[3] : whiteList[2]; + +const loginPlanPage = isDev ? whiteList[5] : whiteList[4]; + +// 获取路径数据 +function getRouteInfo(to) { + let loginPath = '/'; + let getTokenInfo = () => { }; + let clientId = ''; + let toRoutePath = to.redirectedFrom || to.path; + + if (/^\/dp/.test(toRoutePath) || /^\/display\/dp/.test(toRoutePath)) { + loginPath = loginScreenPage + getTokenInfo = getScreenToken; + clientId = LoginParams.DaPing.clientId; + } else if (/^\/plan/.test(toRoutePath) || /^\/display\/plan/.test(toRoutePath) || /^\/planEdit/.test(toRoutePath)) { + loginPath = loginPlanPage + getTokenInfo = getPlanToken; + clientId = LoginParams.LianJiHua.clientId; + } else { + loginPath = loginPage; + getTokenInfo = getToken; + clientId = null; + } + + return { clientId, loginPath, getTokenInfo } +} + +function handleRoute(to, from, next, routeInfo) { + if (store.getters.roles.length === 0) { + // 拉取用户信息 + store.dispatch('GetInfo', routeInfo.getTokenInfo).then(res => { + // 根据roles权限生成可访问的路由表 + const roles = res.roles; + + store.dispatch('GenerateRoutes', { roles, clientId: routeInfo.clientId }).then(() => { + // 动态添加可访问路由表 + router.addRoutes(store.getters.addRouters); + if (to.redirectedFrom) { + next({ path: to.redirectedFrom, replace: true }); + } else { + next({ ...to, replace: true }); + } + }); + + }).catch(() => { + store.dispatch('FedLogOut', routeInfo.clientId).then(() => { + Vue.$messageBox('验证失败,请重新登陆!'); + next({ path: routeInfo.loginPath }); + }); + }); + } else { + //除没有动态改变权限的需求可直接next() 删下方权限判断 + if (hasPermission(store.getters.roles, to.meta.roles)) { + next(); + } else { + next({ path: '/401', replace: true, query: { noGoBack: true } }); + } + } +} + +router.beforeEach((to, from, next) => { + NProgress.start(); + let routeInfo = getRouteInfo(to); + if (routeInfo.getTokenInfo()) { + // 已登录 + if (to.path === routeInfo.loginPath) { + // 登录页面不拦截 + next(); + } else { + // 进入系统重新计算路由 + handleRoute(to, from, next, routeInfo); + } + } else { + // 未登录情况下 + if (whiteList.indexOf(to.path) !== -1) { + // 在免登录白名单,直接进入 + next(); + } else { + // 否则全部重定向到登录页 + next(routeInfo.loginPath); + } + } +}); + +router.afterEach(() => { + // 结束Progress + NProgress.done(); +}); diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 000000000..795f6a994 --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,118 @@ +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 (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 + } + */ + +export const user = '01'; // 普通用户 +export const mapCreater = '02'; // 地图创建权限 +export const lessonCreater = '03'; // 课程创建权限 +export const admin = '04'; // 管理员 +export const superAdmin = '05'; // 超级管理员 + +export const userExam = '011'; // 考试系统 +export const userLesson = '012'; // 教学系统 +export const userSimulation = '013'; // 仿真系统 +export const userScreen = '014'; // 大屏系统 +export const userPlan = '015'; // 计划系统 + + +export const UrlConfig = { + display: '/display', + examRuleDraft: '/examRule/draft', + examRuleManage: '/examRule/manage' +}; + +/** + * constantRoutes + * a base page that does not have permission requirements + * all roles can be accessed + */ +export const constantRoutes = [ + { + path: '/login', + component: () => import('@/views/login/loginNew'), + hidden: true + }, + + { + path: '/login1', + component: () => import('@/views/login/index'), + hidden: true + }, + + { + path: '/401', + component: () => import('@/views/error-page/401'), + hidden: true + }, + + { + path: '/404', + component: () => import('@/views/error-page/404'), + hidden: true + }, + + { + path: '/', + component: Layout, + redirect: '/dashboard', + children: [{ + path: 'dashboard', + name: 'Dashboard', + component: () => import('@/views/dashboard/index'), + meta: { title: '首页', icon: 'dashboard' } + }] + } +] + +export const asyncRouter = [ + // { + // path: '/map', + // component: Layout, + // meta: { + // roles: [admin] + // }, + // children: [ + // { + // path: 'manage', + // name: '地图管理', + // component: () => import('@/views/mapmanage/index'), + // meta: { title: '地图管理', icon: 'map-mange' } + // } + // ] + // }, + + { path: '*', redirect: '/404', hidden: true } +] + +const createRouter = () => new Router({ + mode: 'history', // require service support + scrollBehavior: () => ({ y: 0 }), + routes: constantRoutes +}) + +const router = createRouter() + +export default router diff --git a/src/scripts/ConstConfig.js b/src/scripts/ConstConfig.js new file mode 100644 index 000000000..e283692e9 --- /dev/null +++ b/src/scripts/ConstConfig.js @@ -0,0 +1,13 @@ +export default { + ConstSelect: { + Status: [ + { label: '无效', value: '0' }, + { label: '有效', value: '1' } + ], + + Whether: [ + { label: '否', value: false }, + { label: '是', value: true } + ] + } +} diff --git a/src/scripts/GlobalPlugin.js b/src/scripts/GlobalPlugin.js new file mode 100644 index 000000000..a6a1e0dde --- /dev/null +++ b/src/scripts/GlobalPlugin.js @@ -0,0 +1,35 @@ +import Vue from 'vue' +import QueryListPage from '@/components/QueryListPage/QueryListPage' +import DataForm from '@/components/QueryListPage/DataForm' +import TurnbackBar from '@/components/TurnbackBar' +import ConstConfig from '@/scripts/ConstConfig' + +// 全局组件 +Vue.component('DataForm', DataForm) +Vue.component('QueryListPage', QueryListPage) +Vue.component('TurnbackBar', TurnbackBar) + +Vue.prototype.$ConstSelect = (function() { + ConstConfig.ConstSelect.translate = function(value, codeName) { + if (codeName) { + const obj = this[codeName].filter(function(item) { + return item.value === value + })[0] + return obj && obj.label + } + } + return ConstConfig.ConstSelect +}()) + +Vue.prototype.__windowResizeFlag = false +Vue.prototype.$addWindowResizeListener = function(cb) { + window.addEventListener('resize', function() { + if (!Vue.__windowResizeFlag) { + Vue.__windowResizeFlag = true + setTimeout(function() { + Vue.__windowResizeFlag = false + cb() + }, 100) + } + }) +} diff --git a/src/settings.js b/src/settings.js new file mode 100644 index 000000000..19d2115a3 --- /dev/null +++ b/src/settings.js @@ -0,0 +1,16 @@ +module.exports = { + + title: '琏课堂', + + /** + * @type {boolean} true | false + * @description Whether fix the header + */ + fixedHeader: false, + + /** + * @type {boolean} true | false + * @description Whether show the logo in sidebar + */ + sidebarLogo: false +} diff --git a/src/store/getters.js b/src/store/getters.js new file mode 100644 index 000000000..a1ce78c00 --- /dev/null +++ b/src/store/getters.js @@ -0,0 +1,16 @@ +const getters = { + routers: state => state.permission.routes, + addRouters: state => state.permission.addRouters, + sidebar: state => state.app.sidebar, + lessonbar: state => state.app.lessonBar, + device: state => state.app.device, + token: state => state.user.token, + avatar: state => state.user.avatar, + name: state => state.user.name, + nickname: state => state.user.nickname, + roles: state => state.user.roles, + canvasWidth: state => state.config.width, + canvasHeight: state => state.config.height, + permission_routes: state => state.permission.routes +} +export default getters diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 000000000..da7578325 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,21 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import app from './modules/app' +import settings from './modules/settings' +import user from './modules/user' +import permission from './modules/permission' +import getters from './getters' + +Vue.use(Vuex) + +const store = new Vuex.Store({ + modules: { + app, + settings, + user, + permission + }, + getters +}) + +export default store diff --git a/src/store/modules/app.js b/src/store/modules/app.js new file mode 100644 index 000000000..e830c3574 --- /dev/null +++ b/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 +} diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js new file mode 100644 index 000000000..59896dfce --- /dev/null +++ b/src/store/modules/permission.js @@ -0,0 +1,106 @@ +import { asyncRouter, constantRoutes, user, userLesson, userExam, userSimulation, userScreen, userPlan, superAdmin } from '@/router'; +import { PermissionType } from '@/utils/PermissionType'; +import { UrlConfig } from '@/router/index'; + +function setHonePagePath(route, roles) { + if (roles && roles.length === 2 && roles.indexOf(user) >= 0 && (route.path === '/' || route.path === 'dashboard')) { + if (roles.indexOf(userLesson) >= 0) { + route.redirect = `${UrlConfig.teach.home}`; + } else if (roles.indexOf(userExam) >= 0) { + route.redirect = `${UrlConfig.exam.home}`; + } else if (roles.indexOf(userSimulation) >= 0) { + route.redirect = `${UrlConfig.demonstration.home}`; + } else if (roles.indexOf(userScreen) >= 0) { + route.redirect = `${UrlConfig.dp.home}`; + } else if (roles.indexOf(userPlan) >= 0) { + route.redirect = `${UrlConfig.plan.home}`; + } + } +} + +/** + * 通过meta.role判断是否与当前用户权限匹配 + * @param roles + * @param route + */ +function hasPermission(roles, route, parentsRoles) { + setHonePagePath(route, roles); + if (route.meta && route.meta.roles) { + //如果存在本级路由,则使用自己的roles过滤 + return roles.some(role => route.meta.roles.indexOf(role) >= 0); + } else if (parentsRoles) { + //如果没有本级路由,有父级路由,则使用父级路由过滤 + return roles.some(role => parentsRoles.indexOf(role) >= 0); + } else { + //如果父级和本级都没有则默认不需要过滤 + return true; + } +} + +/** + * 根据roles和系统类型重新设置权限列表 + * @param roles + * @param type + */ +function convertRouterRoles({ roles, permissionType }) { + if (roles && roles.indexOf(user) >= 0) { + switch (permissionType) { + case PermissionType.LESSON: { roles.push(userLesson); } break; + case PermissionType.EXAM: { roles.push(userExam); } break; + case PermissionType.SIMULATION: { roles.push(userSimulation); } break; + case PermissionType.SCREEN: { roles.push(userScreen); } break; + case PermissionType.PLAN: { roles.push(userPlan); } break; + } + } + + return { roles }; +} + +/** + * 递归过滤异步路由表,返回符合用户角色权限的路由表 + * @param asyncRouter + * @param roles + */ +function filterAsyncRouter(asyncRouter, roles, parentsRoles) { + return asyncRouter.filter(route => { + if (hasPermission(roles, route, parentsRoles)) { + if (route.children && route.children.length) { + route.children = filterAsyncRouter(route.children, roles, route.meta ? route.meta.roles : undefined); + } + return true; + } + return false; + }); +} + +const permission = { + state: { + routes: constantRoutes, + addRouters: [], + }, + mutations: { + SET_ROUTERS: (state, routes) => { + state.addRouters = routes; + state.routes = constantRoutes.concat(routes); + } + }, + actions: { + // 生成其他权限路由 + GenerateRoutes({ commit }, data) { + return new Promise(resolve => { + const { roles } = convertRouterRoles(data); + let accessedRouters; + if (roles.indexOf(superAdmin) >= 0) { + accessedRouters = asyncRouter; + } else { + accessedRouters = filterAsyncRouter(asyncRouter, roles); + } + + commit('SET_ROUTERS', accessedRouters); + resolve(); + }); + } + } +}; + +export default permission; diff --git a/src/store/modules/settings.js b/src/store/modules/settings.js new file mode 100644 index 000000000..70481c6cd --- /dev/null +++ b/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 +} + diff --git a/src/store/modules/user.js b/src/store/modules/user.js new file mode 100644 index 000000000..fc0b16567 --- /dev/null +++ b/src/store/modules/user.js @@ -0,0 +1,183 @@ +import localStore from 'storejs'; +import { login, logout, getInfo } from '@/api/login'; +import { getToken, setToken, removeToken, removeScreenToken, setScreenToken, getScreenToken, setPlanToken, getPlanToken, removePlanToken, handleToken, handleRemoveToken } from '@/utils/auth'; +import { getUserConfigInfo } from '@/api/user'; +import { creatSubscribe, perpetualTopic } from '@/utils/stomp'; +import { LoginParams } from '@/utils/login'; + +const user = { + state: { + token: getToken(), + tokenScreen: getScreenToken(), + tokenPlan: getPlanToken(), + name: '', + nickname: '', + nationcode: '', + roles: [], + id: '', + admin: false, + wxId: '', + wxUnionId: '', + }, + + mutations: { + SET_TOKEN: (state, token) => { + state.token = token; + }, + SET_TOKENSCREEN: (state, token) => { + state.tokenScreen = token; + }, + SET_TOKENPLAN: (state, token) => { + state.tokenPlan = token; + }, + SET_NAME: (state, name) => { + state.name = name; + }, + SET_NICKNAME: (state, nickname) => { + state.nickname = nickname; + }, + SET_ROLES: (state, roles) => { + state.roles = roles; + }, + SET_ID: (state, id) => { + state.id = id; + }, + SET_NATIONCODE: (state, nationcode) => { + state.nationcode = nationcode; + }, + SET_WXID: (state, wxId) => { + state.wxId = wxId; + }, + SET_WXUNIONID: (state, wxUnionId) => { + state.wxUnionId = wxUnionId; + }, + }, + + actions: { + // 登录 + Login({ commit }, userInfo) { + const username = userInfo.username.trim(); + const password = userInfo.password.trim(); + return new Promise((resolve, reject) => { + let params = null; + let execFc = null; + if (userInfo.type == 'dp') { + params = Object.assign({ account: username, password }, LoginParams.DaPing); + execFc = (token) => { + setScreenToken(token); + commit('SET_TOKENSCREEN', token); + // let header = { group: '', 'X-Token': getScreenToken() }; + // creatSubscribe(perpetualTopic, header); + } + } else if (userInfo.type == 'plan') { + params = Object.assign({ account: username, password }, LoginParams.LianJiHua); + execFc = (token) => { + setPlanToken(token); + commit('SET_TOKENPLAN', token); + // let header = { group: '', 'X-Token': getPlanToken() }; + // creatSubscribe(perpetualTopic, header); + } + } else { + params = Object.assign({ account: username, password }, LoginParams.LianKeTang); + execFc = (token) => { + setToken(token); + commit('SET_TOKEN', token); + // let header = { group: '', 'X-Token': getToken() }; + // creatSubscribe(perpetualTopic, header); + } + } + + // 登录系统 + login(params).then(resp => { + execFc(resp.data); + resolve(); + }).catch(error => { reject(error); }); + }); + }, + + //扫码登陆设置 + QrLoginSetting({ dispatch, commit }, token) { + return new Promise((resolve, reject) => { + //设置user域token值 + commit(token.key, token.value); + // let header = { group: '', 'X-Token': token.value }; + // creatSubscribe(perpetualTopic, header); + //获取用户参数 + dispatch('GetUserConfigInfo').then(response => { + resolve(response); + }).catch(error => { + reject(error); + }); + }); + }, + + // 获取用户信息 + GetInfo({ commit }, getTokenInfo) { + return new Promise((resolve, reject) => { + getInfo(getTokenInfo()).then(response => { + const user = response.data; + if (user.roles && user.roles.length > 0) { // 验证返回的roles是否是一个非空数组 + commit('SET_ROLES', user.roles); + } else { + reject('getInfo: roles must be a non-null array !'); + } + commit('SET_NAME', user.name); + commit('SET_NICKNAME', user.nickname); + commit('SET_ID', user.id); + commit('SET_NATIONCODE', user.nationcode); + commit('SET_WXID', user.wxId); + commit('SET_WXUNIONID', user.wxUnionId); + resolve(user); + }).catch(error => { + reject(error); + }); + }); + }, + + // 获取用户参数 + GetUserConfigInfo() { + getUserConfigInfo().then(resp => { + if (resp.data) { + resp.data.forEach(elem => { + localStore(elem.code, elem.val); + }); + } + }); + }, + + // 前端登出 + FedLogOut({ commit }, clientId) { + return new Promise(resolve => { + if (clientId == LoginParams.DaPing.clientId) { + commit('SET_TOKENSCREEN', ''); + removeScreenToken(); + } else if (clientId == LoginParams.LianJiHua.clientId) { + commit('SET_TOKENPLAN', ''); + removePlanToken(); + } else { + commit('SET_TOKEN', ''); + removeToken(); + } + + resolve(); + }); + }, + + // 登出系统 + LogOut({ commit }) { + return new Promise((resolve, reject) => { + logout(handleToken()).then(() => { + commit('SET_TOKEN', ''); + commit('SET_ROLES', []); + commit('SET_ID', ''); + handleRemoveToken(); + resolve(); + }).catch(error => { + reject(error); + }); + }); + } + } +}; + +export default user; diff --git a/src/styles/element-ui.scss b/src/styles/element-ui.scss new file mode 100644 index 000000000..6af3bfdeb --- /dev/null +++ b/src/styles/element-ui.scss @@ -0,0 +1,44 @@ +// 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 + } +} diff --git a/src/styles/index.scss b/src/styles/index.scss new file mode 100644 index 000000000..3b4da517c --- /dev/null +++ b/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; +} diff --git a/src/styles/mixin.scss b/src/styles/mixin.scss new file mode 100644 index 000000000..36b74bbd9 --- /dev/null +++ b/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%; +} diff --git a/src/styles/sidebar.scss b/src/styles/sidebar.scss new file mode 100644 index 000000000..e55f86564 --- /dev/null +++ b/src/styles/sidebar.scss @@ -0,0 +1,213 @@ +#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; + } + + .svg-icon { + margin-right: 0px; + } + + .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; + } + } +} diff --git a/src/styles/transition.scss b/src/styles/transition.scss new file mode 100644 index 000000000..4cb27cc81 --- /dev/null +++ b/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; +} diff --git a/src/styles/variables.scss b/src/styles/variables.scss new file mode 100644 index 000000000..f211cbdb2 --- /dev/null +++ b/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: 0px; //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; +} diff --git a/src/utils/Export2Excel.js b/src/utils/Export2Excel.js new file mode 100644 index 000000000..bdb6f7989 --- /dev/null +++ b/src/utils/Export2Excel.js @@ -0,0 +1,270 @@ +/* eslint-disable */ +import XLSX from 'xlsx'; + +function generateArray(table) { + var out = []; + var rows = table.querySelectorAll('tr'); + var ranges = []; + for (var R = 0; R < rows.length; ++R) { + var outRow = []; + var row = rows[R]; + var columns = row.querySelectorAll('td'); + for (var C = 0; C < columns.length; ++C) { + var cell = columns[C]; + var colspan = cell.getAttribute('colspan'); + var rowspan = cell.getAttribute('rowspan'); + var cellValue = cell.innerText; + if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue; + + //Skip ranges + ranges.forEach(function (range) { + if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) { + for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null); + } + }); + + //Handle Row Span + if (rowspan || colspan) { + rowspan = rowspan || 1; + colspan = colspan || 1; + ranges.push({ s: { r: R, c: outRow.length }, e: { r: R + rowspan - 1, c: outRow.length + colspan - 1 } }); + } + ; + + //Handle Value + outRow.push(cellValue !== "" ? cellValue : null); + + //Handle Colspan + if (colspan) for (var k = 0; k < colspan - 1; ++k) outRow.push(null); + } + out.push(outRow); + } + return [out, ranges]; +}; + +function datenum(v, date1904) { + if (date1904) v += 1462; + var epoch = Date.parse(v); + return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000); +} + +function sheet_from_array_of_arrays(data, opts) { + var ws = {}; + var range = { s: { c: 10000000, r: 10000000 }, e: { c: 0, r: 0 } }; + for (var R = 0; R != data.length; ++R) { + for (var C = 0; C != data[R].length; ++C) { + if (range.s.r > R) range.s.r = R; + if (range.s.c > C) range.s.c = C; + if (range.e.r < R) range.e.r = R; + if (range.e.c < C) range.e.c = C; + var cell = { v: data[R][C], t: 's' }; + if (cell.v == null) continue; + var cell_ref = XLSX.utils.encode_cell({ c: C, r: R }); + + if (typeof cell.v === 'number') cell.t = 'n'; + else if (typeof cell.v === 'boolean') cell.t = 'b'; + else if (typeof cell.v === 'object') { + cell.t = 'o'; + cell.v = JSON.stringify(cell.v); + cell.h = 'o'; + } + else if (cell.v instanceof Date) { + cell.t = 'n'; + cell.z = XLSX.SSF._table[14]; + cell.v = datenum(cell.v); + } + // else cell.t = 's'; + + ws[cell_ref] = cell; + } + } + if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range); + return ws; +} + +function encode_row(row) { return "" + (row + 1); }; +function encode_col(col) { var s = ""; for (++col; col; col = Math.floor((col - 1) / 26)) s = String.fromCharCode(((col - 1) % 26) + 65) + s; return s; } +function safe_format_cell(cell, v) { + var q = (cell.t == 'd' && v instanceof Date); + if (cell.z != null) try { return (cell.w = SSF.format(cell.z, q ? datenum(v) : v)); } catch (e) { } + try { return (cell.w = SSF.format((cell.XF || {}).numFmtId || (q ? 14 : 0), q ? datenum(v) : v)); } catch (e) { return '' + v; } +} + +function format_cell(cell, v, o) { + if (cell == null || cell.t == null || cell.t == 'z') return ''; + if (cell.t == 'b' || cell.t == 'n') return cell.v; + if (cell.t == 'o') return JSON.parse(cell.v); + if (cell.w !== undefined) return cell.w; + // if (cell.v !== undefined) return cell.v; + if (v == undefined) return safe_format_cell(cell, cell.v); + return safe_format_cell(cell, v); +} + +export function sheet_to_json(sheet, opts) { + if (sheet == null || sheet["!ref"] == null) return []; + var val = { t: 'n', v: 0 }, header = 0, offset = 1, hdr = [], isempty = true, v = 0, vv = ""; + var r = { s: { r: 0, c: 0 }, e: { r: 0, c: 0 } }; + var o = opts || {}; + var raw = o.raw; + var defval = o.defval; + var range = o.range != null ? o.range : sheet["!ref"]; + if (o.header === 1) header = 1; + else if (o.header === "A") header = 2; + else if (Array.isArray(o.header)) header = 3; + switch (typeof range) { + case 'string': r = safe_decode_range(range); break; + case 'number': r = safe_decode_range(sheet["!ref"]); r.s.r = range; break; + default: r = range; + } + if (header > 0) offset = 0; + var rr = encode_row(r.s.r); + var cols = []; + var out = []; + var outi = 0, counter = 0; + var dense = Array.isArray(sheet); + var R = r.s.r, C = 0, CC = 0; + if (dense && !sheet[R]) sheet[R] = []; + for (C = r.s.c; C <= r.e.c; ++C) { + cols[C] = encode_col(C); + val = dense ? sheet[R][C] : sheet[cols[C] + rr]; + switch (header) { + case 1: hdr[C] = C - r.s.c; break; + case 2: hdr[C] = cols[C]; break; + case 3: hdr[C] = o.header[C - r.s.c]; break; + default: + if (val == null) val = { w: "__EMPTY", t: "s" }; + vv = v = format_cell(val, null, o); + counter = 0; + for (CC = 0; CC < hdr.length; ++CC) if (hdr[CC] == vv) vv = v + "_" + (++counter); + hdr[C] = vv; + } + } + var row = (header === 1) ? [] : {}; + for (R = r.s.r + offset; R <= r.e.r; ++R) { + rr = encode_row(R); + isempty = true; + if (header === 1) row = []; + else { + row = {}; + if (Object.defineProperty) try { Object.defineProperty(row, '__rowNum__', { value: R, enumerable: false }); } catch (e) { row.__rowNum__ = R; } + else row.__rowNum__ = R; + } + if (!dense || sheet[R]) for (C = r.s.c; C <= r.e.c; ++C) { + val = dense ? sheet[R][C] : sheet[cols[C] + rr]; + if (val === undefined || val.t === undefined) { + if (defval === undefined) continue; + if (hdr[C] != null) { row[hdr[C]] = defval; } + continue; + } + v = val.v; + switch (val.t) { + case 'z': if (v == null) break; continue; + case 'e': v = void 0; break; + case 's': case 'd': case 'b': case 'n': break; + default: throw new Error('unrecognized type ' + val.t); + } + if (hdr[C] != null) { + if (v == null) { + if (defval !== undefined) row[hdr[C]] = defval; + else if (raw && v === null) row[hdr[C]] = null; + else continue; + } else { + row[hdr[C]] = raw ? v : format_cell(val, v, o); + } + if (v != null) isempty = false; + } + } + if ((isempty === false) || (header === 1 ? o.blankrows !== false : !!o.blankrows)) out[outi++] = row; + } + out.length = outi; + return out; +} + +function safe_decode_range(range) { + var o = { s: { c: 0, r: 0 }, e: { c: 0, r: 0 } }; + var idx = 0, i = 0, cc = 0; + var len = range.length; + for (idx = 0; i < len; ++i) { + if ((cc = range.charCodeAt(i) - 64) < 1 || cc > 26) break; + idx = 26 * idx + cc; + } + o.s.c = --idx; + + for (idx = 0; i < len; ++i) { + if ((cc = range.charCodeAt(i) - 48) < 0 || cc > 9) break; + idx = 10 * idx + cc; + } + o.s.r = --idx; + + if (i === len || range.charCodeAt(++i) === 58) { o.e.c = o.s.c; o.e.r = o.s.r; return o; } + + for (idx = 0; i != len; ++i) { + if ((cc = range.charCodeAt(i) - 64) < 1 || cc > 26) break; + idx = 26 * idx + cc; + } + o.e.c = --idx; + + for (idx = 0; i != len; ++i) { + if ((cc = range.charCodeAt(i) - 48) < 0 || cc > 9) break; + idx = 10 * idx + cc; + } + o.e.r = --idx; + return o; +} + +function Workbook() { + if (!(this instanceof Workbook)) return new Workbook(); + this.SheetNames = []; + this.Sheets = {}; +} + +function s2ab(s) { + var buf = new ArrayBuffer(s.length); + var view = new Uint8Array(buf); + for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF; + return buf; +} + +export function export_table_to_excel(id) { + var theTable = document.getElementById(id); + var oo = generateArray(theTable); + var ranges = oo[1]; + + /* original data */ + var data = oo[0]; + var ws_name = "SheetJS"; + + var wb = new Workbook(), ws = sheet_from_array_of_arrays(data); + + /* add ranges to worksheet */ + // ws['!cols'] = ['apple', 'banan']; + ws['!merges'] = ranges; + + /* add worksheet to workbook */ + wb.SheetNames.push(ws_name); + wb.Sheets[ws_name] = ws; + + var wbout = XLSX.write(wb, { bookType: 'xlsx', bookSST: false, type: 'binary' }); + + saveAs(new Blob([s2ab(wbout)], { type: "application/octet-stream" }), "test.xlsx") +} + +export function export_json_to_excel(th, jsonData, defaultTitle) { + + /* original data */ + + var data = jsonData; + data.unshift(th); + var ws_name = "SheetJS"; + + var wb = new Workbook(), ws = sheet_from_array_of_arrays(data); + + + /* add worksheet to workbook */ + wb.SheetNames.push(ws_name); + wb.Sheets[ws_name] = ws; + + var wbout = XLSX.write(wb, { bookType: 'xlsx', bookSST: false, type: 'binary' }); + var title = defaultTitle || '列表' + saveAs(new Blob([s2ab(wbout)], { type: "application/octet-stream" }), title + ".xlsx") +} diff --git a/src/utils/PermissionType.js b/src/utils/PermissionType.js new file mode 100644 index 000000000..a6773dbf6 --- /dev/null +++ b/src/utils/PermissionType.js @@ -0,0 +1,11 @@ +/** + * 权限类型 + */ +export const PermissionType = { + LESSON: '01', + EXAM: '02', + SIMULATION: '03', + SCREEN: '04', + PLAN: '05', + REPLAY: '06' +}; \ No newline at end of file diff --git a/src/utils/auth.js b/src/utils/auth.js new file mode 100644 index 000000000..762ce7220 --- /dev/null +++ b/src/utils/auth.js @@ -0,0 +1,86 @@ +import Cookies from 'js-cookie'; +import { LoginParams } from '@/utils/login'; + +const TokenKey = 'Admin-Token'; + +const TokenScreenKey = 'Screen-Token'; + +const TokenPlanKey = 'Plan-Token'; + + +// 设置教学,实训,仿真系统token +export function getToken() { + return Cookies.get(TokenKey); +} + +export function setToken(token) { + return Cookies.set(TokenKey, token); +} + +export function removeToken() { + return Cookies.remove(TokenKey); +} + +// 设置大屏token +export function getScreenToken() { + return Cookies.get(TokenScreenKey); +} + +export function setScreenToken(token) { + return Cookies.set(TokenScreenKey, token); +} + +export function removeScreenToken() { + return Cookies.remove(TokenScreenKey); +} + +// 设置琏计划token +export function getPlanToken() { + return Cookies.get(TokenPlanKey); +} + +export function setPlanToken(token) { + return Cookies.set(TokenPlanKey, token); +} + +export function removePlanToken() { + return Cookies.remove(TokenPlanKey); +} + +// 根据路径判断获取token +export function handleToken() { + let path = window.location.href; + if (path.includes('/dp/') || path.includes('/display/dp')) { + return getScreenToken(); + } else if (path.includes('/plan') || path.includes('/display/plan')) { + return getPlanToken(); + } else { + return getToken(); + } +} + +// 根据路径清除token +export function handleRemoveToken() { + let path = window.location.href; + if (path.includes('/dp/') || path.includes('/display/dp')) { + return removeScreenToken(); + } else if (path.includes('/plan') || path.includes('/display/plan')) { + return removePlanToken(); + } else { + return removeToken(); + } +} + + +// 根据route路径判断系统类型 +export function gainClientId() { + let path = window.location.href; + let clientId = null; + if (path.includes('/dp/') || path.includes('/display/dp')) { + clientId = LoginParams.DaPing.clientId; + } else if (path.includes('/plan') || path.includes('/display/plan')) { + clientId = LoginParams.LianJiHua.clientId; + } + + return clientId; +} \ No newline at end of file diff --git a/src/utils/baseUrl.js b/src/utils/baseUrl.js new file mode 100644 index 000000000..d4448b37e --- /dev/null +++ b/src/utils/baseUrl.js @@ -0,0 +1,14 @@ + +export function getBaseUrl() { + let BASE_API + if (process.env.NODE_ENV === 'development') { + // BASE_API = 'https://joylink.club/jlcloud' + BASE_API = 'https://test.joylink.club/jlcloud' + // BASE_API = 'http://192.168.3.5:9010' // 袁琪 + // BASE_API = 'http://192.168.3.6:9010' // 旭强 + // BASE_API = 'http://192.168.3.4:9010' // 琰培 + } else { + BASE_API = process.env.VUE_APP_BASE_API + } + return BASE_API +} diff --git a/src/utils/get-page-title.js b/src/utils/get-page-title.js new file mode 100644 index 000000000..5e12af804 --- /dev/null +++ b/src/utils/get-page-title.js @@ -0,0 +1,10 @@ +import defaultSettings from '@/settings' + +const title = defaultSettings.title || 'Vue Admin Template' + +export default function getPageTitle(pageTitle) { + if (pageTitle) { + return `${pageTitle} - ${title}` + } + return `${title}` +} diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 000000000..69df30890 --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,111 @@ +/* eslint-disable no-mixed-spaces-and-tabs */ +/** + * Created by PanJiaChen on 16/11/18. + */ + +/** + * Parse the time to string + * @param {(Object|string|number)} time + * @param {string} cFormat + * @returns {string} + */ +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(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { + let value = formatObj[key] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } + if (result.length > 0 && value < 10) { + value = '0' + value + } + return value || 0 + }) + return time_str +} + +/** + * @param {number} time + * @param {string} option + * @returns {string} + */ +export function timeFormat(time, option) { + if (('' + time).length === 10) { + time = parseInt(time) * 1000 + } else { + time = +time + } + const d = new Date(time) + const now = Date.now() + + const diff = (now - d) / 1000 + + if (diff < 30) { + return '刚刚' + } else if (diff < 3600) { + // less 1 hour + return Math.ceil(diff / 60) + '分钟前' + } else if (diff < 3600 * 24) { + return Math.ceil(diff / 3600) + '小时前' + } else if (diff < 3600 * 24 * 2) { + return '1天前' + } + if (option) { + return parseTime(time, option) + } else { + return ( + d.getMonth() + + 1 + + '月' + + d.getDate() + + '日' + + d.getHours() + + '时' + + d.getMinutes() + + '分' + ) + } +} + +/** + * @param {string} url + * @returns {Object} + */ +export function param2Obj(url) { + const search = url.split('?')[1] + if (!search) { + return {} + } + return JSON.parse( + '{"' + + decodeURIComponent(search) + .replace(/"/g, '\\"') + .replace(/&/g, '","') + .replace(/=/g, '":"') + .replace(/\+/g, ' ') + + '"}' + ) +} diff --git a/src/utils/login.js b/src/utils/login.js new file mode 100644 index 000000000..73d1eb7bc --- /dev/null +++ b/src/utils/login.js @@ -0,0 +1,8 @@ + +// 登陆配置参数 +export const LoginParams = { + LianKeTang: { clientId: "1", secret: "joylink" }, //"琏课堂" + DaPing: { clientId: "2", secret: "bigscreen" }, //"大屏系统" + LianJiHua: { clientId: "3", secret: "linkplan" }, //"琏计划" + Assistant: { clientId: "4", secret: "linkassistant" }, //"琏课堂助手" +} diff --git a/src/utils/request.js b/src/utils/request.js new file mode 100644 index 000000000..42474d884 --- /dev/null +++ b/src/utils/request.js @@ -0,0 +1,64 @@ +import axios from 'axios'; +import store from '../store'; +import { MessageBox } from 'element-ui'; +import { handleToken, gainClientId } from '@/utils/auth'; +import { getBaseUrl } from '@/utils/baseUrl' + +const BASE_API = getBaseUrl() + +// 创建axios实例 +const service = axios.create({ + baseURL: BASE_API, // api的base_url + withCredentials: true, // 跨域请求时是否需要使用凭证 + timeout: 60000 // 请求超时时间 +}); + +// request拦截器 +service.interceptors.request.use(config => { + if (handleToken()) { + config.headers['X-Token'] = handleToken(); // 让每个请求携带自定义token 请根据实际情况自行修改 + } + if (config.time) { + config.timeout = config.time; // 让每个请求携带自定义token 请根据实际情况自行修改 + } + return config; +}, error => { + // Do something with request error + Promise.reject(error); +}); + +// respone拦截器 +service.interceptors.response.use( + response => { + /** code为非200是抛错 可结合自己业务进行修改*/ + const res = response.data; + if (res.code !== 200) { + // 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了; + if (res.code === 40004 || res.code === 40005 || res.code === 40003) { + // eslint-disable-next-line no-undef + eventBus.$emit('stop'); + // eslint-disable-next-line no-undef + eventBus.$emit('viewLoading', false); + // eslint-disable-next-line no-undef + eventBus.$emit('clearCheckLogin'); + MessageBox.confirm('你已被登出,请重新登录', '确定登出', { + confirmButtonText: '重新登录', + showCancelButton: false, + type: 'warning' + }).then(() => { + store.dispatch('FedLogOut', gainClientId()).then(() => { + location.reload();// 为了重新实例化vue-router对象 避免bug + }); + }); + } + return Promise.reject(res); + } else { + return response.data; + } + }, + error => { + return Promise.reject(error); + } +); + +export default service; diff --git a/src/utils/runPlan.js b/src/utils/runPlan.js new file mode 100644 index 000000000..13e7253de --- /dev/null +++ b/src/utils/runPlan.js @@ -0,0 +1,282 @@ +/** 创建一个车次数据点*/ +export function createMartPoint(opt) { + const rotate = opt.directionCode === '2' ? 45 : (opt.directionCode === '1' ? -45 : 0) + const position = opt.type ? 'insideBottomLeft' : 'insideTopLeft' + return { + coord: opt.coord, + name: opt.name, + label: { + normal: { + rotate: rotate, + formatter: '{b}', + backgroundColor: 'rgb(242,242,242,0.1)', + color: 'black', + position: position + } + } + } +} + +/** 创建一个车次数据序列*/ +export function createSeriesModel(opt, lineStyle) { + if (opt) { + return { + z: opt.z || 5, + zlevel: opt.zlevel || 0, + type: 'line', + name: opt.name, + data: opt.data, + sampling: 'average', + lineStyle: lineStyle || {}, + markPoint: { + symbol: 'roundRect', + symbolSize: 1, + data: opt.markPointData + } + } + } +} + +/** 创建标记横线*/ +export function createMarkLineModels(stations, computedYaxis) { + const markLineModel = {} + if (stations && stations.length) { + markLineModel.type = 'line' + markLineModel.name = 'markline' + markLineModel.markLine = {} + markLineModel.markLine.silent = true + markLineModel.markLine.data = [] + markLineModel.markLine.lineStyle = { color: '#B0C4DE', width: 0.5 } + markLineModel.markLine.symbol = 'none' + stations.forEach((elem, index) => { + markLineModel.markLine.data.push( + { + label: { + show: true, + position: 'start', + formatter: elem.name, + color: 'black' + }, + lineStyle: { + type: 'solid', + width: 0.5, + opacity: 0.5 + }, + yAxis: computedYaxis(elem, index) + } + ) + }) + } + return markLineModel +} + +/** 创建不会重复颜色的内部对象*/ +export const HexColor = { + colorIndex: 0, + difValue: 0.25, // 一般为0.25 + oddColor: null, + eveColor: null, + oldColor: null, + newColor: null, + colorList: [ + '#000000', '#0000FF', '#8A2BE2', '#A52A2A', '#DEB887', '#5F9EA0', '#7FFF00', '#FF7F50', '#6495ED', '#DC143C', + '#00FFFF', '#008B8B', '#B8860B', '#BDB76B', '#8B008B', '#FF8C00', '#9932CC', '#8FBC8F', '#FF1493', '#00BFFF', + '#FF00FF', '#FFD700', '#FF69B4', '#FF4500', '#DB7093', '#4169E1', '#6A5ACD', '#00FF7F', '#EE82EE', '#40E0D0' + ], + randomHsl: function () { + const h = Math.random() + const s = Math.random() + const l = Math.random() + return [h, s, l] + }, + hslToRgb: function (h, s, l) { + let r, g, b + + if (s === 0) { + r = g = b = l // achromatic + } else { + const hue2rgb = function hue2rgb(p, q, t) { + if (t < 0) t += 1 + if (t > 1) t -= 1 + if (t < 1 / 6) return p + (q - p) * 6 * t + if (t < 1 / 2) return q + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6 + return p + } + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s + const p = 2 * l - q + r = hue2rgb(p, q, h + 1 / 3) + g = hue2rgb(p, q, h) + b = hue2rgb(p, q, h - 1 / 3) + } + + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)] + }, + rgbToHsl: function (r, g, b) { + // eslint-disable-next-line no-sequences + r /= 255, g /= 255, b /= 255 + const max = Math.max(r, g, b); const min = Math.min(r, g, b) + let h; let s; const l = (max + min) / 2 + + if (max === min) { + h = s = 0 // achromatic + } else { + const d = max - min + s = l > 0.5 ? d / (2 - max - min) : d / (max + min) + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break + case g: h = (b - r) / d + 2; break + case b: h = (r - g) / d + 4; break + } + h /= 6 + } + + return [h, s, l] + }, + // 固定颜色 + ColorFixed() { + var color = this.colorList[this.colorIndex++ % this.colorList.length] + return color + }, + // 随机颜色 + ColorRandom() { + return '#' + (Math.random() * 0xffffff << 0).toString(16) + }, + // 生成和前一个不同的随机颜色 + ColorContrast() { + this.newColor = this.randomHsl() // 获取随机的hsl,并且给一个默认的hsl + this.newColor[1] = 0.7 + this.newColor[1] * 0.2 // [0.7 - 0.9] 排除过灰颜色 + this.newColor[2] = 0.4 + this.newColor[2] * 0.2 // [0.4 - 0.8] 排除过亮过暗色 + + /** 如果oldColor不为空时,要根据车次号保证两次生成的颜色差值为difValue*/ + this.oldColor = Number(this.colorIndex) % 2 ? this.oddColor : this.eveColor + if (this.oldColor) { + /** 保证本次的颜色和上次的不一致*/ + for (let i = 0; i < this.newColor.length && i < this.oldColor.length; i++) { + if (i === 0 && Math.abs(this.newColor[i].toFixed(2) - this.oldColor[i].toFixed(2)) < this.difValue) { + this.toCreate() + break + } + } + } + + /** 保存之前的颜色状态*/ + if (Number(this.colorIndex) % 2) { + this.oddColor = this.newColor + } else { + this.eveColor = this.newColor + } + + this.colorIndex += 1 + return `#${this.hslToRgb(...this.newColor).map(e => { return Number(e).toString(16) }).join('')}` + }, + // 渐进颜色 + ColorProgressiveColor() { + }, + toCreate: function () { + return this.ColorRandom() + } +} + +/** 对list数据进行排序, 相同元素保持原有顺序*/ +export function SortListByCallBack(list, callback) { + list.map((elem, index) => { elem[`oldIndex`] = index }) + list.sort((a, b) => { + return callback(a, b) || a.oldIndex - b.oldIndex + }) + return list +} + +/** 将数字转换成asc码*/ +export function NumToAsc(num) { + const nmA = 'A'.charCodeAt(0) + const nmZ = 'Z'.charCodeAt(0) + const len = nmZ - nmA + 1 + let str = '' + + while (num >= 0) { + str = String.fromCharCode(num % len + nmA) + str + num = Math.floor(num / len) - 1 + } + + return str +} + +/** 将asc码转换成数字*/ +export function AscToNum(asc) { + const base = 'A'.charCodeAt() - 1 + let idx = asc.length - 1 + let num = 0 + let mulFactor = 1 + while (idx >= 0) { + num += (asc[idx].charCodeAt() - base) * mulFactor + mulFactor *= 26 + idx -= 1 + } + + return num +} + +/** 将时间格式化前补零*/ +export function FormatTime(time) { + let str = `${time}` || '' + if (str) { + const list = str.split(':') + str = list.map(elem => { + return `00000${elem}`.substr(-2) + }).join(':') + } + + return str +} + +/** 根据索引获取单元格的数据*/ +export function getCellValue(Sheet, index) { + let value + const cell = Sheet[index] + if (cell) { + value = cell.w || cell.v + } + + return value +} + +/** 转换sheet数据为json数据*/ +export function ConvertSheetToList(Sheet, isReverse) { + const dataList = [] + + if (Sheet) { + const refarea = Sheet['!ref'] + const regular = /([a-zA-Z]+)([0-9]+):([a-zA-Z]+)([0-9]+)/i + + if (refarea == null) return [] // "A1:M698" + if (regular.test(refarea)) { + /** 正则转换解析行列数据*/ + const CoordList = regular.exec(refarea) + /** 转换数据为二维数组*/ + const colBeg = AscToNum(CoordList[1]) + const colEnd = AscToNum(CoordList[3]) + const rowBeg = Number(CoordList[2]) + const rowEnd = Number(CoordList[4]) + + if (isReverse) { + for (let i = colBeg - 1; i < colEnd; i++) { + dataList.push([]) + for (let j = rowBeg; j <= rowEnd; j++) { + dataList[dataList.length - 1].push(getCellValue(Sheet, NumToAsc(i) + j)) + } + } + } else { + for (let i = rowBeg; i <= rowEnd; i++) { + dataList.push([]) + for (let j = colBeg - 1; j < colEnd; j++) { + dataList[dataList.length - 1].push(getCellValue(Sheet, NumToAsc(j) + i)) + } + } + } + } + } + + return dataList +} diff --git a/src/utils/sock.js b/src/utils/sock.js new file mode 100644 index 000000000..a55474c99 --- /dev/null +++ b/src/utils/sock.js @@ -0,0 +1,211 @@ +import { handleToken } from '@/utils/auth'; +import { checkLoginLine } from '@/api/login'; + +var SockJS = require('sockjs-client'); +var Stomp = require('stompjs'); + +const isDev = process.env.NODE_ENV === 'development'; +const isTest = process.env.NODE_ENV === 'test'; +const websocketUrl = process.env.BASE_API + '/joylink-websocket?token='; + +var StompClient = function (headers) { + this.url = websocketUrl + handleToken(); + this.headers = headers || {}; + this.connect(); +}; + +StompClient.prototype = { + socket: null, + + clientIns: null, + + subscribeMap: null, + + url: '', + + status: false, + + sockStatus: 0, + + headers: { + 'X-Token': handleToken(), + }, + + count: 0, + + topic: '', + + onmessage: null, + + checkTimer: null, + + // 连接服务端 + connect() { + return new Promise((resolve, reject) => { + try { + // 断开已有连接 + if (this.clientIns && this.clientIns.connected) { + this.clientIns.disconnect(); + this.clientIns = null; + } + + // 建立连接对象(还未发起连接) + this.socket = new SockJS(websocketUrl + handleToken()); + + // 获取 STOMP 子协议的客户端对象 + this.clientIns = Stomp.over(this.socket); + + this.closeStompDebug(); + + // 向服务器发起websocket连接并发送CONNECT帧 + this.clientIns.connect({ 'X-Token': handleToken(), }, () => { + console.info('连接成功.'); + this.count = 0; + this.status = true; + + // 恢复订阅 + if (this.topic && this.onmessage) { + this.unsubscribe(this.topic); + this.subscribe(this.topic, this.onmessage, this.headers); + } + + // 检测sock是否断开 + if (!this.clientIns.ws.onclose) { + this.clientIns.ws.onclose = () => { + console.info('检测到socket断开!'); + this.status = false; + this.count++; + this.reconnect(this.count); + }; + } + + // 定时器检测网络 + if (!this.checkTimer) { + this.checkTimer = setInterval(() => { + if (!this.status || !this.clientIns.ws.onclose) { + // 发送检测心跳,如果失败则在如下情况时需要断开WebSocket; + // 40003/40004/40005: 登陆过期; + // 50008: 非法的token; + // 50012: 其他客户端登录了; + // 50014: Token 过期了; + checkLoginLine().then(() => { + // 会有连接延时,需要多次判断 + // 如果socket或着clientIns断开,则重新连接 + if (!this.status || !this.clientIns.ws.onclose) { + this.count++; + this.reconnect(this.count); + } + }).catch((err) => { + if (err.code == 40003 || err.code == 40004 || err.code == 40005 || + err.code == 50008 || err.code == 50012 || err.code == 50014) { + this.status = false; + this.count++; + this.reconnect(this.count); + } + }); + } + }, 30000); + } + resolve(this); + }, () => { + this.connect(); + }); + } catch (err) { + reject(err); + } + }); + + }, + // 恢复链接 + reconnect(count) { + console.info(`尝试第${count || 1}次连接.`); + this.connect().then(() => { }).catch(() => { + this.count++; + this.reconnect(this.count); + }); + }, + + closeStompDebug() { + if (this.clientIns) { + this.clientIns.debug = undefined; + if (isDev || isTest) { + this.clientIns.debug = function (message) { + console.debug(message); + }; + } + } + }, + + // 订阅指定的topic + subscribe(topic, onmessage, headers) { + this.topic = topic; + this.onmessage = onmessage; + this.headers = headers; + if (this.status) { + if (!this.subscribeMap) { + this.subscribeMap = new Map(); + } + + try { + var subscription = this.subscribeMap.get(topic); + if (!subscription) { + subscription = this.clientIns.subscribe(topic, onmessage, headers); // 接收消息通过 subscribe() 方法实现 + this.subscribeMap.set(topic, subscription); + } + } catch (err) { + setTimeout(() => { + this.subscribe(topic, onmessage, headers); + }, 300); + } + + } else { + setTimeout(() => { + this.subscribe(topic, onmessage, headers); + }, 300); + } + }, + + unsubscribe(topic) { + if (this.subscribeMap) { + let subscription = this.subscribeMap.get(topic); + if (subscription) { + subscription.unsubscribe(); + this.subscribeMap.delete(topic); + console.log("取消订阅"); + } + } + }, + + // 发送消息 + send(url, msg) { + if (this.status) { + try { + this.clientIns.send(url, {}, msg); + } catch (err) { + this.status = false; + this.send(url, msg); + } + } else { + setTimeout(() => { + this.send(url, msg); + }, 300); + } + }, + + disconnect() { + if (this.checkTimer) { + clearInterval(this.checkTimer); + this.checkTimer = null; + } + + if (this.clientIns && this.clientIns.connected) { + this.clientIns.disconnect(); + this.clientIns = null; + } + this.status = false; + console.log("断开连接"); + } + +}; + +export default StompClient; diff --git a/src/utils/stomp.js b/src/utils/stomp.js new file mode 100644 index 000000000..5a962e351 --- /dev/null +++ b/src/utils/stomp.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import StompClient from '@/utils/sock'; +import store from '@/store'; + +export const perpetualTopic = '/user/topic/message'; // 公用topic +export const displayTopic = '/user/queue/simulation'; // 其他仿真topic +export const screenTopic = '/user/queue/simulation/bigScreen'; // 大屏仿真 +export const planTopic = '/user/queue/simulation/plan'; // 测试运行仿真 + +// 建立连接并订阅地址 +export function creatSubscribe(topic, header) { + try { + if (!Vue.prototype.$stomp) { + Vue.prototype.$stomp = new StompClient(); + } + Vue.prototype.$stomp.subscribe(topic, callback, header); + } catch (error) { + console.error('websocket订阅失败'); + } +} + +// 回调函数 +function callback(Response) { + if (store) { + let data = JSON.parse(Response.body); + store.dispatch('socket/setStomp', data); + } else { + callback(Response); + } +} + +// 删除订阅路径 +export function clearSubscribe(topic) { + if (Vue.prototype.$stomp) { + Vue.prototype.$stomp.unsubscribe(topic); + } +} \ No newline at end of file diff --git a/src/utils/throttle.js b/src/utils/throttle.js new file mode 100644 index 000000000..d0e02b4c4 --- /dev/null +++ b/src/utils/throttle.js @@ -0,0 +1,170 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +var ORIGIN_METHOD = '\0__throttleOriginMethod' +var RATE = '\0__throttleRate' +var THROTTLE_TYPE = '\0__throttleType' + +/** + * @public + * @param {(Function)} fn + * @param {number} [delay=0] Unit: ms. + * @param {boolean} [debounce=false] + * true: If call interval less than `delay`, only the last call works. + * false: If call interval less than `delay, call works on fixed rate. + * @return {(Function)} throttled fn. + */ +export function throttle(fn, delay, debounce) { + var currCall + var lastCall = 0 + var lastExec = 0 + var timer = null + var diff + var scope + var args + var debounceNextCall + + delay = delay || 0 + + function exec() { + lastExec = (new Date()).getTime() + timer = null + fn.apply(scope, args || []) + } + + var cb = function() { + currCall = (new Date()).getTime() + scope = this + args = arguments + var thisDelay = debounceNextCall || delay + var thisDebounce = debounceNextCall || debounce + debounceNextCall = null + diff = currCall - (thisDebounce ? lastCall : lastExec) - thisDelay + + clearTimeout(timer) + + // Here we should make sure that: the `exec` SHOULD NOT be called later + // than a new call of `cb`, that is, preserving the command order. Consider + // calculating "scale rate" when roaming as an example. When a call of `cb` + // happens, either the `exec` is called dierectly, or the call is delayed. + // But the delayed call should never be later than next call of `cb`. Under + // this assurance, we can simply update view state each time `dispatchAction` + // triggered by user roaming, but not need to add extra code to avoid the + // state being "rolled-back". + if (thisDebounce) { + timer = setTimeout(exec, thisDelay) + } else { + if (diff >= 0) { + exec() + } else { + timer = setTimeout(exec, -diff) + } + } + + lastCall = currCall + } + + /** + * Clear throttle. + * @public + */ + cb.clear = function() { + if (timer) { + clearTimeout(timer) + timer = null + } + } + + /** + * Enable debounce once. + */ + cb.debounceNextCall = function(debounceDelay) { + debounceNextCall = debounceDelay + } + + return cb +} + +/** + * Create throttle method or update throttle rate. + * + * @example + * ComponentView.prototype.render = function () { + * ... + * throttle.createOrUpdate( + * this, + * '_dispatchAction', + * this.model.get('throttle'), + * 'fixRate' + * ); + * }; + * ComponentView.prototype.remove = function () { + * throttle.clear(this, '_dispatchAction'); + * }; + * ComponentView.prototype.dispose = function () { + * throttle.clear(this, '_dispatchAction'); + * }; + * + * @public + * @param {Object} obj + * @param {string} fnAttr + * @param {number} [rate] + * @param {string} [throttleType='fixRate'] 'fixRate' or 'debounce' + * @return {Function} throttled function. + */ +export function createOrUpdate(obj, fnAttr, rate, throttleType) { + var fn = obj[fnAttr] + + if (!fn) { + return + } + + var originFn = fn[ORIGIN_METHOD] || fn + var lastThrottleType = fn[THROTTLE_TYPE] + var lastRate = fn[RATE] + + if (lastRate !== rate || lastThrottleType !== throttleType) { + if (rate == null || !throttleType) { + return (obj[fnAttr] = originFn) + } + + fn = obj[fnAttr] = throttle( + originFn, rate, throttleType === 'debounce' + ) + fn[ORIGIN_METHOD] = originFn + fn[THROTTLE_TYPE] = throttleType + fn[RATE] = rate + } + + return fn +} + +/** + * Clear throttle. Example see throttle.createOrUpdate. + * + * @public + * @param {Object} obj + * @param {string} fnAttr + */ +export function clear(obj, fnAttr) { + var fn = obj[fnAttr] + if (fn && fn[ORIGIN_METHOD]) { + obj[fnAttr] = fn[ORIGIN_METHOD] + } +} diff --git a/src/utils/time.js b/src/utils/time.js new file mode 100644 index 000000000..e9eef6b07 --- /dev/null +++ b/src/utils/time.js @@ -0,0 +1,46 @@ +export function now() { + var d = new Date() + var year = d.getFullYear() + var month = change(d.getMonth() + 1) + var day = change(d.getDate()) + var hour = change(d.getHours()) + var minute = change(d.getMinutes()) + var second = change(d.getSeconds()) + function change(t) { + if (t < 10) { + return '0' + t + } else { + return t + } + } + return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second +} + +export function timeFormat(time) { + function getTimeStr(val) { + return val < 10 ? '0' + val : val + } + + if (Number.isInteger(time)) { + let hour = 0; let minute = 0; let second = 0 + if (time) { + if (time >= 3600) { + hour = Math.floor(time / 3600) % 24 + time = (time % 3600) + } + if (time >= 60) { + minute = Math.floor(time / 60) + time = time % 60 + } + second = Math.floor(time) + } + + return getTimeStr(hour) + ':' + getTimeStr(minute) + ':' + getTimeStr(second) + } else { + return '' + } +} + +export function prefixIntrger(num, length) { + return (Array(length).join('0') + num).slice(-length) +} diff --git a/src/utils/validate.js b/src/utils/validate.js new file mode 100644 index 000000000..55d9eb01c --- /dev/null +++ b/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 = [''] + return valid_map.indexOf(str.trim()) < 0 +} diff --git a/src/views/components/drapLeft/index.vue b/src/views/components/drapLeft/index.vue new file mode 100644 index 000000000..7c5bfb0fb --- /dev/null +++ b/src/views/components/drapLeft/index.vue @@ -0,0 +1,106 @@ + + + + + + + + diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue new file mode 100644 index 000000000..32d939bf3 --- /dev/null +++ b/src/views/dashboard/index.vue @@ -0,0 +1,31 @@ + + + name: {{ name }} + + + + + + diff --git a/src/views/error-page/401.vue b/src/views/error-page/401.vue new file mode 100644 index 000000000..5059873e8 --- /dev/null +++ b/src/views/error-page/401.vue @@ -0,0 +1,107 @@ + + + + 返回 + + + + + Oops! + + gif来源airbnb 页面 + 你没有权限去该页面 + 如有不满请联系你领导 + + 或者你可以去: + + + 回首页 + + + + 随便看看 + + 点我看图 + + + + + + + + + + + + + + + diff --git a/src/views/error-page/404.vue b/src/views/error-page/404.vue new file mode 100644 index 000000000..03e20fcd5 --- /dev/null +++ b/src/views/error-page/404.vue @@ -0,0 +1,255 @@ + + + + + + + + + + + OOPS! + All rights reserved + wallstreetcn + + {{ message }} + Please check that the URL you entered is correct, or click the button below to + return to the homepage. + Back to home + + + + + + + + diff --git a/src/views/login/index.vue b/src/views/login/index.vue new file mode 100644 index 000000000..3a7629bf2 --- /dev/null +++ b/src/views/login/index.vue @@ -0,0 +1,208 @@ + + + + 用户登陆 + + + + + + + + + + + + + + + + + + 琏课堂 + + + + 登陆 + + + + + + + + + + + \ No newline at end of file diff --git a/src/views/login/loginNew.vue b/src/views/login/loginNew.vue new file mode 100644 index 000000000..49b26caf5 --- /dev/null +++ b/src/views/login/loginNew.vue @@ -0,0 +1,192 @@ + + + + + 琏课堂 + + + + + + + 扫码成功 + + 请在微信上进行后续操作 + + 使用手机微信扫码登录 + + + + 推荐配置 + 浏览器: + 谷歌浏览器 + + + 屏幕分辨率:1920*1080 + + + + + + + + + + \ No newline at end of file diff --git a/src/views/login/loginNewPlan.vue b/src/views/login/loginNewPlan.vue new file mode 100644 index 000000000..9e92fab48 --- /dev/null +++ b/src/views/login/loginNewPlan.vue @@ -0,0 +1,188 @@ + + + + + 琏计划 + + + + + + + 扫码成功 + + 请在微信上进行后续操作 + + 使用手机微信扫码登录 + + + + 推荐配置 + 浏览器: + 谷歌浏览器 + + + 屏幕分辨率:1920*1080 + + + + + + + + + + \ No newline at end of file diff --git a/src/views/login/loginNewScreen.vue b/src/views/login/loginNewScreen.vue new file mode 100644 index 000000000..96c7dce2a --- /dev/null +++ b/src/views/login/loginNewScreen.vue @@ -0,0 +1,188 @@ + + + + + 大屏系统 + + + + + + + 扫码成功 + + 请在微信上进行后续操作 + + 使用手机微信扫码登录 + + + + 推荐配置 + 浏览器: + 谷歌浏览器 + + + 屏幕分辨率:1920*1080 + + + + + + + + + + \ No newline at end of file diff --git a/src/views/login/loginPlan.vue b/src/views/login/loginPlan.vue new file mode 100644 index 000000000..835bc2a61 --- /dev/null +++ b/src/views/login/loginPlan.vue @@ -0,0 +1,210 @@ + + + + 用户登陆 + + + + + + + + + + + + + + + + + + 琏计划 + + + + 登陆 + + + + + + + + + + + \ No newline at end of file diff --git a/src/views/login/loginScreen.vue b/src/views/login/loginScreen.vue new file mode 100644 index 000000000..01ee17457 --- /dev/null +++ b/src/views/login/loginScreen.vue @@ -0,0 +1,210 @@ + + + + 用户登陆 + + + + + + + + + + + + + + + + + + 大屏系统 + + + + 登陆 + + + + + + + + + + + \ No newline at end of file diff --git a/tests/unit/.eslintrc.js b/tests/unit/.eslintrc.js new file mode 100644 index 000000000..958d51ba2 --- /dev/null +++ b/tests/unit/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + env: { + jest: true + } +} diff --git a/tests/unit/components/Breadcrumb.spec.js b/tests/unit/components/Breadcrumb.spec.js new file mode 100644 index 000000000..1d94c8fc7 --- /dev/null +++ b/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) + }) +}) diff --git a/tests/unit/components/Hamburger.spec.js b/tests/unit/components/Hamburger.spec.js new file mode 100644 index 000000000..01ea303a5 --- /dev/null +++ b/tests/unit/components/Hamburger.spec.js @@ -0,0 +1,18 @@ +import { shallowMount } from '@vue/test-utils' +import Hamburger from '@/components/Hamburger/index.vue' +describe('Hamburger.vue', () => { + it('toggle click', () => { + const wrapper = shallowMount(Hamburger) + const mockFn = jest.fn() + wrapper.vm.$on('toggleClick', mockFn) + wrapper.find('.hamburger').trigger('click') + expect(mockFn).toBeCalled() + }) + it('prop isActive', () => { + const wrapper = shallowMount(Hamburger) + wrapper.setProps({ isActive: true }) + expect(wrapper.contains('.is-active')).toBe(true) + wrapper.setProps({ isActive: false }) + expect(wrapper.contains('.is-active')).toBe(false) + }) +}) diff --git a/tests/unit/components/SvgIcon.spec.js b/tests/unit/components/SvgIcon.spec.js new file mode 100644 index 000000000..31467a9f6 --- /dev/null +++ b/tests/unit/components/SvgIcon.spec.js @@ -0,0 +1,22 @@ +import { shallowMount } from '@vue/test-utils' +import SvgIcon from '@/components/SvgIcon/index.vue' +describe('SvgIcon.vue', () => { + it('iconClass', () => { + const wrapper = shallowMount(SvgIcon, { + propsData: { + iconClass: 'test' + } + }) + expect(wrapper.find('use').attributes().href).toBe('#icon-test') + }) + it('className', () => { + const wrapper = shallowMount(SvgIcon, { + propsData: { + iconClass: 'test' + } + }) + expect(wrapper.classes().length).toBe(1) + wrapper.setProps({ className: 'test' }) + expect(wrapper.classes().includes('test')).toBe(true) + }) +}) diff --git a/tests/unit/utils/formatTime.spec.js b/tests/unit/utils/formatTime.spec.js new file mode 100644 index 000000000..24e165b42 --- /dev/null +++ b/tests/unit/utils/formatTime.spec.js @@ -0,0 +1,30 @@ +import { formatTime } from '@/utils/index.js' + +describe('Utils:formatTime', () => { + const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" + const retrofit = 5 * 1000 + + it('ten digits timestamp', () => { + expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') + }) + it('test now', () => { + expect(formatTime(+new Date() - 1)).toBe('刚刚') + }) + it('less two minute', () => { + expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') + }) + it('less two hour', () => { + expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') + }) + it('less one day', () => { + expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') + }) + it('more than one day', () => { + expect(formatTime(d)).toBe('7月13日17时54分') + }) + it('format', () => { + expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') + expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') + expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') + }) +}) diff --git a/tests/unit/utils/parseTime.spec.js b/tests/unit/utils/parseTime.spec.js new file mode 100644 index 000000000..41d1b028c --- /dev/null +++ b/tests/unit/utils/parseTime.spec.js @@ -0,0 +1,28 @@ +import { parseTime } from '@/utils/index.js' + +describe('Utils:parseTime', () => { + const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" + it('timestamp', () => { + expect(parseTime(d)).toBe('2018-07-13 17:54:01') + }) + it('ten digits timestamp', () => { + expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') + }) + it('new Date', () => { + expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') + }) + it('format', () => { + expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') + expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') + expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') + }) + it('get the day of the week', () => { + expect(parseTime(d, '{a}')).toBe('五') // 星期五 + }) + it('get the day of the week', () => { + expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 + }) + it('empty argument', () => { + expect(parseTime()).toBeNull() + }) +}) diff --git a/tests/unit/utils/validate.spec.js b/tests/unit/utils/validate.spec.js new file mode 100644 index 000000000..f774905b0 --- /dev/null +++ b/tests/unit/utils/validate.spec.js @@ -0,0 +1,17 @@ +import { validUsername, isExternal } from '@/utils/validate.js' + +describe('Utils:validate', () => { + it('validUsername', () => { + expect(validUsername('admin')).toBe(true) + expect(validUsername('editor')).toBe(true) + expect(validUsername('xxxx')).toBe(false) + }) + it('isExternal', () => { + expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) + expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) + expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false) + expect(isExternal('/dashboard')).toBe(false) + expect(isExternal('./dashboard')).toBe(false) + expect(isExternal('dashboard')).toBe(false) + }) +}) diff --git a/vue.config.js b/vue.config.js new file mode 100644 index 000000000..d17fb6992 --- /dev/null +++ b/vue.config.js @@ -0,0 +1,135 @@ +'use strict' +// import { getBasePathConfig } from '@/utils/baseUrl' + +const path = require('path') +const defaultSettings = require('./src/settings.js') + +function resolve(dir) { + return path.join(__dirname, dir) +} + +const name = defaultSettings.title // page title +const port = 9527 // dev port + +// All configuration item explanations can be find in https://cli.vuejs.org/config/ +module.exports = { + /** + * You will need to set publicPath if you plan to deploy your site under a sub path, + * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/, + * then publicPath should be set to "/bar/". + * In most cases please use '/' !!! + * Detail: https://cli.vuejs.org/config/#publicpath + */ + publicPath: '/', + outputDir: 'dist', + assetsDir: 'static', + lintOnSave: false, + productionSourceMap: false, + devServer: { + port: port, + open: true, + overlay: { + warnings: false, + errors: true + } + // proxy: { + // // change xxx-api/login => mock/login + // // detail: https://cli.vuejs.org/config/#devserver-proxy + // [process.env.VUE_APP_BASE_API]: { + // target: `http://localhost:${port}/mock`, + // changeOrigin: true, + // pathRewrite: { + // ['^' + process.env.VUE_APP_BASE_API]: '' + // } + // } + // }, + // after: require('./mock/mock-server.js') + }, + configureWebpack: { + // provide the app's title in webpack's name field, so that + // it can be accessed in index.html to inject the correct title. + name: name, + resolve: { + alias: { + '@': resolve('src') + } + } + }, + chainWebpack(config) { + config.plugins.delete('preload') // TODO: need test + config.plugins.delete('prefetch') // TODO: need test + + // set svg-sprite-loader + config.module + .rule('svg') + .exclude.add(resolve('src/icons')) + .end() + config.module + .rule('icons') + .test(/\.svg$/) + .include.add(resolve('src/icons')) + .end() + .use('svg-sprite-loader') + .loader('svg-sprite-loader') + .options({ + symbolId: 'icon-[name]' + }) + .end() + + // set preserveWhitespace + config.module + .rule('vue') + .use('vue-loader') + .loader('vue-loader') + .tap(options => { + options.compilerOptions.preserveWhitespace = true + return options + }) + .end() + + config + // https://webpack.js.org/configuration/devtool/#development + .when(process.env.NODE_ENV === 'development', + config => config.devtool('cheap-source-map') + ) + + config + .when(process.env.NODE_ENV !== 'development', + config => { + config + .plugin('ScriptExtHtmlWebpackPlugin') + .after('html') + .use('script-ext-html-webpack-plugin', [{ + // `runtime` must same as runtimeChunk name. default is `runtime` + inline: /runtime\..*\.js$/ + }]) + .end() + config + .optimization.splitChunks({ + chunks: 'all', + cacheGroups: { + libs: { + name: 'chunk-libs', + test: /[\\/]node_modules[\\/]/, + priority: 10, + chunks: 'initial' // only package third parties that are initially dependent + }, + elementUI: { + name: 'chunk-elementUI', // split elementUI into a single package + priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app + test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm + }, + commons: { + name: 'chunk-commons', + test: resolve('src/components'), // can customize your rules + minChunks: 3, // minimum common number + priority: 5, + reuseExistingChunk: true + } + } + }) + config.optimization.runtimeChunk('single') + } + ) + } +}