| @@ -0,0 +1,14 @@ | |||
| # http://editorconfig.org | |||
| root = true | |||
| [*] | |||
| charset = utf-8 | |||
| indent_style = space | |||
| indent_size = 2 | |||
| end_of_line = lf | |||
| insert_final_newline = true | |||
| trim_trailing_whitespace = true | |||
| [*.md] | |||
| insert_final_newline = false | |||
| trim_trailing_whitespace = false | |||
| @@ -0,0 +1,5 @@ | |||
| # just a flag | |||
| ENV = 'development' | |||
| # base api | |||
| VUE_APP_BASE_API = '/video-admin' | |||
| @@ -0,0 +1,6 @@ | |||
| # just a flag | |||
| ENV = 'production' | |||
| # base api | |||
| VUE_APP_BASE_API = 'video-admin' | |||
| @@ -0,0 +1,8 @@ | |||
| NODE_ENV = production | |||
| # just a flag | |||
| ENV = 'staging' | |||
| # base api | |||
| VUE_APP_BASE_API = '/stage-api' | |||
| @@ -0,0 +1,4 @@ | |||
| build/*.js | |||
| src/assets | |||
| public | |||
| dist | |||
| @@ -0,0 +1,198 @@ | |||
| module.exports = { | |||
| root: true, | |||
| parserOptions: { | |||
| parser: 'babel-eslint', | |||
| sourceType: 'module' | |||
| }, | |||
| env: { | |||
| browser: true, | |||
| node: true, | |||
| es6: true, | |||
| }, | |||
| extends: ['plugin:vue/recommended', 'eslint:recommended'], | |||
| // add your custom rules here | |||
| //it is base on https://github.com/vuejs/eslint-config-vue | |||
| rules: { | |||
| "vue/max-attributes-per-line": [2, { | |||
| "singleline": 10, | |||
| "multiline": { | |||
| "max": 1, | |||
| "allowFirstLine": false | |||
| } | |||
| }], | |||
| "vue/singleline-html-element-content-newline": "off", | |||
| "vue/multiline-html-element-content-newline":"off", | |||
| "vue/name-property-casing": ["error", "PascalCase"], | |||
| "vue/no-v-html": "off", | |||
| 'accessor-pairs': 2, | |||
| 'arrow-spacing': [2, { | |||
| 'before': true, | |||
| 'after': true | |||
| }], | |||
| 'block-spacing': [2, 'always'], | |||
| 'brace-style': [2, '1tbs', { | |||
| 'allowSingleLine': true | |||
| }], | |||
| 'camelcase': [0, { | |||
| 'properties': 'always' | |||
| }], | |||
| 'comma-dangle': [2, 'never'], | |||
| 'comma-spacing': [2, { | |||
| 'before': false, | |||
| 'after': true | |||
| }], | |||
| 'comma-style': [2, 'last'], | |||
| 'constructor-super': 2, | |||
| 'curly': [2, 'multi-line'], | |||
| 'dot-location': [2, 'property'], | |||
| 'eol-last': 2, | |||
| 'eqeqeq': ["error", "always", {"null": "ignore"}], | |||
| 'generator-star-spacing': [2, { | |||
| 'before': true, | |||
| 'after': true | |||
| }], | |||
| 'handle-callback-err': [2, '^(err|error)$'], | |||
| 'indent': [2, 2, { | |||
| 'SwitchCase': 1 | |||
| }], | |||
| 'jsx-quotes': [2, 'prefer-single'], | |||
| 'key-spacing': [2, { | |||
| 'beforeColon': false, | |||
| 'afterColon': true | |||
| }], | |||
| 'keyword-spacing': [2, { | |||
| 'before': true, | |||
| 'after': true | |||
| }], | |||
| 'new-cap': [2, { | |||
| 'newIsCap': true, | |||
| 'capIsNew': false | |||
| }], | |||
| 'new-parens': 2, | |||
| 'no-array-constructor': 2, | |||
| 'no-caller': 2, | |||
| 'no-console': 'off', | |||
| 'no-class-assign': 2, | |||
| 'no-cond-assign': 2, | |||
| 'no-const-assign': 2, | |||
| 'no-control-regex': 0, | |||
| 'no-delete-var': 2, | |||
| 'no-dupe-args': 2, | |||
| 'no-dupe-class-members': 2, | |||
| 'no-dupe-keys': 2, | |||
| 'no-duplicate-case': 2, | |||
| 'no-empty-character-class': 2, | |||
| 'no-empty-pattern': 2, | |||
| 'no-eval': 2, | |||
| 'no-ex-assign': 2, | |||
| 'no-extend-native': 2, | |||
| 'no-extra-bind': 2, | |||
| 'no-extra-boolean-cast': 2, | |||
| 'no-extra-parens': [2, 'functions'], | |||
| 'no-fallthrough': 2, | |||
| 'no-floating-decimal': 2, | |||
| 'no-func-assign': 2, | |||
| 'no-implied-eval': 2, | |||
| 'no-inner-declarations': [2, 'functions'], | |||
| 'no-invalid-regexp': 2, | |||
| 'no-irregular-whitespace': 2, | |||
| 'no-iterator': 2, | |||
| 'no-label-var': 2, | |||
| 'no-labels': [2, { | |||
| 'allowLoop': false, | |||
| 'allowSwitch': false | |||
| }], | |||
| 'no-lone-blocks': 2, | |||
| 'no-mixed-spaces-and-tabs': 2, | |||
| 'no-multi-spaces': 2, | |||
| 'no-multi-str': 2, | |||
| 'no-multiple-empty-lines': [2, { | |||
| 'max': 1 | |||
| }], | |||
| 'no-native-reassign': 2, | |||
| 'no-negated-in-lhs': 2, | |||
| 'no-new-object': 2, | |||
| 'no-new-require': 2, | |||
| 'no-new-symbol': 2, | |||
| 'no-new-wrappers': 2, | |||
| 'no-obj-calls': 2, | |||
| 'no-octal': 2, | |||
| 'no-octal-escape': 2, | |||
| 'no-path-concat': 2, | |||
| 'no-proto': 2, | |||
| 'no-redeclare': 2, | |||
| 'no-regex-spaces': 2, | |||
| 'no-return-assign': [2, 'except-parens'], | |||
| 'no-self-assign': 2, | |||
| 'no-self-compare': 2, | |||
| 'no-sequences': 2, | |||
| 'no-shadow-restricted-names': 2, | |||
| 'no-spaced-func': 2, | |||
| 'no-sparse-arrays': 2, | |||
| 'no-this-before-super': 2, | |||
| 'no-throw-literal': 2, | |||
| 'no-trailing-spaces': 2, | |||
| 'no-undef': 2, | |||
| 'no-undef-init': 2, | |||
| 'no-unexpected-multiline': 2, | |||
| 'no-unmodified-loop-condition': 2, | |||
| 'no-unneeded-ternary': [2, { | |||
| 'defaultAssignment': false | |||
| }], | |||
| 'no-unreachable': 2, | |||
| 'no-unsafe-finally': 2, | |||
| 'no-unused-vars': [2, { | |||
| 'vars': 'all', | |||
| 'args': 'none' | |||
| }], | |||
| 'no-useless-call': 2, | |||
| 'no-useless-computed-key': 2, | |||
| 'no-useless-constructor': 2, | |||
| 'no-useless-escape': 0, | |||
| 'no-whitespace-before-property': 2, | |||
| 'no-with': 2, | |||
| 'one-var': [2, { | |||
| 'initialized': 'never' | |||
| }], | |||
| 'operator-linebreak': [2, 'after', { | |||
| 'overrides': { | |||
| '?': 'before', | |||
| ':': 'before' | |||
| } | |||
| }], | |||
| 'padded-blocks': [2, 'never'], | |||
| 'quotes': [2, 'single', { | |||
| 'avoidEscape': true, | |||
| 'allowTemplateLiterals': true | |||
| }], | |||
| 'semi': [2, 'never'], | |||
| 'semi-spacing': [2, { | |||
| 'before': false, | |||
| 'after': true | |||
| }], | |||
| 'space-before-blocks': [2, 'always'], | |||
| 'space-before-function-paren': [2, 'never'], | |||
| 'space-in-parens': [2, 'never'], | |||
| 'space-infix-ops': 2, | |||
| 'space-unary-ops': [2, { | |||
| 'words': true, | |||
| 'nonwords': false | |||
| }], | |||
| 'spaced-comment': [2, 'always', { | |||
| 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] | |||
| }], | |||
| 'template-curly-spacing': [2, 'never'], | |||
| 'use-isnan': 2, | |||
| 'valid-typeof': 2, | |||
| 'wrap-iife': [2, 'any'], | |||
| 'yield-star-spacing': [2, 'both'], | |||
| 'yoda': [2, 'never'], | |||
| 'prefer-const': 2, | |||
| 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, | |||
| 'object-curly-spacing': [2, 'always', { | |||
| objectsInObjects: false | |||
| }], | |||
| 'array-bracket-spacing': [2, 'never'] | |||
| } | |||
| } | |||
| @@ -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 | |||
| @@ -0,0 +1,5 @@ | |||
| language: node_js | |||
| node_js: 10 | |||
| script: npm run test | |||
| notifications: | |||
| email: false | |||
| @@ -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. | |||
| @@ -0,0 +1,102 @@ | |||
| # vue-admin-template | |||
| > 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。 | |||
| [线上地址](http://panjiachen.github.io/vue-admin-template) | |||
| [国内访问](https://panjiachen.gitee.io/vue-admin-template) | |||
| 目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。 | |||
| ## Extra | |||
| 如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) | |||
| ## 相关项目 | |||
| - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) | |||
| - [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) | |||
| - [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) | |||
| - [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312) | |||
| 写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目: | |||
| - [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2) | |||
| - [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac) | |||
| - [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35) | |||
| - [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56) | |||
| - [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836) | |||
| ## Build Setup | |||
| ```bash | |||
| # 克隆项目 | |||
| git clone https://github.com/PanJiaChen/vue-admin-template.git | |||
| # 进入项目目录 | |||
| cd vue-admin-template | |||
| # 安装依赖 | |||
| npm install | |||
| # 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 | |||
| npm install --registry=https://registry.npm.taobao.org | |||
| # 启动服务 | |||
| npm run dev | |||
| ``` | |||
| 浏览器访问 [http://localhost:9528](http://localhost:9528) | |||
| ## 发布 | |||
| ```bash | |||
| # 构建测试环境 | |||
| npm run build:stage | |||
| # 构建生产环境 | |||
| npm run build:prod | |||
| ``` | |||
| ## 其它 | |||
| ```bash | |||
| # 预览发布环境效果 | |||
| npm run preview | |||
| # 预览发布环境效果 + 静态资源分析 | |||
| npm run preview -- --report | |||
| # 代码格式检查 | |||
| npm run lint | |||
| # 代码格式检查并自动修复 | |||
| npm run lint -- --fix | |||
| ``` | |||
| 更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/) | |||
| ## 购买贴纸 | |||
| 你也可以通过 购买[官方授权的贴纸](https://smallsticker.com/product/vue-element-admin) 的方式来支持 vue-element-admin - 每售出一张贴纸,我们将获得 2 元的捐赠。 | |||
| ## Demo | |||
|  | |||
| ## Browsers support | |||
| Modern browsers and Internet Explorer 10+. | |||
| | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | | |||
| | --------- | --------- | --------- | --------- | | |||
| | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions | |||
| ## License | |||
| [MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license. | |||
| Copyright (c) 2017-present PanJiaChen | |||
| @@ -0,0 +1,14 @@ | |||
| module.exports = { | |||
| presets: [ | |||
| // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app | |||
| '@vue/cli-plugin-babel/preset' | |||
| ], | |||
| 'env': { | |||
| 'development': { | |||
| // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require(). | |||
| // This plugin can significantly increase the speed of hot updates, when you have a large number of pages. | |||
| // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html | |||
| 'plugins': ['dynamic-import-node'] | |||
| } | |||
| } | |||
| } | |||
| @@ -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}`) | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| module.exports = { | |||
| moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], | |||
| transform: { | |||
| '^.+\\.vue$': 'vue-jest', | |||
| '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': | |||
| 'jest-transform-stub', | |||
| '^.+\\.jsx?$': 'babel-jest' | |||
| }, | |||
| moduleNameMapper: { | |||
| '^@/(.*)$': '<rootDir>/src/$1' | |||
| }, | |||
| snapshotSerializers: ['jest-serializer-vue'], | |||
| testMatch: [ | |||
| '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' | |||
| ], | |||
| collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], | |||
| coverageDirectory: '<rootDir>/tests/unit/coverage', | |||
| // 'collectCoverage': true, | |||
| 'coverageReporters': [ | |||
| 'lcov', | |||
| 'text-summary' | |||
| ], | |||
| testURL: 'http://localhost/' | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| { | |||
| "compilerOptions": { | |||
| "baseUrl": "./", | |||
| "paths": { | |||
| "@/*": ["src/*"] | |||
| } | |||
| }, | |||
| "exclude": ["node_modules", "dist"] | |||
| } | |||
| @@ -0,0 +1,59 @@ | |||
| const Mock = require('mockjs') | |||
| const { param2Obj } = require('./utils') | |||
| const user = require('./user') | |||
| const table = require('./table') | |||
| const roles = require('./roles') | |||
| const mocks = [ | |||
| ...user, | |||
| ...table, | |||
| ...roles | |||
| ] | |||
| // for front mock | |||
| // please use it cautiously, it will redefine XMLHttpRequest, | |||
| // which will cause many of your third-party libraries to be invalidated(like progress event). | |||
| function mockXHR() { | |||
| // mock patch | |||
| // https://github.com/nuysoft/Mock/issues/300 | |||
| Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send | |||
| Mock.XHR.prototype.send = function() { | |||
| if (this.custom.xhr) { | |||
| this.custom.xhr.withCredentials = this.withCredentials || false | |||
| if (this.responseType) { | |||
| this.custom.xhr.responseType = this.responseType | |||
| } | |||
| } | |||
| this.proxy_send(...arguments) | |||
| } | |||
| function XHR2ExpressReqWrap(respond) { | |||
| return function(options) { | |||
| let result = null | |||
| if (respond instanceof Function) { | |||
| const { body, type, url } = options | |||
| // https://expressjs.com/en/4x/api.html#req | |||
| result = respond({ | |||
| method: type, | |||
| body: JSON.parse(body), | |||
| query: param2Obj(url) | |||
| }) | |||
| } else { | |||
| result = respond | |||
| } | |||
| return Mock.mock(result) | |||
| } | |||
| } | |||
| for (const i of mocks) { | |||
| Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) | |||
| } | |||
| } | |||
| module.exports = { | |||
| mocks, | |||
| mockXHR | |||
| } | |||
| @@ -0,0 +1,81 @@ | |||
| const chokidar = require('chokidar') | |||
| const bodyParser = require('body-parser') | |||
| const chalk = require('chalk') | |||
| const path = require('path') | |||
| const Mock = require('mockjs') | |||
| const mockDir = path.join(process.cwd(), 'mock') | |||
| function registerRoutes(app) { | |||
| let mockLastIndex | |||
| const { mocks } = require('./index.js') | |||
| const mocksForServer = mocks.map(route => { | |||
| return responseFake(route.url, route.type, route.response) | |||
| }) | |||
| for (const mock of mocksForServer) { | |||
| app[mock.type](mock.url, mock.response) | |||
| mockLastIndex = app._router.stack.length | |||
| } | |||
| const mockRoutesLength = Object.keys(mocksForServer).length | |||
| return { | |||
| mockRoutesLength: mockRoutesLength, | |||
| mockStartIndex: mockLastIndex - mockRoutesLength | |||
| } | |||
| } | |||
| function unregisterRoutes() { | |||
| Object.keys(require.cache).forEach(i => { | |||
| if (i.includes(mockDir)) { | |||
| delete require.cache[require.resolve(i)] | |||
| } | |||
| }) | |||
| } | |||
| // for mock server | |||
| const responseFake = (url, type, respond) => { | |||
| return { | |||
| url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`), | |||
| type: type || 'get', | |||
| response(req, res) { | |||
| console.log('request invoke:' + req.path) | |||
| res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) | |||
| } | |||
| } | |||
| } | |||
| module.exports = app => { | |||
| // parse app.body | |||
| // https://expressjs.com/en/4x/api.html#req.body | |||
| app.use(bodyParser.json()) | |||
| app.use(bodyParser.urlencoded({ | |||
| extended: true | |||
| })) | |||
| const mockRoutes = registerRoutes(app) | |||
| var mockRoutesLength = mockRoutes.mockRoutesLength | |||
| var mockStartIndex = mockRoutes.mockStartIndex | |||
| // watch files, hot reload mock server | |||
| chokidar.watch(mockDir, { | |||
| ignored: /mock-server/, | |||
| ignoreInitial: true | |||
| }).on('all', (event, path) => { | |||
| if (event === 'change' || event === 'add') { | |||
| try { | |||
| // remove mock routes stack | |||
| app._router.stack.splice(mockStartIndex, mockRoutesLength) | |||
| // clear routes cache | |||
| unregisterRoutes() | |||
| const mockRoutes = registerRoutes(app) | |||
| mockRoutesLength = mockRoutes.mockRoutesLength | |||
| mockStartIndex = mockRoutes.mockStartIndex | |||
| console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`)) | |||
| } catch (error) { | |||
| console.log(chalk.redBright(error)) | |||
| } | |||
| } | |||
| }) | |||
| } | |||
| @@ -0,0 +1,136 @@ | |||
| const Mock = require('mockjs') | |||
| const data = Mock.mock({ | |||
| "allMenus": [ | |||
| { | |||
| "id": 1, | |||
| "name": "首页", | |||
| "ename": "home", | |||
| "path": "/home", | |||
| "pid": 0, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "id": 2, | |||
| "name": "视频管理", | |||
| "ename": "order-manage", | |||
| "path": "/order-manage", | |||
| "pid": 0, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "id": 3, | |||
| "name": "专题管理", | |||
| "ename": "teacher-manage", | |||
| "path": "/teacher-manage", | |||
| "pid": 0, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "id": 4, | |||
| "name": "角色管理", | |||
| "ename": "course-manage", | |||
| "path": "/course-manage", | |||
| "pid": 0, | |||
| "children": [ | |||
| { | |||
| "id": 5, | |||
| "name": "添加", | |||
| "path": "user-manage", | |||
| "pid": 4, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "id": 6, | |||
| "name": "编辑", | |||
| "path": "user-server", | |||
| "pid": 4, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "id": 7, | |||
| "name": "删除", | |||
| "path": "user-seller", | |||
| "pid": 4, | |||
| "children": [] | |||
| } | |||
| ] | |||
| }, | |||
| { | |||
| "id": 8, | |||
| "name": "用户管理", | |||
| "ename": "user", | |||
| "path": "/user", | |||
| "pid": 0, | |||
| "children": [ | |||
| { | |||
| "id": 9, | |||
| "name": "添加", | |||
| "path": "user-manage", | |||
| "pid": 8, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "id": 10, | |||
| "name": "编辑", | |||
| "path": "user-server", | |||
| "pid": 8, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "id": 11, | |||
| "name": "删除", | |||
| "path": "user-seller", | |||
| "pid": 8, | |||
| "children": [] | |||
| } | |||
| ] | |||
| }, | |||
| { | |||
| "id": 12, | |||
| "name": "系统设置", | |||
| "ename": "system-manage", | |||
| "path": "/system-manage", | |||
| "pid": 0, | |||
| "children": [ | |||
| { | |||
| "id": 13, | |||
| "name": "修改密码", | |||
| "path": "system-password", | |||
| "pid": 12, | |||
| "children": [] | |||
| } | |||
| ] | |||
| }, | |||
| { | |||
| "id": 14, | |||
| "name": "审核管理", | |||
| "ename": "audit-manage", | |||
| "path": "/audit-manage", | |||
| "pid": 0, | |||
| "children": [ | |||
| { | |||
| "id": 15, | |||
| "name": "视频审核", | |||
| "path": "apply-course", | |||
| "pid": 14, | |||
| "children": [] | |||
| } | |||
| ] | |||
| } | |||
| ] | |||
| }) | |||
| module.exports = [ | |||
| { | |||
| url: '/vue-admin-template/roles/list', | |||
| type: 'get', | |||
| response: config => { | |||
| const allMenus = data.allMenus | |||
| return { | |||
| code: 20000, | |||
| data: allMenus | |||
| } | |||
| } | |||
| } | |||
| ] | |||
| @@ -0,0 +1,29 @@ | |||
| const Mock = require('mockjs') | |||
| const data = Mock.mock({ | |||
| 'items|30': [{ | |||
| id: '@id', | |||
| title: '@sentence(10, 20)', | |||
| 'status|1': ['published', 'draft', 'deleted'], | |||
| author: 'name', | |||
| display_time: '@datetime', | |||
| pageviews: '@integer(300, 5000)' | |||
| }] | |||
| }) | |||
| module.exports = [ | |||
| { | |||
| url: '/vue-admin-template/table/list', | |||
| type: 'get', | |||
| response: config => { | |||
| const items = data.items | |||
| return { | |||
| code: 20000, | |||
| data: { | |||
| total: items.length, | |||
| items: items | |||
| } | |||
| } | |||
| } | |||
| } | |||
| ] | |||
| @@ -0,0 +1,84 @@ | |||
| const tokens = { | |||
| admin: { | |||
| token: 'admin-token' | |||
| }, | |||
| editor: { | |||
| token: 'editor-token' | |||
| } | |||
| } | |||
| const users = { | |||
| 'admin-token': { | |||
| roles: ['admin'], | |||
| introduction: 'I am a super administrator', | |||
| avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', | |||
| name: 'Super Admin' | |||
| }, | |||
| 'editor-token': { | |||
| roles: ['editor'], | |||
| introduction: 'I am an editor', | |||
| avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', | |||
| name: 'Normal Editor' | |||
| } | |||
| } | |||
| module.exports = [ | |||
| // user login | |||
| { | |||
| url: '/vue-admin-template/user/login', | |||
| type: 'post', | |||
| response: config => { | |||
| const { username } = config.body | |||
| const token = tokens[username] | |||
| // mock error | |||
| if (!token) { | |||
| return { | |||
| code: 60204, | |||
| message: 'Account and password are incorrect.' | |||
| } | |||
| } | |||
| return { | |||
| code: 20000, | |||
| data: token | |||
| } | |||
| } | |||
| }, | |||
| // get user info | |||
| { | |||
| url: '/vue-admin-template/user/info\.*', | |||
| type: 'get', | |||
| response: config => { | |||
| const { token } = config.query | |||
| const info = users[token] | |||
| // mock error | |||
| if (!info) { | |||
| return { | |||
| code: 50008, | |||
| message: 'Login failed, unable to get user details.' | |||
| } | |||
| } | |||
| return { | |||
| code: 20000, | |||
| data: info | |||
| } | |||
| } | |||
| }, | |||
| // user logout | |||
| { | |||
| url: '/vue-admin-template/user/logout', | |||
| type: 'post', | |||
| response: _ => { | |||
| return { | |||
| code: 20000, | |||
| data: 'success' | |||
| } | |||
| } | |||
| } | |||
| ] | |||
| @@ -0,0 +1,25 @@ | |||
| /** | |||
| * @param {string} url | |||
| * @returns {Object} | |||
| */ | |||
| function param2Obj(url) { | |||
| const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') | |||
| if (!search) { | |||
| return {} | |||
| } | |||
| const obj = {} | |||
| const searchArr = search.split('&') | |||
| searchArr.forEach(v => { | |||
| const index = v.indexOf('=') | |||
| if (index !== -1) { | |||
| const name = v.substring(0, index) | |||
| const val = v.substring(index + 1, v.length) | |||
| obj[name] = val | |||
| } | |||
| }) | |||
| return obj | |||
| } | |||
| module.exports = { | |||
| param2Obj | |||
| } | |||
| @@ -0,0 +1,67 @@ | |||
| { | |||
| "name": "vue-admin-template", | |||
| "version": "4.4.0", | |||
| "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint", | |||
| "author": "", | |||
| "scripts": { | |||
| "dev": "vue-cli-service serve", | |||
| "build:prod": "vue-cli-service build", | |||
| "build:stage": "vue-cli-service build --mode staging", | |||
| "preview": "node build/index.js --preview", | |||
| "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", | |||
| "lint": "eslint --ext .js,.vue src", | |||
| "test:unit": "jest --clearCache && vue-cli-service test:unit", | |||
| "test:ci": "npm run lint && npm run test:unit" | |||
| }, | |||
| "dependencies": { | |||
| "array-to-tree": "^3.3.2", | |||
| "axios": "0.18.1", | |||
| "core-js": "3.6.5", | |||
| "element-ui": "2.13.2", | |||
| "js-cookie": "2.2.0", | |||
| "normalize.css": "7.0.0", | |||
| "nprogress": "0.2.0", | |||
| "path-to-regexp": "2.4.0", | |||
| "sortablejs": "^1.12.0", | |||
| "spark-md5": "^3.0.1", | |||
| "vue": "2.6.10", | |||
| "vue-jsonp": "^0.1.8", | |||
| "vue-router": "3.0.6", | |||
| "vue-simple-uploader": "^0.7.4", | |||
| "vuex": "3.1.0" | |||
| }, | |||
| "devDependencies": { | |||
| "@vue/cli-plugin-babel": "4.4.4", | |||
| "@vue/cli-plugin-eslint": "4.4.4", | |||
| "@vue/cli-plugin-unit-jest": "4.4.4", | |||
| "@vue/cli-service": "4.4.4", | |||
| "@vue/test-utils": "1.0.0-beta.29", | |||
| "autoprefixer": "9.5.1", | |||
| "babel-eslint": "10.1.0", | |||
| "babel-jest": "23.6.0", | |||
| "babel-plugin-dynamic-import-node": "2.3.3", | |||
| "chalk": "2.4.2", | |||
| "connect": "3.6.6", | |||
| "eslint": "6.7.2", | |||
| "eslint-plugin-vue": "6.2.2", | |||
| "html-webpack-plugin": "3.2.0", | |||
| "mockjs": "1.0.1-beta3", | |||
| "runjs": "4.3.2", | |||
| "sass": "1.26.8", | |||
| "sass-loader": "8.0.2", | |||
| "script-ext-html-webpack-plugin": "2.1.3", | |||
| "serve-static": "1.13.2", | |||
| "svg-sprite-loader": "4.1.3", | |||
| "svgo": "1.2.2", | |||
| "vue-template-compiler": "2.6.10" | |||
| }, | |||
| "browserslist": [ | |||
| "> 1%", | |||
| "last 2 versions" | |||
| ], | |||
| "engines": { | |||
| "node": ">=8.9", | |||
| "npm": ">= 3.0.0" | |||
| }, | |||
| "license": "MIT" | |||
| } | |||
| @@ -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': {} | |||
| } | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| <!DOCTYPE html> | |||
| <html> | |||
| <head> | |||
| <meta charset="utf-8"> | |||
| <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> | |||
| <!-- <link rel="icon" href="<%= BASE_URL %>favicon.ico"> --> | |||
| <link rel="icon" href="<%= BASE_URL %>logo.png"> | |||
| <title><%= webpackConfig.name %></title> | |||
| </head> | |||
| <body> | |||
| <noscript> | |||
| <strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> | |||
| </noscript> | |||
| <div id="app"></div> | |||
| <!-- built files will be auto injected --> | |||
| </body> | |||
| </html> | |||
| @@ -0,0 +1,11 @@ | |||
| <template> | |||
| <div id="app"> | |||
| <router-view /> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'App' | |||
| } | |||
| </script> | |||
| @@ -0,0 +1,62 @@ | |||
| import request from '@/utils/request' | |||
| /* 获取权限列表 */ | |||
| export function getMenus() { | |||
| return request({ | |||
| // url: '/vue-admin-template/roles/list', | |||
| url: '/video-admin/sys/menu/list', | |||
| method: 'get' | |||
| }) | |||
| } | |||
| /* 获取角色列表 */ | |||
| export function getRoles(params) { | |||
| return request({ | |||
| url: '/video-admin/sys/role/list', | |||
| method: 'get', | |||
| params | |||
| }) | |||
| } | |||
| /* 获取当前账号角色列表 */ | |||
| export function getRolesByUser() { | |||
| return request({ | |||
| url: '/video-admin/sys/role/select', | |||
| method: 'get' | |||
| }) | |||
| } | |||
| /* 获取角色信息 */ | |||
| export function getRoleMsg(roleId) { | |||
| return request({ | |||
| url: `/video-admin/sys/role/info/${roleId}`, | |||
| method: 'get' | |||
| }) | |||
| } | |||
| /* 创建角色 */ | |||
| export function addRoles(data) { | |||
| return request({ | |||
| url: '/video-admin/sys/role/save', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 删除角色 */ | |||
| export function removeRoles(data) { | |||
| return request({ | |||
| url: '/video-admin/sys/role/delete', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 修改角色 */ | |||
| export function updateRoles(data) { | |||
| return request({ | |||
| url: '/video-admin/sys/role/update', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| @@ -0,0 +1,45 @@ | |||
| import request from '@/utils/request' | |||
| /* 获取标签列表 */ | |||
| export function getTags(params) { | |||
| return request({ | |||
| url: '/video-admin/video/videolabel/list', | |||
| method: 'get', | |||
| params | |||
| }) | |||
| } | |||
| /* 获取标签信息 */ | |||
| export function getTagMsg(typeId) { | |||
| return request({ | |||
| url: `/video-admin/video/videolabel/info/${typeId}`, | |||
| method: 'get' | |||
| }) | |||
| } | |||
| /* 创建标签 */ | |||
| export function addTags(data) { | |||
| return request({ | |||
| url: '/video-admin/video/videolabel/save', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 删除标签 */ | |||
| export function removeTags(data) { | |||
| return request({ | |||
| url: '/video-admin/video/videolabel/delete', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 修改标签 */ | |||
| export function updateTags(data) { | |||
| return request({ | |||
| url: '/video-admin/video/videolabel/update', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| @@ -0,0 +1,63 @@ | |||
| import request from '@/utils/request' | |||
| /* 获取专题列表 */ | |||
| export function getTypes(params) { | |||
| return request({ | |||
| url: '/video-admin/video/videosubject/list', | |||
| method: 'get', | |||
| params | |||
| }) | |||
| } | |||
| /* 获取所有专题 */ | |||
| export function getTypesAll(params) { | |||
| return request({ | |||
| url: '/video-admin/video/videosubject/queryAll', | |||
| method: 'get', | |||
| params | |||
| }) | |||
| } | |||
| /* 获取专题信息 */ | |||
| export function getTypeMsg(typeId) { | |||
| return request({ | |||
| url: `/video-admin/video/videosubject/info/${typeId}`, | |||
| method: 'get' | |||
| }) | |||
| } | |||
| /* 创建专题 */ | |||
| export function addTypes(data) { | |||
| return request({ | |||
| url: '/video-admin/video/videosubject/save', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 删除专题 */ | |||
| export function removeTypes(data) { | |||
| return request({ | |||
| url: '/video-admin/video/videosubject/delete', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 修改专题 */ | |||
| export function updateTypes(data) { | |||
| return request({ | |||
| url: '/video-admin/video/videosubject/update', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 专题排序 */ | |||
| export function updateTypesOrder(data) { | |||
| return request({ | |||
| url: '/video-admin/video/videosubject/updateOrder', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| @@ -0,0 +1,88 @@ | |||
| import request from '@/utils/request' | |||
| /* 登录 */ | |||
| export function login(data) { | |||
| return request({ | |||
| url: '/video-admin/sys/login', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 获取登录用户信息 */ | |||
| export function getInfo(token) { | |||
| return request({ | |||
| url: '/video-admin/sys/user/info', | |||
| method: 'get', | |||
| params: { token } | |||
| }) | |||
| } | |||
| /* 退出登录 */ | |||
| export function logout() { | |||
| return request({ | |||
| url: '/vue-admin-template/user/logout', | |||
| method: 'post' | |||
| }) | |||
| } | |||
| /* 获取用户列表 */ | |||
| export function getUsers(params) { | |||
| return request({ | |||
| url: '/video-admin/sys/user/list', | |||
| method: 'get', | |||
| params | |||
| }) | |||
| } | |||
| /* 获取用户信息 */ | |||
| export function getUserMsg(userId) { | |||
| return request({ | |||
| url: `/video-admin/sys/user/info/${userId}`, | |||
| method: 'get' | |||
| }) | |||
| } | |||
| /* 创建用户 */ | |||
| export function addUsers(data) { | |||
| return request({ | |||
| url: '/video-admin/sys/user/save', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 删除用户 */ | |||
| export function removeUsers(data) { | |||
| return request({ | |||
| url: '/video-admin/sys/user/delete', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 修改用户 */ | |||
| export function updateUsers(data) { | |||
| return request({ | |||
| url: '/video-admin/sys/user/update', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 修改密码 */ | |||
| export function updatePassword(data) { | |||
| return request({ | |||
| url: '/video-admin/sys/user/password', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 获取导航菜单列表 */ | |||
| export function getNavMenus() { | |||
| return request({ | |||
| url: '/video-admin/sys/menu/nav', | |||
| method: 'get' | |||
| }) | |||
| } | |||
| @@ -0,0 +1,92 @@ | |||
| // import axios from 'axios' | |||
| import request from '@/utils/request' | |||
| /* 获取视频列表 */ | |||
| export function getVideos(params) { | |||
| return request({ | |||
| url: '/video-admin/video/video/list', | |||
| method: 'get', | |||
| params | |||
| }) | |||
| } | |||
| /* 获取视频信息 */ | |||
| export function getVideoMsg(videoId) { | |||
| return request({ | |||
| url: `/video-admin/video/video/info/${videoId}`, | |||
| method: 'get' | |||
| }) | |||
| } | |||
| /* 创建视频 */ | |||
| export function addVideos(data) { | |||
| return request({ | |||
| url: '/video-admin/video/video/save', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 删除视频 */ | |||
| export function removeVideos(data) { | |||
| return request({ | |||
| url: '/video-admin/video/video/delete', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 修改视频 */ | |||
| export function updateVideos(data) { | |||
| return request({ | |||
| url: '/video-admin/video/video/update', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 视频排序 */ | |||
| export function updateVideosOrder(data) { | |||
| return request({ | |||
| url: '/video-admin/video/video/updateOrder', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 视频发布 */ | |||
| export function videosUp(data) { | |||
| return request({ | |||
| url: '/video-admin/video/video/up', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| /* 视频取消发布 */ | |||
| export function videosDown(data) { | |||
| return request({ | |||
| url: '/video-admin/video/video/down', | |||
| method: 'post', | |||
| data | |||
| }) | |||
| } | |||
| // const service = axios.create({ | |||
| // baseURL: 'qq', | |||
| // timeout: 5000 | |||
| // }) | |||
| /* 获取腾讯视频真实地址 */ | |||
| // export function getVideoUrl(vid) { | |||
| // return service({ | |||
| // url: `/getinfo?vids=${vid}&platform=101001&charge=0&otype=json`, | |||
| // method: 'get' | |||
| // }) | |||
| // } | |||
| // export function getVideoUrl(vid) { | |||
| // return request({ | |||
| // url: `/video-admin/qq/getinfo?vids=${vid}&platform=101001&charge=0&otype=json`, | |||
| // method: 'get' | |||
| // }) | |||
| // } | |||
| @@ -0,0 +1,78 @@ | |||
| <template> | |||
| <el-breadcrumb class="app-breadcrumb" separator="/"> | |||
| <transition-group name="breadcrumb"> | |||
| <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path"> | |||
| <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span> | |||
| <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a> | |||
| </el-breadcrumb-item> | |||
| </transition-group> | |||
| </el-breadcrumb> | |||
| </template> | |||
| <script> | |||
| import pathToRegexp from 'path-to-regexp' | |||
| export default { | |||
| data() { | |||
| return { | |||
| levelList: null | |||
| } | |||
| }, | |||
| watch: { | |||
| $route() { | |||
| this.getBreadcrumb() | |||
| } | |||
| }, | |||
| created() { | |||
| this.getBreadcrumb() | |||
| }, | |||
| methods: { | |||
| getBreadcrumb() { | |||
| // only show routes with meta.title | |||
| let matched = this.$route.matched.filter(item => item.meta && item.meta.title) | |||
| const first = matched[0] | |||
| if (!this.isDashboard(first)) { | |||
| matched = [{ path: '/dashboard', meta: { title: '主页' }}].concat(matched) | |||
| } | |||
| this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false) | |||
| }, | |||
| isDashboard(route) { | |||
| const name = route && route.name | |||
| if (!name) { | |||
| return false | |||
| } | |||
| return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase() | |||
| }, | |||
| pathCompile(path) { | |||
| // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561 | |||
| const { params } = this.$route | |||
| var toPath = pathToRegexp.compile(path) | |||
| return toPath(params) | |||
| }, | |||
| handleLink(item) { | |||
| const { redirect, path } = item | |||
| if (redirect) { | |||
| this.$router.push(redirect) | |||
| return | |||
| } | |||
| this.$router.push(this.pathCompile(path)) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .app-breadcrumb.el-breadcrumb { | |||
| display: inline-block; | |||
| font-size: 14px; | |||
| line-height: 50px; | |||
| margin-left: 8px; | |||
| .no-redirect { | |||
| color: #97a8be; | |||
| cursor: text; | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,44 @@ | |||
| <template> | |||
| <div style="padding: 0 15px;" @click="toggleClick"> | |||
| <svg | |||
| :class="{'is-active':isActive}" | |||
| class="hamburger" | |||
| viewBox="0 0 1024 1024" | |||
| xmlns="http://www.w3.org/2000/svg" | |||
| width="64" | |||
| height="64" | |||
| > | |||
| <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" /> | |||
| </svg> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'Hamburger', | |||
| props: { | |||
| isActive: { | |||
| type: Boolean, | |||
| default: false | |||
| } | |||
| }, | |||
| methods: { | |||
| toggleClick() { | |||
| this.$emit('toggleClick') | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped> | |||
| .hamburger { | |||
| display: inline-block; | |||
| vertical-align: middle; | |||
| width: 20px; | |||
| height: 20px; | |||
| } | |||
| .hamburger.is-active { | |||
| transform: rotate(180deg); | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,62 @@ | |||
| <template> | |||
| <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" /> | |||
| <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners"> | |||
| <use :xlink:href="iconName" /> | |||
| </svg> | |||
| </template> | |||
| <script> | |||
| // doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage | |||
| import { isExternal } from '@/utils/validate' | |||
| export default { | |||
| name: 'SvgIcon', | |||
| props: { | |||
| iconClass: { | |||
| type: String, | |||
| required: true | |||
| }, | |||
| className: { | |||
| type: String, | |||
| default: '' | |||
| } | |||
| }, | |||
| computed: { | |||
| isExternal() { | |||
| return isExternal(this.iconClass) | |||
| }, | |||
| iconName() { | |||
| return `#icon-${this.iconClass}` | |||
| }, | |||
| svgClass() { | |||
| if (this.className) { | |||
| return 'svg-icon ' + this.className | |||
| } else { | |||
| return 'svg-icon' | |||
| } | |||
| }, | |||
| styleExternalIcon() { | |||
| return { | |||
| mask: `url(${this.iconClass}) no-repeat 50% 50%`, | |||
| '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%` | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped> | |||
| .svg-icon { | |||
| width: 1em; | |||
| height: 1em; | |||
| vertical-align: -0.15em; | |||
| fill: currentColor; | |||
| overflow: hidden; | |||
| } | |||
| .svg-external-icon { | |||
| background-color: currentColor; | |||
| mask-size: cover!important; | |||
| display: inline-block; | |||
| } | |||
| </style> | |||
| @@ -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) | |||
| @@ -0,0 +1 @@ | |||
| <svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.068 23.784c-1.02 0-1.877-.32-2.572-.96a8.588 8.588 0 0 1-1.738-2.237 11.524 11.524 0 0 1-1.042-2.621c-.232-.895-.348-1.641-.348-2.238V0h.278c.834 0 1.622.085 2.363.256.742.17 1.645.575 2.711 1.214 1.066.64 2.363 1.535 3.892 2.686 1.53 1.15 3.453 2.664 5.77 4.54 2.502 2.045 4.494 3.771 5.977 5.178 1.483 1.406 2.618 2.6 3.406 3.58.787.98 1.274 1.812 1.46 2.494.185.682.277 1.278.277 1.79v2.046H84.068zM127.3 84.01c.278.682.464 1.535.556 2.558.093 1.023-.37 2.003-1.39 2.94-.463.427-.88.832-1.25 1.215-.372.384-.696.704-.974.96a6.69 6.69 0 0 1-.973.767l-11.816-10.741a44.331 44.331 0 0 0 1.877-1.535 31.028 31.028 0 0 1 1.737-1.406c1.112-.938 2.317-1.343 3.615-1.215 1.297.128 2.363.405 3.197.83.927.427 1.923 1.173 2.989 2.239 1.065 1.065 1.876 2.195 2.432 3.388zM78.23 95.902c2.038 0 3.752-.511 5.143-1.534l-26.969 25.83H18.037c-1.761 0-3.684-.47-5.77-1.407a24.549 24.549 0 0 1-5.838-3.709 21.373 21.373 0 0 1-4.518-5.306c-1.204-2.003-1.807-4.07-1.807-6.202V16.495c0-1.79.44-3.665 1.32-5.626A18.41 18.41 0 0 1 5.04 5.562a21.798 21.798 0 0 1 5.213-3.964C12.198.533 14.237 0 16.37 0h53.24v15.984c0 1.62.278 3.367.834 5.242a16.704 16.704 0 0 0 2.572 5.179c1.159 1.577 2.665 2.898 4.518 3.964 1.853 1.066 4.078 1.598 6.673 1.598h20.295v42.325L85.458 92.45c1.02-1.364 1.529-2.856 1.529-4.476 0-2.216-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1c-2.409 0-4.448.789-6.116 2.366-1.668 1.577-2.502 3.474-2.502 5.69 0 2.217.834 4.092 2.502 5.626 1.668 1.535 3.707 2.302 6.117 2.302h52.13zM26.1 47.951c-2.41 0-4.449.789-6.117 2.366-1.668 1.577-2.502 3.473-2.502 5.69 0 2.216.834 4.092 2.502 5.626 1.668 1.534 3.707 2.302 6.117 2.302h52.13c2.409 0 4.47-.768 6.185-2.302 1.715-1.534 2.572-3.41 2.572-5.626 0-2.217-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1zm52.407 64.063l1.807-1.663 3.476-3.196a479.75 479.75 0 0 0 4.587-4.284 500.757 500.757 0 0 1 5.004-4.667c3.985-3.666 8.48-7.758 13.485-12.276l11.677 10.741-13.485 12.404-5.004 4.603-4.587 4.22a179.46 179.46 0 0 0-3.267 3.068c-.88.853-1.367 1.322-1.46 1.407-.463.341-.973.703-1.529 1.087-.556.383-1.112.703-1.668.959-.556.256-1.413.575-2.572.959a83.5 83.5 0 0 1-3.545 1.087 72.2 72.2 0 0 1-3.475.895c-1.112.256-1.946.426-2.502.511-1.112.17-1.854.043-2.224-.383-.371-.426-.464-1.151-.278-2.174.092-.511.278-1.279.556-2.302.278-1.023.602-2.067.973-3.132l1.042-3.005c.325-.938.58-1.577.765-1.918a10.157 10.157 0 0 1 2.224-2.941z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg> | |||
| @@ -0,0 +1 @@ | |||
| <svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg> | |||
| @@ -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' | |||
| @@ -0,0 +1,40 @@ | |||
| <template> | |||
| <section class="app-main"> | |||
| <transition name="fade-transform" mode="out-in"> | |||
| <router-view :key="key" /> | |||
| </transition> | |||
| </section> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'AppMain', | |||
| computed: { | |||
| key() { | |||
| return this.$route.path | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped> | |||
| .app-main { | |||
| /*50 = navbar */ | |||
| min-height: calc(100vh - 50px); | |||
| width: 100%; | |||
| position: relative; | |||
| overflow: hidden; | |||
| } | |||
| .fixed-header+.app-main { | |||
| padding-top: 50px; | |||
| } | |||
| </style> | |||
| <style lang="scss"> | |||
| // fix css style bug in open el-dialog | |||
| .el-popup-parent--hidden { | |||
| .fixed-header { | |||
| padding-right: 15px; | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,230 @@ | |||
| <template> | |||
| <div class="navbar"> | |||
| <hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" /> | |||
| <breadcrumb class="breadcrumb-container" /> | |||
| <!-- <el-badge :value="200" :max="99" class="item"> | |||
| <i class="el-icon-bell" @click="handleClickBell" /> | |||
| </el-badge> --> | |||
| <div class="right-menu"> | |||
| <el-dropdown class="avatar-container" trigger="click"> | |||
| <div class="avatar-wrapper"> | |||
| <!-- <img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar"> --> | |||
| <!-- <img src="@/assets/logo.png" class="user-avatar"> --> | |||
| <i class="el-icon-user-solid" style="font-size: 40px; margin-right: 5px;" /> | |||
| <span style="font-size: 16px; margin-right: 5px;">{{ name }}</span> | |||
| </div> | |||
| <el-dropdown-menu slot="dropdown" class="user-dropdown"> | |||
| <router-link to="/"> | |||
| <el-dropdown-item> | |||
| 主页 | |||
| </el-dropdown-item> | |||
| </router-link> | |||
| <el-dropdown-item @click.native="handleOpenDialog"> | |||
| 修改密码 | |||
| </el-dropdown-item> | |||
| <el-dropdown-item divided @click.native="logout"> | |||
| <span style="display:block;">退出登录</span> | |||
| </el-dropdown-item> | |||
| </el-dropdown-menu> | |||
| </el-dropdown> | |||
| <public-dialog | |||
| v-if="dialogFormVisible" | |||
| dialog-title="修改密码" | |||
| :dialog-form-visible="dialogFormVisible" | |||
| > | |||
| <template #content> | |||
| <el-form | |||
| ref="ruleForm" | |||
| :model="ruleForm" | |||
| :rules="rules" | |||
| size="small" | |||
| label-width="100px" | |||
| > | |||
| <el-form-item label="密码" prop="password"> | |||
| <el-input v-model="ruleForm.password" placeholder="请输入原密码" /> | |||
| </el-form-item> | |||
| <el-form-item label="确认密码" prop="newPassword"> | |||
| <el-input v-model="ruleForm.newPassword" placeholder="请输入新密码" /> | |||
| </el-form-item> | |||
| </el-form> | |||
| </template> | |||
| <template #footer> | |||
| <el-button @click="handleCloseDialog">取 消</el-button> | |||
| <el-button type="primary" @click="handleSubmitClick">确 定</el-button> | |||
| </template> | |||
| </public-dialog> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { mapGetters, mapActions } from 'vuex' | |||
| import Breadcrumb from '@/components/Breadcrumb' | |||
| import Hamburger from '@/components/Hamburger' | |||
| import PublicDialog from './PublicDialog' | |||
| export default { | |||
| components: { | |||
| Breadcrumb, | |||
| Hamburger, | |||
| PublicDialog | |||
| }, | |||
| data() { | |||
| const checkNewPass = (rule, value, callback) => { | |||
| if (value === '') { | |||
| callback(new Error('请输入新密码')) | |||
| } else { | |||
| callback() | |||
| } | |||
| } | |||
| return { | |||
| dialogFormVisible: false, | |||
| ruleForm: { | |||
| password: '', | |||
| newPassword: '' | |||
| }, | |||
| rules: { | |||
| password: [ | |||
| { message: '请输入原密码', trigger: 'blur', required: true } | |||
| ], | |||
| newPassword: [ | |||
| { validator: checkNewPass, trigger: 'blur', required: true } | |||
| ] | |||
| } | |||
| } | |||
| }, | |||
| computed: { | |||
| ...mapGetters([ | |||
| 'sidebar', | |||
| 'avatar', | |||
| 'name' | |||
| ]) | |||
| }, | |||
| methods: { | |||
| ...mapActions({ | |||
| updatePassword: 'user/updatePassword', | |||
| toggleSideBar: 'app/toggleSideBar' | |||
| }), | |||
| // toggleSideBar() { | |||
| // this.$store.dispatch('app/toggleSideBar') | |||
| // }, | |||
| async logout() { | |||
| await this.$store.dispatch('user/logout') | |||
| this.$router.push(`/login?redirect=${this.$route.fullPath}`) | |||
| }, | |||
| handleClickBell() { | |||
| console.log('bell') | |||
| }, | |||
| handleOpenDialog() { | |||
| this.dialogFormVisible = true | |||
| }, | |||
| handleCloseDialog() { | |||
| this.$nextTick(() => { | |||
| this.$refs['ruleForm'].resetFields() | |||
| this.dialogFormVisible = false | |||
| }) | |||
| }, | |||
| handleSubmitClick() { | |||
| this.$refs['ruleForm'].validate((valid) => { | |||
| this.updatePassword({ ...this.ruleForm }).then(res => { | |||
| this.$message({ | |||
| message: '修改成功!', | |||
| type: 'success' | |||
| }) | |||
| this.handleCloseDialog() | |||
| }) | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .navbar { | |||
| height: 50px; | |||
| overflow: hidden; | |||
| position: relative; | |||
| background: #fff; | |||
| box-shadow: 0 1px 4px rgba(0,21,41,.08); | |||
| .hamburger-container { | |||
| line-height: 46px; | |||
| height: 100%; | |||
| float: left; | |||
| cursor: pointer; | |||
| transition: background .3s; | |||
| -webkit-tap-highlight-color:transparent; | |||
| &:hover { | |||
| background: rgba(0, 0, 0, .025) | |||
| } | |||
| } | |||
| .breadcrumb-container { | |||
| float: left; | |||
| } | |||
| .item { | |||
| position: absolute; | |||
| right: 80px; | |||
| top: 18px; | |||
| font-size: 18px; | |||
| } | |||
| .right-menu { | |||
| float: right; | |||
| height: 100%; | |||
| line-height: 50px; | |||
| &:focus { | |||
| outline: none; | |||
| } | |||
| .right-menu-item { | |||
| display: inline-block; | |||
| padding: 0 8px; | |||
| height: 100%; | |||
| font-size: 18px; | |||
| color: #5a5e66; | |||
| vertical-align: text-bottom; | |||
| &.hover-effect { | |||
| cursor: pointer; | |||
| transition: background .3s; | |||
| &:hover { | |||
| background: rgba(0, 0, 0, .025) | |||
| } | |||
| } | |||
| } | |||
| .avatar-container { | |||
| margin-right: 10px; | |||
| .avatar-wrapper { | |||
| margin-top: 5px; | |||
| position: relative; | |||
| .user-avatar { | |||
| cursor: pointer; | |||
| width: 40px; | |||
| height: 40px; | |||
| border-radius: 50%; | |||
| } | |||
| .el-icon-caret-bottom { | |||
| cursor: pointer; | |||
| position: absolute; | |||
| right: -20px; | |||
| top: 25px; | |||
| font-size: 12px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,51 @@ | |||
| <template> | |||
| <el-dialog | |||
| :title="dialogTitle" | |||
| :visible.sync="dialogFormVisible" | |||
| :show-close="false" | |||
| :append-to-body="true" | |||
| :close-on-press-escape="false" | |||
| :close-on-click-modal="false" | |||
| :destroy-on-close="true" | |||
| :width="`${dialogWidth}px`" | |||
| > | |||
| <el-row :gutter="20"> | |||
| <el-col :span="colWidth" :offset="dialogOffset"> | |||
| <slot name="content" /> | |||
| </el-col> | |||
| </el-row> | |||
| <div slot="footer"> | |||
| <slot name="footer" /> | |||
| </div> | |||
| </el-dialog> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'PublicDialog', | |||
| props: { | |||
| dialogTitle: { | |||
| type: String, | |||
| default: '' | |||
| }, | |||
| dialogFormVisible: { | |||
| type: Boolean, | |||
| default: false | |||
| }, | |||
| dialogWidth: { | |||
| type: Number, | |||
| default: 500 | |||
| }, | |||
| colWidth: { | |||
| type: Number, | |||
| default: 17 | |||
| }, | |||
| dialogOffset: { | |||
| type: Number, | |||
| default: 2 | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style> | |||
| </style> | |||
| @@ -0,0 +1,244 @@ | |||
| <template> | |||
| <div class="app-header"> | |||
| <el-select | |||
| v-if="isShow" | |||
| v-model="typeValue" | |||
| size="small" | |||
| placeholder="视频专题" | |||
| clearable | |||
| class="app-select" | |||
| > | |||
| <el-option | |||
| v-for="item in allTypes" | |||
| :key="item.id" | |||
| :label="item.name" | |||
| :value="item.id" | |||
| /> | |||
| </el-select> | |||
| <el-select | |||
| v-if="isShow" | |||
| v-model="videoStateValue" | |||
| size="small" | |||
| placeholder="发布状态" | |||
| style="width: 100px;" | |||
| class="app-select" | |||
| @change="handleSelectStateClick" | |||
| > | |||
| <el-option value="" label="全部" /> | |||
| <el-option value="0" label="已发布" /> | |||
| <el-option value="1" label="未发布" /> | |||
| </el-select> | |||
| <el-date-picker | |||
| v-if="isShow" | |||
| v-model="dateValue" | |||
| type="datetimerange" | |||
| size="small" | |||
| align="right" | |||
| unlink-panels | |||
| class="app-date" | |||
| range-separator="至" | |||
| start-placeholder="开始日期" | |||
| end-placeholder="结束日期" | |||
| value-format="yyyy-MM-dd HH:mm:ss" | |||
| :picker-options="pickerOptions" | |||
| /> | |||
| <el-input | |||
| v-model="inputText" | |||
| :placeholder="placeholderText" | |||
| class="app-input" | |||
| size="small" | |||
| suffix-icon="el-icon-search" | |||
| /> | |||
| <el-button | |||
| type="primary" | |||
| size="small" | |||
| class="app-btn" | |||
| @click="handleInputClick" | |||
| > | |||
| 查询 | |||
| </el-button> | |||
| <el-button | |||
| v-if="isBtn" | |||
| type="primary" | |||
| size="small" | |||
| class="app-btn" | |||
| @click="handleClickAdd" | |||
| > | |||
| <i v-if="isShow" class="el-icon-upload" /> | |||
| <i v-else class="el-icon-plus" /> | |||
| {{ buttonText }} | |||
| </el-button> | |||
| <el-button | |||
| v-if="isType" | |||
| type="primary" | |||
| size="small" | |||
| class="app-btn" | |||
| @click="handleSearchMediaClick" | |||
| > | |||
| 查看视频 | |||
| </el-button> | |||
| <el-button | |||
| v-if="videoStateValue" | |||
| type="primary" | |||
| size="small" | |||
| class="app-btn" | |||
| @click="handleClosePublicClick" | |||
| > | |||
| {{ videoStateValue !== '1' ? '批量取消发布' : '批量发布' }} | |||
| </el-button> | |||
| <el-button | |||
| v-if="!isHide" | |||
| type="primary" | |||
| size="small" | |||
| class="app-btn" | |||
| style="float: right;" | |||
| @click="handleOrderTopClick" | |||
| > | |||
| 置顶 | |||
| </el-button> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { Debounce } from '@/utils/index' | |||
| import { mapGetters } from 'vuex' | |||
| export default { | |||
| name: 'PublicHeader', | |||
| props: { | |||
| placeholderText: { | |||
| type: String, | |||
| default: '请输入...' | |||
| }, | |||
| buttonText: { | |||
| type: String, | |||
| default: '添加' | |||
| }, | |||
| isShow: { | |||
| type: Boolean, | |||
| default: false | |||
| }, | |||
| isBtn: { | |||
| type: Boolean, | |||
| default: true | |||
| }, | |||
| isType: { | |||
| type: Boolean, | |||
| default: false | |||
| }, | |||
| isHide: { | |||
| type: Boolean, | |||
| default: true | |||
| } | |||
| }, | |||
| data() { | |||
| return { | |||
| inputText: '', | |||
| dateValue: '', | |||
| typeValue: '', | |||
| videoStateValue: '', | |||
| pickerOptions: { | |||
| shortcuts: [ | |||
| { | |||
| text: '最近一周', | |||
| onClick(picker) { | |||
| const end = new Date() | |||
| const start = new Date() | |||
| start.setTime(start.getTime() - 3600 * 1000 * 24 * 7) | |||
| picker.$emit('pick', [start, end]) | |||
| } | |||
| }, | |||
| { | |||
| text: '最近一个月', | |||
| onClick(picker) { | |||
| const end = new Date() | |||
| const start = new Date() | |||
| start.setTime(start.getTime() - 3600 * 1000 * 24 * 30) | |||
| picker.$emit('pick', [start, end]) | |||
| } | |||
| }, | |||
| { | |||
| text: '最近三个月', | |||
| onClick(picker) { | |||
| const end = new Date() | |||
| const start = new Date() | |||
| start.setTime(start.getTime() - 3600 * 1000 * 24 * 90) | |||
| picker.$emit('pick', [start, end]) | |||
| } | |||
| } | |||
| ] | |||
| } | |||
| } | |||
| }, | |||
| computed: { | |||
| ...mapGetters([ | |||
| 'allTypes' | |||
| ]) | |||
| }, | |||
| methods: { | |||
| handleInputClick: Debounce(function() { | |||
| this.$emit('handleClickInput', { | |||
| typeValue: this.typeValue, | |||
| inputText: this.inputText, | |||
| dateValue: this.dateValue, | |||
| videoStateValue: this.videoStateValue | |||
| }) | |||
| }), | |||
| handleClickAdd: Debounce(function() { | |||
| this.$emit('handleClickCreate', true) | |||
| }), | |||
| handleSearchMediaClick: Debounce(function() { | |||
| this.$emit('handleSearchMediaClick') | |||
| }), | |||
| handleOrderTopClick: Debounce(function() { | |||
| this.$emit('handleOrderTopClick') | |||
| }), | |||
| handleSelectStateClick(val) { | |||
| this.$emit('handleQueryVideosByState', val) | |||
| }, | |||
| handleClosePublicClick: Debounce(function() { | |||
| this.$emit('handleClosePublicClick') | |||
| }) | |||
| } | |||
| } | |||
| </script> | |||
| <style> | |||
| .app-header { | |||
| margin-bottom: 5px; | |||
| } | |||
| .app-select { | |||
| vertical-align: middle; | |||
| margin-right: 5px; | |||
| } | |||
| .app-date { | |||
| margin-right: 5px; | |||
| vertical-align: middle; | |||
| } | |||
| .app-input { | |||
| width: 200px; | |||
| margin-right: 5px; | |||
| vertical-align: middle; | |||
| } | |||
| .app-btn { | |||
| vertical-align: middle; | |||
| } | |||
| .el-date-editor--datetimerange.el-input__inner { | |||
| width: 350px; | |||
| } | |||
| .el-range-editor--small .el-range-separator { | |||
| line-height: 28px; | |||
| } | |||
| .el-range-editor--small .el-range__close-icon, .el-range-editor--small .el-range__icon { | |||
| line-height: 27px; | |||
| } | |||
| .el-input--small .el-input__icon { | |||
| line-height: 36px; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,224 @@ | |||
| <template> | |||
| <div class="app-table"> | |||
| <el-table | |||
| :data="tableData" | |||
| element-loading-text="Loading" | |||
| border | |||
| :height="tableHeight" | |||
| highlight-current-row | |||
| @row-click="handleCurrentRowClick" | |||
| @selection-change="handleSelectionChange" | |||
| > | |||
| <el-table-column | |||
| v-if="!isDrop" | |||
| type="index" | |||
| width="60" | |||
| label="#" | |||
| align="center" | |||
| /> | |||
| <el-table-column | |||
| v-if="isVideo" | |||
| type="selection" | |||
| width="60" | |||
| align="center" | |||
| /> | |||
| <template v-for="(item, index) in tableHeader"> | |||
| <el-table-column | |||
| v-if="item.prop === 'imageUrl'" | |||
| :key="index" | |||
| :prop="item.prop" | |||
| :width="item.width" | |||
| :label="item.label" | |||
| align="center" | |||
| > | |||
| <template slot-scope="scope"> | |||
| <img :src="scope.row.imageUrl" style="width: 100px; height: 80px; object-fit:cover;"> | |||
| </template> | |||
| </el-table-column> | |||
| <el-table-column | |||
| v-else-if="item.prop === 'subjectId'" | |||
| :key="index" | |||
| :prop="item.prop" | |||
| :width="item.width" | |||
| :label="item.label" | |||
| :formatter="item.formatter" | |||
| align="center" | |||
| > | |||
| <!-- <template slot-scope="scope"> | |||
| <el-tag | |||
| v-for="(type, i) in scope.row.subjectId" | |||
| :key="`${type}-${i}`" | |||
| :disable-transitions="false" | |||
| style="margin: 0 5px 2px 0;" | |||
| > | |||
| {{ type }} | |||
| </el-tag> | |||
| </template> --> | |||
| </el-table-column> | |||
| <el-table-column | |||
| v-else-if="item.prop === 'label'" | |||
| :key="index" | |||
| :prop="item.prop" | |||
| :width="item.width" | |||
| :label="item.label" | |||
| align="center" | |||
| > | |||
| <template slot-scope="scope"> | |||
| <el-tag | |||
| v-for="(tag, i) in scope.row.label" | |||
| :key="`${tag}-${i}`" | |||
| :disable-transitions="false" | |||
| style="margin: 0 5px 2px 0;" | |||
| > | |||
| {{ tag }} | |||
| </el-tag> | |||
| </template> | |||
| </el-table-column> | |||
| <el-table-column | |||
| v-else | |||
| :key="index" | |||
| :prop="item.prop" | |||
| :width="item.width" | |||
| :label="item.label" | |||
| :formatter="item.formatter" | |||
| :show-overflow-tooltip="true" | |||
| align="center" | |||
| /> | |||
| </template> | |||
| <el-table-column v-if="isShow" label="操作" width="100px" align="center"> | |||
| <template slot-scope="scope"> | |||
| <el-button type="text" size="small" @click="handleClickUpdate(scope.row)">编辑</el-button> | |||
| <el-button type="text" size="small" @click="handleClickRemove(scope.row)">删除</el-button> | |||
| </template> | |||
| </el-table-column> | |||
| </el-table> | |||
| <el-pagination | |||
| :current-page="pageData.currPage" | |||
| :page-sizes="[10, 20, 30, 40, 50]" | |||
| :page-size="pageData.pageSize" | |||
| :total="pageData.totalCount" | |||
| layout="total, sizes, prev, pager, next, jumper" | |||
| class="app-pagination" | |||
| @size-change="handleSizeChange" | |||
| @current-change="handleCurrentChange" | |||
| /> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import Sortable from 'sortablejs' | |||
| export default { | |||
| name: 'PublicTable', | |||
| props: { | |||
| tableHeader: { | |||
| type: Array, | |||
| default: () => [] | |||
| }, | |||
| tableData: { | |||
| type: Array, | |||
| default: () => [] | |||
| }, | |||
| pageData: { | |||
| type: Object, | |||
| default: () => ( | |||
| { | |||
| pageSize: 10, | |||
| currPage: 1, | |||
| totalPage: 1, | |||
| totalCount: 0 | |||
| } | |||
| ) | |||
| }, | |||
| isShow: { | |||
| type: Boolean, | |||
| default: true | |||
| }, | |||
| isDrop: { | |||
| type: Boolean, | |||
| default: false | |||
| }, | |||
| isVideo: { | |||
| type: Boolean, | |||
| default: false | |||
| } | |||
| }, | |||
| data() { | |||
| return { | |||
| tableHeight: window.innerHeight * 0.84 | |||
| } | |||
| }, | |||
| mounted() { | |||
| if (this.isDrop) this.rowDrop() | |||
| document.body.ondrop = function(e) { | |||
| e.preventDefault() | |||
| e.stopPropagation() | |||
| } | |||
| }, | |||
| methods: { | |||
| handleClickUpdate(row) { | |||
| this.$emit('handleUpdateClick', row) | |||
| }, | |||
| handleClickRemove(row) { | |||
| this.$emit('handleRemoveClick', row) | |||
| }, | |||
| handleClickAudit(row) { | |||
| this.$emit('handleAuditClick', row) | |||
| }, | |||
| handleSizeChange(size) { | |||
| this.$emit('handlePageSizeChange', size) | |||
| }, | |||
| handleCurrentChange(currentPage) { | |||
| this.$emit('handlePageChange', currentPage) | |||
| }, | |||
| handleCurrentRowClick(val) { | |||
| this.$emit('handleCurrentRowClick', val['id']) | |||
| }, | |||
| handleSelectionChange(val) { | |||
| this.$emit('handleSelectionChange', val) | |||
| }, | |||
| rowDrop() { | |||
| const tbody = document.querySelector('.app-table .el-table__body-wrapper tbody') | |||
| const _this = this | |||
| Sortable.create(tbody, { | |||
| onEnd(ev) { | |||
| const { newIndex, oldIndex } = ev | |||
| const newData = [..._this.tableData] | |||
| const currRow = newData.splice(oldIndex, 1)[0] | |||
| let nextRow = '' | |||
| if (newIndex !== oldIndex) { | |||
| if (newIndex > oldIndex) { | |||
| nextRow = newData.splice(newIndex - 1, 1)[0] | |||
| } else { | |||
| nextRow = newData.splice(newIndex, 1)[0] | |||
| } | |||
| _this.$emit('handleDropClick', { dragId: currRow['id'], objectId: nextRow['id'] }) | |||
| } | |||
| } | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scope> | |||
| .app-table { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| .app-pagination { | |||
| margin-top: 10px; | |||
| } | |||
| /* .el-table__body { | |||
| height: 100%; | |||
| } */ | |||
| </style> | |||
| @@ -0,0 +1,144 @@ | |||
| <template> | |||
| <div | |||
| class="grid-content bg-purple" | |||
| @mouseenter="handleMouseover" | |||
| @mouseleave="handleMouseout" | |||
| > | |||
| <div class="video-contanier"> | |||
| <video | |||
| ref="video" | |||
| :src="videoSrc" | |||
| class="video" | |||
| @pause="videoPause" | |||
| @play="videoPlay" | |||
| @click="handleVideoClick" | |||
| /> | |||
| <div v-show="isPlay" class="play"> | |||
| <i class="el-icon-video-play" @click="handleVideoClick" /> | |||
| </div> | |||
| </div> | |||
| <div class="footer"> | |||
| <span class="title">{{ mediaData.title }}</span> | |||
| {{ mediaData.createTime }} 更新 | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'PublicVideo', | |||
| props: { | |||
| mediaData: { | |||
| type: Object, | |||
| default: () => ({}) | |||
| } | |||
| }, | |||
| data() { | |||
| return { | |||
| isPlay: false, | |||
| playState: false, | |||
| videoSrc: '' | |||
| } | |||
| }, | |||
| mounted() { | |||
| const url = this.mediaData.url | |||
| const start = url.lastIndexOf('/') + 1 | |||
| const end = url.lastIndexOf('.') | |||
| const vid = url.substring(start, end) | |||
| const self = this | |||
| this.$jsonp(`https://vv.video.qq.com/getinfo?vids=${vid}&platform=101001&charge=0&otype=json`).then(res => { | |||
| const ul = res.vl.vi | |||
| if (ul.length > 0) { | |||
| const fn = ul[0].fn | |||
| const vkey = ul[0].fvkey | |||
| const uis = ul[0].ul.ui[0] | |||
| const url = uis['url'].replace(/http:|https:/, '') | |||
| self.videoSrc = `${url}${fn}?vkey=${vkey}` | |||
| } | |||
| }).catch(err => { | |||
| console.log(err) | |||
| }) | |||
| // this.$store.dispatch('videos/getUrl', vid).then(res => { | |||
| // const ul = res.vl.vi | |||
| // if (ul.length > 0) { | |||
| // const fn = ul[0].fn | |||
| // const vkey = ul[0].fvkey | |||
| // const uis = ul[0].ul.ui[0] | |||
| // self.videoSrc = `${uis['url']}${fn}?vkey=${vkey}` | |||
| // } | |||
| // }).catch(err => { | |||
| // console.log(err) | |||
| // }) | |||
| }, | |||
| methods: { | |||
| // 根据播放状态判断是否显示播放按钮 | |||
| handleMouseover() { | |||
| if (!this.playState) this.isPlay = true | |||
| }, | |||
| handleMouseout() { | |||
| this.isPlay = false | |||
| }, | |||
| // 播放 | |||
| handleVideoClick() { | |||
| this.$emit('handleVideoClick', { id: this.mediaData.id, video: this.$refs['video'], playState: this.playState }) | |||
| }, | |||
| // 播放事件 | |||
| videoPlay() { | |||
| this.isPlay = false | |||
| this.playState = true | |||
| }, | |||
| // 暂停事件 | |||
| videoPause() { | |||
| this.isPlay = true | |||
| this.playState = false | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .grid-content { | |||
| border-radius: 4px; | |||
| min-height: 36px; | |||
| height: 100%; | |||
| // position: relative; | |||
| } | |||
| .video-contanier { | |||
| position: relative; | |||
| height: 100%; | |||
| width: 100%; | |||
| } | |||
| .video { | |||
| background-color: black; | |||
| width: 100%; | |||
| border-radius: 5px; | |||
| } | |||
| .play { | |||
| width: 100%; | |||
| height: calc(100% - 4px); | |||
| position: absolute; | |||
| top: 0; | |||
| border-radius: 5px; | |||
| background: #808080; | |||
| opacity: 0.5; | |||
| font-size: 56px; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| color: #fff; | |||
| :hover { | |||
| cursor: pointer; | |||
| } | |||
| } | |||
| .footer { | |||
| // white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| padding: 10px 2px; | |||
| font-size: 14px; | |||
| color: #545a5f; | |||
| } | |||
| .title { | |||
| display: block; | |||
| font-size: 15px; | |||
| margin-bottom: 4px; | |||
| } | |||
| </style> | |||
| @@ -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) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,41 @@ | |||
| <script> | |||
| export default { | |||
| name: 'MenuItem', | |||
| functional: true, | |||
| props: { | |||
| icon: { | |||
| type: String, | |||
| default: '' | |||
| }, | |||
| title: { | |||
| type: String, | |||
| default: '' | |||
| } | |||
| }, | |||
| render(h, context) { | |||
| const { icon, title } = context.props | |||
| const vnodes = [] | |||
| if (icon) { | |||
| if (icon.includes('el-icon')) { | |||
| vnodes.push(<i class={[icon, 'sub-el-icon']} />) | |||
| } else { | |||
| vnodes.push(<svg-icon icon-class={icon}/>) | |||
| } | |||
| } | |||
| if (title) { | |||
| vnodes.push(<span slot='title'>{(title)}</span>) | |||
| } | |||
| return vnodes | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped> | |||
| .sub-el-icon { | |||
| color: currentColor; | |||
| width: 1em; | |||
| height: 1em; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,43 @@ | |||
| <template> | |||
| <component :is="type" v-bind="linkProps(to)"> | |||
| <slot /> | |||
| </component> | |||
| </template> | |||
| <script> | |||
| import { isExternal } from '@/utils/validate' | |||
| export default { | |||
| props: { | |||
| to: { | |||
| type: String, | |||
| required: true | |||
| } | |||
| }, | |||
| computed: { | |||
| isExternal() { | |||
| return isExternal(this.to) | |||
| }, | |||
| type() { | |||
| if (this.isExternal) { | |||
| return 'a' | |||
| } | |||
| return 'router-link' | |||
| } | |||
| }, | |||
| methods: { | |||
| linkProps(to) { | |||
| if (this.isExternal) { | |||
| return { | |||
| href: to, | |||
| target: '_blank', | |||
| rel: 'noopener' | |||
| } | |||
| } | |||
| return { | |||
| to: to | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| @@ -0,0 +1,83 @@ | |||
| <template> | |||
| <div class="sidebar-logo-container" :class="{'collapse':collapse}"> | |||
| <transition name="sidebarLogoFade"> | |||
| <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/"> | |||
| <img v-if="logo" src="@/assets/logo.png" class="sidebar-logo"> | |||
| <h1 v-else class="sidebar-title">{{ title }} </h1> | |||
| </router-link> | |||
| <router-link v-else key="expand" class="sidebar-logo-link" to="/"> | |||
| <img v-if="logo" src="@/assets/logo.png" class="sidebar-logo"> | |||
| <h1 class="sidebar-title">{{ title }} </h1> | |||
| </router-link> | |||
| </transition> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'SidebarLogo', | |||
| props: { | |||
| collapse: { | |||
| type: Boolean, | |||
| required: true | |||
| } | |||
| }, | |||
| data() { | |||
| return { | |||
| title: '视频管理平台', | |||
| logo: '@/assets/logo.png' | |||
| // logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png' | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .sidebarLogoFade-enter-active { | |||
| transition: opacity 1.5s; | |||
| } | |||
| .sidebarLogoFade-enter, | |||
| .sidebarLogoFade-leave-to { | |||
| opacity: 0; | |||
| } | |||
| .sidebar-logo-container { | |||
| position: relative; | |||
| width: 100%; | |||
| height: 50px; | |||
| line-height: 50px; | |||
| background: #2b2f3a; | |||
| text-align: center; | |||
| overflow: hidden; | |||
| & .sidebar-logo-link { | |||
| height: 100%; | |||
| width: 100%; | |||
| & .sidebar-logo { | |||
| width: 32px; | |||
| height: 32px; | |||
| vertical-align: middle; | |||
| margin-right: 12px; | |||
| } | |||
| & .sidebar-title { | |||
| display: inline-block; | |||
| margin: 0; | |||
| color: #fff; | |||
| font-weight: 600; | |||
| line-height: 50px; | |||
| font-size: 14px; | |||
| font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif; | |||
| vertical-align: middle; | |||
| } | |||
| } | |||
| &.collapse { | |||
| .sidebar-logo { | |||
| margin-right: 0px; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,95 @@ | |||
| <template> | |||
| <div v-if="!item.hidden"> | |||
| <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow"> | |||
| <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)"> | |||
| <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}"> | |||
| <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" /> | |||
| </el-menu-item> | |||
| </app-link> | |||
| </template> | |||
| <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body> | |||
| <template slot="title"> | |||
| <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" /> | |||
| </template> | |||
| <sidebar-item | |||
| v-for="child in item.children" | |||
| :key="child.path" | |||
| :is-nest="true" | |||
| :item="child" | |||
| :base-path="resolvePath(child.path)" | |||
| class="nest-menu" | |||
| /> | |||
| </el-submenu> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import path from 'path' | |||
| import { isExternal } from '@/utils/validate' | |||
| import Item from './Item' | |||
| import AppLink from './Link' | |||
| import FixiOSBug from './FixiOSBug' | |||
| export default { | |||
| name: 'SidebarItem', | |||
| components: { Item, AppLink }, | |||
| mixins: [FixiOSBug], | |||
| props: { | |||
| // route object | |||
| item: { | |||
| type: Object, | |||
| required: true | |||
| }, | |||
| isNest: { | |||
| type: Boolean, | |||
| default: false | |||
| }, | |||
| basePath: { | |||
| type: String, | |||
| default: '' | |||
| } | |||
| }, | |||
| data() { | |||
| // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237 | |||
| // TODO: refactor with render function | |||
| this.onlyOneChild = null | |||
| return {} | |||
| }, | |||
| methods: { | |||
| hasOneShowingChild(children = [], parent) { | |||
| const showingChildren = children.filter(item => { | |||
| if (item.hidden) { | |||
| return false | |||
| } else { | |||
| // Temp set(will be used if only has one showing child) | |||
| this.onlyOneChild = item | |||
| return true | |||
| } | |||
| }) | |||
| // When there is only one child router, the child router is displayed by default | |||
| if (showingChildren.length === 1) { | |||
| return true | |||
| } | |||
| // Show parent if there are no child router to display | |||
| if (showingChildren.length === 0) { | |||
| this.onlyOneChild = { ... parent, path: '', noShowingChildren: true } | |||
| return true | |||
| } | |||
| return false | |||
| }, | |||
| resolvePath(routePath) { | |||
| if (isExternal(routePath)) { | |||
| return routePath | |||
| } | |||
| if (isExternal(this.basePath)) { | |||
| return this.basePath | |||
| } | |||
| return path.resolve(this.basePath, routePath) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| @@ -0,0 +1,56 @@ | |||
| <template> | |||
| <div :class="{'has-logo':showLogo}"> | |||
| <logo v-if="showLogo" :collapse="isCollapse" /> | |||
| <el-scrollbar wrap-class="scrollbar-wrapper"> | |||
| <el-menu | |||
| :default-active="activeMenu" | |||
| :collapse="isCollapse" | |||
| :background-color="variables.menuBg" | |||
| :text-color="variables.menuText" | |||
| :unique-opened="false" | |||
| :active-text-color="variables.menuActiveText" | |||
| :collapse-transition="false" | |||
| mode="vertical" | |||
| > | |||
| <sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" /> | |||
| </el-menu> | |||
| </el-scrollbar> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { mapGetters } from 'vuex' | |||
| import Logo from './Logo' | |||
| import SidebarItem from './SidebarItem' | |||
| import variables from '@/styles/variables.scss' | |||
| export default { | |||
| components: { SidebarItem, Logo }, | |||
| computed: { | |||
| ...mapGetters([ | |||
| 'sidebar' | |||
| ]), | |||
| routes() { | |||
| return this.$router.options.routes | |||
| }, | |||
| activeMenu() { | |||
| const route = this.$route | |||
| const { meta, path } = route | |||
| // if set path, the sidebar will highlight the path you set | |||
| if (meta.activeMenu) { | |||
| return meta.activeMenu | |||
| } | |||
| return path | |||
| }, | |||
| showLogo() { | |||
| return this.$store.state.settings.sidebarLogo | |||
| }, | |||
| variables() { | |||
| return variables | |||
| }, | |||
| isCollapse() { | |||
| return !this.sidebar.opened | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| @@ -0,0 +1,3 @@ | |||
| export { default as Navbar } from './Navbar' | |||
| export { default as Sidebar } from './Sidebar' | |||
| export { default as AppMain } from './AppMain' | |||
| @@ -0,0 +1,93 @@ | |||
| <template> | |||
| <div :class="classObj" class="app-wrapper"> | |||
| <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" /> | |||
| <sidebar class="sidebar-container" /> | |||
| <div class="main-container"> | |||
| <div :class="{'fixed-header':fixedHeader}"> | |||
| <navbar /> | |||
| </div> | |||
| <app-main /> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { Navbar, Sidebar, AppMain } from './components' | |||
| import ResizeMixin from './mixin/ResizeHandler' | |||
| export default { | |||
| name: 'Layout', | |||
| components: { | |||
| Navbar, | |||
| Sidebar, | |||
| AppMain | |||
| }, | |||
| mixins: [ResizeMixin], | |||
| computed: { | |||
| sidebar() { | |||
| return this.$store.state.app.sidebar | |||
| }, | |||
| device() { | |||
| return this.$store.state.app.device | |||
| }, | |||
| fixedHeader() { | |||
| return this.$store.state.settings.fixedHeader | |||
| }, | |||
| classObj() { | |||
| return { | |||
| hideSidebar: !this.sidebar.opened, | |||
| openSidebar: this.sidebar.opened, | |||
| withoutAnimation: this.sidebar.withoutAnimation, | |||
| mobile: this.device === 'mobile' | |||
| } | |||
| } | |||
| }, | |||
| methods: { | |||
| handleClickOutside() { | |||
| this.$store.dispatch('app/closeSideBar', { withoutAnimation: false }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| @import "~@/styles/mixin.scss"; | |||
| @import "~@/styles/variables.scss"; | |||
| .app-wrapper { | |||
| @include clearfix; | |||
| position: relative; | |||
| height: 100%; | |||
| width: 100%; | |||
| &.mobile.openSidebar{ | |||
| position: fixed; | |||
| top: 0; | |||
| } | |||
| } | |||
| .drawer-bg { | |||
| background: #000; | |||
| opacity: 0.3; | |||
| width: 100%; | |||
| top: 0; | |||
| height: 100%; | |||
| position: absolute; | |||
| z-index: 999; | |||
| } | |||
| .fixed-header { | |||
| position: fixed; | |||
| top: 0; | |||
| right: 0; | |||
| z-index: 9; | |||
| width: calc(100% - #{$sideBarWidth}); | |||
| transition: width 0.28s; | |||
| } | |||
| .hideSidebar .fixed-header { | |||
| width: calc(100% - 54px) | |||
| } | |||
| .mobile .fixed-header { | |||
| width: 100%; | |||
| } | |||
| </style> | |||
| @@ -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 }) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,64 @@ | |||
| import Vue from 'vue' | |||
| import uploader from 'vue-simple-uploader' | |||
| import VueJsonp from 'vue-jsonp' | |||
| 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 | |||
| /** | |||
| * If you don't want to use mock-server | |||
| * you want to use MockJs for mock api | |||
| * you can execute: mockXHR() | |||
| * | |||
| * Currently MockJs will be used in the production environment, | |||
| * please remove it before going online ! ! ! | |||
| */ | |||
| // if (process.env.NODE_ENV === 'production') { | |||
| // const { mockXHR } = require('../mock') | |||
| // mockXHR() | |||
| // } | |||
| // set ElementUI lang to EN | |||
| // Vue.use(ElementUI, { locale }) | |||
| Vue.use(uploader) | |||
| Vue.use(VueJsonp) | |||
| // 如果想要中文版 element-ui,按如下方式声明 | |||
| Vue.use(ElementUI) | |||
| Vue.config.productionTip = false | |||
| // const on = Vue.prototype.$on | |||
| // 节流 | |||
| // Vue.prototype.$on = function (event, func) { | |||
| // let previous = 0 | |||
| // const newFunc = func | |||
| // if (event === 'click') { | |||
| // newFunc = function () { | |||
| // const now = new Date().getTime() | |||
| // if (previous + 1000 <= now) { | |||
| // func.apply(this, arguments) | |||
| // previous = now | |||
| // } | |||
| // } | |||
| // } | |||
| // on.call(this, event, newFunc) | |||
| // } | |||
| new Vue({ | |||
| el: '#app', | |||
| router, | |||
| store, | |||
| render: h => h(App) | |||
| }) | |||
| @@ -0,0 +1,64 @@ | |||
| import router from './router' | |||
| import store from './store' | |||
| import { Message } from 'element-ui' | |||
| import NProgress from 'nprogress' // progress bar | |||
| import 'nprogress/nprogress.css' // progress bar style | |||
| import { getToken } from '@/utils/auth' // get token from cookie | |||
| import getPageTitle from '@/utils/get-page-title' | |||
| NProgress.configure({ showSpinner: false }) // NProgress Configuration | |||
| const whiteList = ['/login'] // no redirect whitelist | |||
| router.beforeEach(async(to, from, next) => { | |||
| // start progress bar | |||
| NProgress.start() | |||
| // set page title | |||
| document.title = getPageTitle(to.meta.title) | |||
| // determine whether the user has logged in | |||
| const hasToken = getToken() | |||
| if (hasToken) { | |||
| if (to.path === '/login') { | |||
| // if is logged in, redirect to the home page | |||
| next({ path: '/' }) | |||
| NProgress.done() | |||
| } else { | |||
| const hasGetUserInfo = store.getters.name | |||
| if (hasGetUserInfo) { | |||
| next() | |||
| } else { | |||
| try { | |||
| // get user info | |||
| await store.dispatch('user/getInfo') | |||
| next() | |||
| } catch (error) { | |||
| // remove token and go to login page to re-login | |||
| await store.dispatch('user/resetToken') | |||
| Message.error(error || 'Has Error') | |||
| next(`/login?redirect=${to.path}`) | |||
| NProgress.done() | |||
| } | |||
| } | |||
| } | |||
| } else { | |||
| /* has no token*/ | |||
| if (whiteList.indexOf(to.path) !== -1) { | |||
| // in the free login whitelist, go directly | |||
| next() | |||
| } else { | |||
| // other pages that do not have permission to access are redirected to the login page. | |||
| next(`/login?redirect=${to.path}`) | |||
| NProgress.done() | |||
| } | |||
| } | |||
| }) | |||
| router.afterEach(() => { | |||
| // finish progress bar | |||
| NProgress.done() | |||
| }) | |||
| @@ -0,0 +1,147 @@ | |||
| import Vue from 'vue' | |||
| import Router from 'vue-router' | |||
| Vue.use(Router) | |||
| /* Layout */ | |||
| import Layout from '@/layout' | |||
| /** | |||
| * Note: sub-menu only appear when route children.length >= 1 | |||
| * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html | |||
| * | |||
| * hidden: true if set true, item will not show in the sidebar(default is false) | |||
| * alwaysShow: true if set true, will always show the root menu | |||
| * if not set alwaysShow, when item has more than one children route, | |||
| * it will becomes nested mode, otherwise not show the root menu | |||
| * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb | |||
| * name:'router-name' the name is used by <keep-alive> (must set!!!) | |||
| * meta : { | |||
| roles: ['admin','editor'] control the page roles (you can set multiple roles) | |||
| title: 'title' the name show in sidebar and breadcrumb (recommend set) | |||
| icon: 'svg-name'/'el-icon-x' the icon show in the sidebar | |||
| breadcrumb: false if set false, the item will hidden in breadcrumb(default is true) | |||
| activeMenu: '/example/list' if set path, the sidebar will highlight the path you set | |||
| } | |||
| */ | |||
| /** | |||
| * constantRoutes | |||
| * a base page that does not have permission requirements | |||
| * all roles can be accessed | |||
| */ | |||
| export const constantRoutes = [ | |||
| { | |||
| path: '/login', | |||
| component: () => import('@/views/login/index'), | |||
| hidden: true | |||
| }, | |||
| { | |||
| path: '/404', | |||
| component: () => import('@/views/404'), | |||
| hidden: true | |||
| }, | |||
| { | |||
| path: '/', | |||
| component: Layout, | |||
| redirect: '/dashboard', | |||
| children: [{ | |||
| path: 'dashboard', | |||
| name: 'Dashboard', | |||
| component: () => import('@/views/dashboard/index'), | |||
| meta: { title: '主页', icon: 'el-icon-s-home' } | |||
| }] | |||
| }, | |||
| { | |||
| path: '/media', | |||
| component: Layout, | |||
| children: [ | |||
| { | |||
| path: 'index', | |||
| name: 'Media', | |||
| component: () => import('@/views/mediaManager/index'), | |||
| meta: { title: '视频管理', icon: 'el-icon-video-camera' } | |||
| } | |||
| ] | |||
| }, | |||
| { | |||
| path: '/media-type', | |||
| component: Layout, | |||
| children: [ | |||
| { | |||
| path: 'index', | |||
| name: 'MediaType', | |||
| component: () => import('@/views/mediaTypeManager/index'), | |||
| meta: { title: '专题管理', icon: 'el-icon-s-grid' } | |||
| } | |||
| ] | |||
| }, | |||
| { | |||
| path: '/tags', | |||
| component: Layout, | |||
| children: [ | |||
| { | |||
| path: 'index', | |||
| name: 'Tags', | |||
| component: () => import('@/views/tagsManager/index'), | |||
| meta: { title: '标签管理', icon: 'el-icon-s-grid' } | |||
| } | |||
| ] | |||
| }, | |||
| // { | |||
| // path: '/department', | |||
| // component: Layout, | |||
| // children: [ | |||
| // { | |||
| // path: 'index', | |||
| // name: 'Department', | |||
| // component: () => import('@/views/departmentManager/index'), | |||
| // meta: { title: '部门管理', icon: 'el-icon-suitcase' } | |||
| // } | |||
| // ] | |||
| // }, | |||
| { | |||
| path: '/user', | |||
| component: Layout, | |||
| children: [ | |||
| { | |||
| path: 'index', | |||
| name: 'User', | |||
| component: () => import('@/views/userManager/index'), | |||
| meta: { title: '用户管理', icon: 'el-icon-user' } | |||
| } | |||
| ] | |||
| }, | |||
| { | |||
| path: '/roles', | |||
| component: Layout, | |||
| children: [ | |||
| { | |||
| path: 'index', | |||
| name: 'Roles', | |||
| component: () => import('@/views/rolesManager/index'), | |||
| meta: { title: '角色管理', icon: 'el-icon-monitor' } | |||
| } | |||
| ] | |||
| }, | |||
| // 404 page must be placed at the end !!! | |||
| { path: '*', redirect: '/404', hidden: true } | |||
| ] | |||
| const createRouter = () => new Router({ | |||
| // mode: 'history', // require service support | |||
| scrollBehavior: () => ({ y: 0 }), | |||
| routes: constantRoutes | |||
| }) | |||
| const router = createRouter() | |||
| // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 | |||
| export function resetRouter() { | |||
| const newRouter = createRouter() | |||
| router.matcher = newRouter.matcher // reset router | |||
| } | |||
| export default router | |||
| @@ -0,0 +1,16 @@ | |||
| module.exports = { | |||
| title: '视频管理平台', | |||
| /** | |||
| * @type {boolean} true | false | |||
| * @description Whether fix the header | |||
| */ | |||
| fixedHeader: true, | |||
| /** | |||
| * @type {boolean} true | false | |||
| * @description Whether show the logo in sidebar | |||
| */ | |||
| sidebarLogo: true | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| const getters = { | |||
| sidebar: state => state.app.sidebar, | |||
| device: state => state.app.device, | |||
| token: state => state.user.token, | |||
| avatar: state => state.user.avatar, | |||
| name: state => state.user.name, | |||
| userInfo: state => state.user.userInfo, | |||
| userData: state => state.user.userData, | |||
| userMsg: state => state.user.userMsg, | |||
| navMenuData: state => state.user.navMenuData, | |||
| menuData: state => state.roles.menuData, | |||
| roleData: state => state.roles.roleData, | |||
| userRoleData: state => state.roles.userRoleData, | |||
| roleMsg: state => state.roles.roleMsg, | |||
| typeData: state => state.types.typeData, | |||
| allTypes: state => state.types.allTypes, | |||
| typeMsg: state => state.types.typeMsg, | |||
| videoData: state => state.videos.videoData, | |||
| videoMsg: state => state.videos.videoMsg, | |||
| tagData: state => state.tags.tagData, | |||
| tagMsg: state => state.tags.tagMsg | |||
| } | |||
| export default getters | |||
| @@ -0,0 +1,28 @@ | |||
| import Vue from 'vue' | |||
| import Vuex from 'vuex' | |||
| import getters from './getters' | |||
| import app from './modules/app' | |||
| import settings from './modules/settings' | |||
| import user from './modules/user' | |||
| import roles from './modules/roles' | |||
| import types from './modules/types' | |||
| import tags from './modules/tags' | |||
| import videos from './modules/videos' | |||
| Vue.use(Vuex) | |||
| const store = new Vuex.Store({ | |||
| strict: true, | |||
| modules: { | |||
| app, | |||
| settings, | |||
| user, | |||
| roles, | |||
| types, | |||
| tags, | |||
| videos | |||
| }, | |||
| getters | |||
| }) | |||
| export default store | |||
| @@ -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 | |||
| } | |||
| @@ -0,0 +1,111 @@ | |||
| import arrayToTree from 'array-to-tree' | |||
| import { getMenus, getRoles, getRolesByUser, getRoleMsg, addRoles, removeRoles, updateRoles } from '@/api/roles' | |||
| const state = { | |||
| menuData: [], | |||
| roleData: '', | |||
| userRoleData: '', | |||
| roleMsg: '' | |||
| } | |||
| const mutations = { | |||
| SET_MENU_DATA: (state, payload) => { | |||
| state.menuData = payload | |||
| }, | |||
| SET_ROLE_DATA: (state, payload) => { | |||
| state.roleData = payload | |||
| }, | |||
| SET_USER_ROLE_DATA: (state, payload) => { | |||
| state.userRoleData = payload | |||
| }, | |||
| SET_ROLE_MSG: (state, payload) => { | |||
| state.roleMsg = payload | |||
| } | |||
| } | |||
| const actions = { | |||
| fetchMenuList({ commit }) { | |||
| return new Promise((resolve, reject) => { | |||
| getMenus().then((res) => { | |||
| const menu = arrayToTree(res, { | |||
| parentProperty: 'parentId', | |||
| customID: 'menuId' | |||
| }) | |||
| menu[0]['children'] = menu[0]['children'].filter( | |||
| item => item.menuId === 2 || item.menuId === 3 || item.menuId === 31 || item.menuId === 36 || item.menuId === 41 | |||
| ) | |||
| commit('SET_MENU_DATA', menu) | |||
| resolve() | |||
| }).catch((err) => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| fetchRoleList({ commit }, option) { | |||
| return new Promise((resolve, reject) => { | |||
| getRoles(option).then((res) => { | |||
| const { page } = res | |||
| commit('SET_ROLE_DATA', page) | |||
| resolve() | |||
| }).catch((err) => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| fetchRoleListByUserId({ commit }) { | |||
| return new Promise((resolve, reject) => { | |||
| getRolesByUser().then((res) => { | |||
| const { list } = res | |||
| commit('SET_USER_ROLE_DATA', list) | |||
| resolve() | |||
| }).catch((err) => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| fetchRoleMsg({ commit }, roleId) { | |||
| return new Promise((resolve, reject) => { | |||
| getRoleMsg(roleId).then((res) => { | |||
| const { role } = res | |||
| commit('SET_ROLE_MSG', role) | |||
| resolve() | |||
| }).catch((err) => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| saveRoles({ commit }, roleMsg) { | |||
| return new Promise((resolve, reject) => { | |||
| addRoles(roleMsg).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| removeRoles({ commit }, rolesId) { | |||
| return new Promise((resolve, reject) => { | |||
| removeRoles(rolesId).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| updateRoles({ commit }, roleMsg) { | |||
| return new Promise((resolve, reject) => { | |||
| updateRoles(roleMsg).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| } | |||
| } | |||
| export default { | |||
| namespaced: true, | |||
| state, | |||
| mutations, | |||
| actions | |||
| } | |||
| @@ -0,0 +1,32 @@ | |||
| import defaultSettings from '@/settings' | |||
| const { showSettings, fixedHeader, sidebarLogo } = defaultSettings | |||
| const state = { | |||
| showSettings: showSettings, | |||
| fixedHeader: fixedHeader, | |||
| sidebarLogo: sidebarLogo | |||
| } | |||
| const mutations = { | |||
| CHANGE_SETTING: (state, { key, value }) => { | |||
| // eslint-disable-next-line no-prototype-builtins | |||
| if (state.hasOwnProperty(key)) { | |||
| state[key] = value | |||
| } | |||
| } | |||
| } | |||
| const actions = { | |||
| changeSetting({ commit }, data) { | |||
| commit('CHANGE_SETTING', data) | |||
| } | |||
| } | |||
| export default { | |||
| namespaced: true, | |||
| state, | |||
| mutations, | |||
| actions | |||
| } | |||
| @@ -0,0 +1,74 @@ | |||
| import { getTags, getTagMsg, addTags, removeTags, updateTags } from '@/api/tags' | |||
| const state = { | |||
| tagData: '', | |||
| tagMsg: '' | |||
| } | |||
| const mutations = { | |||
| SET_TAG_DATA: (state, payload) => { | |||
| state.tagData = payload | |||
| }, | |||
| SET_TAG_MSG: (state, payload) => { | |||
| state.tagMsg = payload | |||
| } | |||
| } | |||
| const actions = { | |||
| fetchTagList({ commit }, option) { | |||
| return new Promise((resolve, reject) => { | |||
| getTags(option).then((res) => { | |||
| const { page } = res | |||
| commit('SET_TAG_DATA', page) | |||
| resolve(page) | |||
| }).catch((err) => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| fetchTagMsg({ commit }, tagId) { | |||
| return new Promise((resolve, reject) => { | |||
| getTagMsg(tagId).then((res) => { | |||
| const { videoLabel } = res | |||
| commit('SET_TAG_MSG', videoLabel) | |||
| resolve() | |||
| }).catch((err) => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| saveTags({ commit }, tagMsg) { | |||
| return new Promise((resolve, reject) => { | |||
| addTags(tagMsg).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| removeTags({ commit }, tagsId) { | |||
| return new Promise((resolve, reject) => { | |||
| removeTags(tagsId).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| updateTags({ commit }, tagMsg) { | |||
| return new Promise((resolve, reject) => { | |||
| updateTags(tagMsg).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| } | |||
| } | |||
| export default { | |||
| namespaced: true, | |||
| state, | |||
| mutations, | |||
| actions | |||
| } | |||
| @@ -0,0 +1,101 @@ | |||
| import { getTypes, getTypesAll, getTypeMsg, addTypes, removeTypes, updateTypes, updateTypesOrder } from '@/api/types' | |||
| const state = { | |||
| typeData: '', | |||
| allTypes: '', | |||
| typeMsg: '' | |||
| } | |||
| const mutations = { | |||
| SET_TYPE_DATA: (state, payload) => { | |||
| state.typeData = payload | |||
| }, | |||
| SET_ALL_TYPES: (state, payload) => { | |||
| state.allTypes = payload | |||
| }, | |||
| SET_TYPE_MSG: (state, payload) => { | |||
| state.typeMsg = payload | |||
| } | |||
| } | |||
| const actions = { | |||
| clearTypeList({ commit }) { | |||
| commit('SET_TYPE_DATA', '') | |||
| }, | |||
| fetchTypeList({ commit }, option) { | |||
| return new Promise((resolve, reject) => { | |||
| getTypes(option).then((res) => { | |||
| const { page } = res | |||
| commit('SET_TYPE_DATA', page) | |||
| resolve() | |||
| }).catch((err) => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| fetchTypesAll({ commit }, option) { | |||
| return new Promise((resolve, reject) => { | |||
| getTypesAll(option).then((res) => { | |||
| const { list } = res | |||
| commit('SET_ALL_TYPES', list) | |||
| resolve() | |||
| }).catch((err) => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| fetchTypeMsg({ commit }, typeId) { | |||
| return new Promise((resolve, reject) => { | |||
| getTypeMsg(typeId).then((res) => { | |||
| const { videoSubject } = res | |||
| commit('SET_TYPE_MSG', videoSubject) | |||
| resolve() | |||
| }).catch((err) => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| saveTypes({ commit }, typeMsg) { | |||
| return new Promise((resolve, reject) => { | |||
| addTypes(typeMsg).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| removeTypes({ commit }, typesId) { | |||
| return new Promise((resolve, reject) => { | |||
| removeTypes(typesId).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| updateTypes({ commit }, typeMsg) { | |||
| return new Promise((resolve, reject) => { | |||
| updateTypes(typeMsg).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| updateOrderTypes({ commit }, order) { | |||
| return new Promise((resolve, reject) => { | |||
| updateTypesOrder(order).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| } | |||
| } | |||
| export default { | |||
| namespaced: true, | |||
| state, | |||
| mutations, | |||
| actions | |||
| } | |||
| @@ -0,0 +1,187 @@ | |||
| import { login, getInfo, getUserMsg, getUsers, addUsers, removeUsers, updateUsers, updatePassword, getNavMenus } from '@/api/user' | |||
| import { getToken, setToken, removeToken } from '@/utils/auth' | |||
| import { resetRouter } from '@/router' | |||
| const getDefaultState = () => { | |||
| return { | |||
| token: getToken(), | |||
| name: '', | |||
| avatar: '', | |||
| userInfo: '', | |||
| createState: false, | |||
| userData: '', | |||
| userMsg: '', | |||
| navMenuData: '' | |||
| } | |||
| } | |||
| const state = getDefaultState() | |||
| const mutations = { | |||
| RESET_STATE: (state) => { | |||
| Object.assign(state, getDefaultState()) | |||
| }, | |||
| SET_TOKEN: (state, token) => { | |||
| state.token = token | |||
| }, | |||
| SET_NAME: (state, name) => { | |||
| state.name = name | |||
| }, | |||
| SET_AVATAR: (state, avatar) => { | |||
| state.avatar = avatar | |||
| }, | |||
| SET_USER_INFO: (state, userInfo) => { | |||
| state.userInfo = userInfo | |||
| }, | |||
| SET_CREATE_STATE: (state, payload) => { | |||
| state.createState = payload | |||
| }, | |||
| SET_USER_DATA: (state, payload) => { | |||
| state.userData = payload | |||
| }, | |||
| SET_USER_MSG: (state, payload) => { | |||
| state.userMsg = payload | |||
| }, | |||
| SET_NAV_MENU_DATA: (state, payload) => { | |||
| state.navMenuData = payload | |||
| } | |||
| } | |||
| const actions = { | |||
| // user login | |||
| login({ commit }, userInfo) { | |||
| const { username, password, captcha, uuid } = userInfo | |||
| return new Promise((resolve, reject) => { | |||
| login({ username: username.trim(), password, captcha, uuid }).then(response => { | |||
| commit('SET_TOKEN', response.token) | |||
| setToken(response.token) | |||
| resolve() | |||
| }).catch(error => { | |||
| reject(error) | |||
| }) | |||
| }) | |||
| }, | |||
| // get user info | |||
| getInfo({ commit, state }) { | |||
| return new Promise((resolve, reject) => { | |||
| getInfo(state.token).then(response => { | |||
| const { user } = response | |||
| if (!user) { | |||
| reject('验证失败,请重新登录.') | |||
| } | |||
| const { username, avatar } = user | |||
| commit('SET_NAME', username) | |||
| commit('SET_AVATAR', avatar) | |||
| commit('SET_USER_INFO', user) | |||
| resolve(user) | |||
| }).catch(error => { | |||
| reject(error) | |||
| }) | |||
| }) | |||
| }, | |||
| // user logout | |||
| logout({ commit, state }) { | |||
| return new Promise((resolve, reject) => { | |||
| removeToken() // must remove token first | |||
| resetRouter() | |||
| commit('RESET_STATE') | |||
| resolve() | |||
| // logout(state.token).then(() => { | |||
| // }).catch(error => { | |||
| // reject(error) | |||
| // }) | |||
| }) | |||
| }, | |||
| // remove token | |||
| resetToken({ commit }) { | |||
| return new Promise(resolve => { | |||
| removeToken() // must remove token first | |||
| commit('RESET_STATE') | |||
| resolve() | |||
| }) | |||
| }, | |||
| fetchUserList({ commit }, option) { | |||
| return new Promise((resolve, reject) => { | |||
| getUsers(option).then((res) => { | |||
| const { page } = res | |||
| commit('SET_USER_DATA', page) | |||
| resolve() | |||
| }).catch((err) => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| fetchUserMsg({ commit }, userId) { | |||
| return new Promise((resolve, reject) => { | |||
| getUserMsg(userId).then((res) => { | |||
| const { user } = res | |||
| commit('SET_USER_MSG', user) | |||
| resolve() | |||
| }).catch((err) => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| saveUsers({ commit }, userMsg) { | |||
| return new Promise((resolve, reject) => { | |||
| addUsers(userMsg).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| removeUsers({ commit }, userId) { | |||
| return new Promise((resolve, reject) => { | |||
| removeUsers(userId).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| updateUsers({ commit }, userMsg) { | |||
| return new Promise((resolve, reject) => { | |||
| updateUsers(userMsg).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| updatePassword({ commit }, pass) { | |||
| return new Promise((resolve, reject) => { | |||
| updatePassword(pass).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| fetchNavMenus({ commit }) { | |||
| return new Promise((resolve, reject) => { | |||
| getNavMenus().then((res) => { | |||
| const { menuList } = res | |||
| commit('SET_NAV_MENU_DATA', menuList) | |||
| resolve() | |||
| }).catch((err) => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| } | |||
| } | |||
| export default { | |||
| namespaced: true, | |||
| state, | |||
| mutations, | |||
| actions | |||
| } | |||
| @@ -0,0 +1,130 @@ | |||
| import { getVideos, getVideoMsg, addVideos, removeVideos, updateVideos, updateVideosOrder, videosUp, videosDown } from '@/api/videos' | |||
| const state = { | |||
| videoData: '', | |||
| videoMsg: '' | |||
| } | |||
| const mutations = { | |||
| SET_VIDEO_DATA: (state, payload) => { | |||
| state.videoData = payload | |||
| }, | |||
| SET_VIDEO_MSG: (state, payload) => { | |||
| state.videoMsg = payload | |||
| } | |||
| } | |||
| const actions = { | |||
| clearVideoData({ commit }) { | |||
| commit('SET_VIDEO_DATA', '') | |||
| }, | |||
| fetchVideoList({ commit }, option) { | |||
| return new Promise((resolve, reject) => { | |||
| getVideos(option).then((res) => { | |||
| const { page } = res | |||
| page['list'].map(item => { | |||
| item['label'] = item['label'] ? item['label'].split(',') : [] | |||
| return item | |||
| }) | |||
| commit('SET_VIDEO_DATA', page) | |||
| resolve(res) | |||
| }).catch((err) => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| fetchVideoMsg({ commit }, videoId) { | |||
| return new Promise((resolve, reject) => { | |||
| getVideoMsg(videoId).then((res) => { | |||
| const { video } = res | |||
| video['label'] = video['label'] ? video['label'].split(',') : [] | |||
| commit('SET_VIDEO_MSG', video) | |||
| resolve() | |||
| }).catch((err) => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| saveVideos({ commit }, videoMsg) { | |||
| return new Promise((resolve, reject) => { | |||
| addVideos(videoMsg).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| removeVideos({ commit }, videoId) { | |||
| return new Promise((resolve, reject) => { | |||
| removeVideos(videoId).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| updateVideos({ commit }, videoMsg) { | |||
| return new Promise((resolve, reject) => { | |||
| updateVideos(videoMsg).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| updateOrderVideos({ commit }, order) { | |||
| return new Promise((resolve, reject) => { | |||
| updateVideosOrder(order).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| publicVideos({ commit }, order) { | |||
| return new Promise((resolve, reject) => { | |||
| videosUp(order).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| }, | |||
| privateVideos({ commit }, order) { | |||
| return new Promise((resolve, reject) => { | |||
| videosDown(order).then(res => { | |||
| resolve(res) | |||
| }).catch(err => { | |||
| reject(err) | |||
| }) | |||
| }) | |||
| } | |||
| // getUrl({ commit }, vid) { | |||
| // return new Promise((resolve, reject) => { | |||
| // // this.$jsonp(`http://vv.video.qq.com/getinfo?vids=${vid}&platform=101001&charge=0&otype=json`).then(res => { | |||
| // // console.log(res, '===========') | |||
| // // const start = res.data.indexOf('{') | |||
| // // const end = res.data.lastIndexOf(';') | |||
| // // const newData = res.data.substring(start, end) | |||
| // // resolve(JSON.parse(newData)) | |||
| // // }).catch(err => { | |||
| // // reject(err) | |||
| // // }); | |||
| // getVideoUrl(vid).then(res => { | |||
| // const start = res.data.indexOf('{') | |||
| // const end = res.data.lastIndexOf(';') | |||
| // const newData = res.data.substring(start, end) | |||
| // resolve(JSON.parse(newData)) | |||
| // }).catch(err => { | |||
| // reject(err) | |||
| // }) | |||
| // }) | |||
| // } | |||
| } | |||
| export default { | |||
| namespaced: true, | |||
| state, | |||
| mutations, | |||
| actions | |||
| } | |||
| @@ -0,0 +1,49 @@ | |||
| // cover some element-ui styles | |||
| .el-breadcrumb__inner, | |||
| .el-breadcrumb__inner a { | |||
| font-weight: 400 !important; | |||
| } | |||
| .el-upload { | |||
| input[type="file"] { | |||
| display: none !important; | |||
| } | |||
| } | |||
| .el-upload__input { | |||
| display: none; | |||
| } | |||
| // to fixed https://github.com/ElemeFE/element/issues/2461 | |||
| .el-dialog { | |||
| transform: none; | |||
| left: 0; | |||
| position: relative; | |||
| margin: 0 auto; | |||
| } | |||
| // refine element ui upload | |||
| .upload-container { | |||
| .el-upload { | |||
| width: 100%; | |||
| .el-upload-dragger { | |||
| width: 100%; | |||
| height: 200px; | |||
| } | |||
| } | |||
| } | |||
| // dropdown | |||
| .el-dropdown-menu { | |||
| a { | |||
| display: block | |||
| } | |||
| } | |||
| // to fix el-date-picker css style | |||
| .el-range-separator { | |||
| box-sizing: content-box; | |||
| } | |||
| @@ -0,0 +1,66 @@ | |||
| @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%; | |||
| overflow: hidden; | |||
| 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; | |||
| } | |||
| @@ -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%; | |||
| } | |||
| @@ -0,0 +1,226 @@ | |||
| #app { | |||
| .main-container { | |||
| min-height: 100%; | |||
| transition: margin-left .28s; | |||
| margin-left: $sideBarWidth; | |||
| position: relative; | |||
| } | |||
| .sidebar-container { | |||
| transition: width 0.28s; | |||
| width: $sideBarWidth !important; | |||
| background-color: $menuBg; | |||
| height: 100%; | |||
| position: fixed; | |||
| font-size: 0px; | |||
| top: 0; | |||
| bottom: 0; | |||
| left: 0; | |||
| z-index: 1001; | |||
| overflow: hidden; | |||
| // reset element-ui css | |||
| .horizontal-collapse-transition { | |||
| transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; | |||
| } | |||
| .scrollbar-wrapper { | |||
| overflow-x: hidden !important; | |||
| } | |||
| .el-scrollbar__bar.is-vertical { | |||
| right: 0px; | |||
| } | |||
| .el-scrollbar { | |||
| height: 100%; | |||
| } | |||
| &.has-logo { | |||
| .el-scrollbar { | |||
| height: calc(100% - 50px); | |||
| } | |||
| } | |||
| .is-horizontal { | |||
| display: none; | |||
| } | |||
| a { | |||
| display: inline-block; | |||
| width: 100%; | |||
| overflow: hidden; | |||
| } | |||
| .svg-icon { | |||
| margin-right: 16px; | |||
| } | |||
| .sub-el-icon { | |||
| margin-right: 12px; | |||
| margin-left: -2px; | |||
| } | |||
| .el-menu { | |||
| border: none; | |||
| height: 100%; | |||
| width: 100% !important; | |||
| } | |||
| // menu hover | |||
| .submenu-title-noDropdown, | |||
| .el-submenu__title { | |||
| &:hover { | |||
| background-color: $menuHover !important; | |||
| } | |||
| } | |||
| .is-active>.el-submenu__title { | |||
| color: $subMenuActiveText !important; | |||
| } | |||
| & .nest-menu .el-submenu>.el-submenu__title, | |||
| & .el-submenu .el-menu-item { | |||
| min-width: $sideBarWidth !important; | |||
| background-color: $subMenuBg !important; | |||
| &:hover { | |||
| background-color: $subMenuHover !important; | |||
| } | |||
| } | |||
| } | |||
| .hideSidebar { | |||
| .sidebar-container { | |||
| width: 54px !important; | |||
| } | |||
| .main-container { | |||
| margin-left: 54px; | |||
| } | |||
| .submenu-title-noDropdown { | |||
| padding: 0 !important; | |||
| position: relative; | |||
| .el-tooltip { | |||
| padding: 0 !important; | |||
| .svg-icon { | |||
| margin-left: 20px; | |||
| } | |||
| .sub-el-icon { | |||
| margin-left: 19px; | |||
| } | |||
| } | |||
| } | |||
| .el-submenu { | |||
| overflow: hidden; | |||
| &>.el-submenu__title { | |||
| padding: 0 !important; | |||
| .svg-icon { | |||
| margin-left: 20px; | |||
| } | |||
| .sub-el-icon { | |||
| margin-left: 19px; | |||
| } | |||
| .el-submenu__icon-arrow { | |||
| display: none; | |||
| } | |||
| } | |||
| } | |||
| .el-menu--collapse { | |||
| .el-submenu { | |||
| &>.el-submenu__title { | |||
| &>span { | |||
| height: 0; | |||
| width: 0; | |||
| overflow: hidden; | |||
| visibility: hidden; | |||
| display: inline-block; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .el-menu--collapse .el-menu .el-submenu { | |||
| min-width: $sideBarWidth !important; | |||
| } | |||
| // mobile responsive | |||
| .mobile { | |||
| .main-container { | |||
| margin-left: 0px; | |||
| } | |||
| .sidebar-container { | |||
| transition: transform .28s; | |||
| width: $sideBarWidth !important; | |||
| } | |||
| &.hideSidebar { | |||
| .sidebar-container { | |||
| pointer-events: none; | |||
| transition-duration: 0.3s; | |||
| transform: translate3d(-$sideBarWidth, 0, 0); | |||
| } | |||
| } | |||
| } | |||
| .withoutAnimation { | |||
| .main-container, | |||
| .sidebar-container { | |||
| transition: none; | |||
| } | |||
| } | |||
| } | |||
| // when menu collapsed | |||
| .el-menu--vertical { | |||
| &>.el-menu { | |||
| .svg-icon { | |||
| margin-right: 16px; | |||
| } | |||
| .sub-el-icon { | |||
| margin-right: 12px; | |||
| margin-left: -2px; | |||
| } | |||
| } | |||
| .nest-menu .el-submenu>.el-submenu__title, | |||
| .el-menu-item { | |||
| &:hover { | |||
| // you can use $subMenuHover | |||
| background-color: $menuHover !important; | |||
| } | |||
| } | |||
| // the scroll bar appears when the subMenu is too long | |||
| >.el-menu--popup { | |||
| max-height: 100vh; | |||
| overflow-y: auto; | |||
| &::-webkit-scrollbar-track-piece { | |||
| background: #d3dce6; | |||
| } | |||
| &::-webkit-scrollbar { | |||
| width: 6px; | |||
| } | |||
| &::-webkit-scrollbar-thumb { | |||
| background: #99a9bf; | |||
| border-radius: 20px; | |||
| } | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -0,0 +1,25 @@ | |||
| // sidebar | |||
| $menuText:#bfcbd9; | |||
| $menuActiveText:#409EFF; | |||
| $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951 | |||
| $menuBg:#304156; | |||
| $menuHover:#263445; | |||
| $subMenuBg:#1f2d3d; | |||
| $subMenuHover:#001528; | |||
| $sideBarWidth: 210px; | |||
| // the :export directive is the magic sauce for webpack | |||
| // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass | |||
| :export { | |||
| menuText: $menuText; | |||
| menuActiveText: $menuActiveText; | |||
| subMenuActiveText: $subMenuActiveText; | |||
| menuBg: $menuBg; | |||
| menuHover: $menuHover; | |||
| subMenuBg: $subMenuBg; | |||
| subMenuHover: $subMenuHover; | |||
| sideBarWidth: $sideBarWidth; | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| import Cookies from 'js-cookie' | |||
| const TokenKey = 'video_token' | |||
| export function getToken() { | |||
| return Cookies.get(TokenKey) | |||
| } | |||
| export function setToken(token) { | |||
| return Cookies.set(TokenKey, token) | |||
| } | |||
| export function removeToken() { | |||
| return Cookies.remove(TokenKey) | |||
| } | |||
| @@ -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}` | |||
| } | |||
| @@ -0,0 +1,132 @@ | |||
| /** | |||
| * Created by PanJiaChen on 16/11/18. | |||
| */ | |||
| /** | |||
| * Parse the time to string | |||
| * @param {(Object|string|number)} time | |||
| * @param {string} cFormat | |||
| * @returns {string | null} | |||
| */ | |||
| export function parseTime(time, cFormat) { | |||
| if (arguments.length === 0 || !time) { | |||
| return null | |||
| } | |||
| const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' | |||
| let date | |||
| if (typeof time === 'object') { | |||
| date = time | |||
| } else { | |||
| if ((typeof time === 'string')) { | |||
| if ((/^[0-9]+$/.test(time))) { | |||
| // support "1548221490638" | |||
| time = parseInt(time) | |||
| } else { | |||
| // support safari | |||
| // https://stackoverflow.com/questions/4310953/invalid-date-in-safari | |||
| time = time.replace(new RegExp(/-/gm), '/') | |||
| } | |||
| } | |||
| if ((typeof time === 'number') && (time.toString().length === 10)) { | |||
| time = time * 1000 | |||
| } | |||
| date = new Date(time) | |||
| } | |||
| const formatObj = { | |||
| y: date.getFullYear(), | |||
| m: date.getMonth() + 1, | |||
| d: date.getDate(), | |||
| h: date.getHours(), | |||
| i: date.getMinutes(), | |||
| s: date.getSeconds(), | |||
| a: date.getDay() | |||
| } | |||
| const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { | |||
| const value = formatObj[key] | |||
| // Note: getDay() returns 0 on Sunday | |||
| if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } | |||
| return value.toString().padStart(2, '0') | |||
| }) | |||
| return time_str | |||
| } | |||
| /** | |||
| * @param {number} time | |||
| * @param {string} option | |||
| * @returns {string} | |||
| */ | |||
| export function formatTime(time, option) { | |||
| if (('' + time).length === 10) { | |||
| time = parseInt(time) * 1000 | |||
| } else { | |||
| time = +time | |||
| } | |||
| const d = new Date(time) | |||
| const now = Date.now() | |||
| const diff = (now - d) / 1000 | |||
| if (diff < 30) { | |||
| return '刚刚' | |||
| } else if (diff < 3600) { | |||
| // less 1 hour | |||
| return Math.ceil(diff / 60) + '分钟前' | |||
| } else if (diff < 3600 * 24) { | |||
| return Math.ceil(diff / 3600) + '小时前' | |||
| } else if (diff < 3600 * 24 * 2) { | |||
| return '1天前' | |||
| } | |||
| if (option) { | |||
| return parseTime(time, option) | |||
| } else { | |||
| return ( | |||
| d.getMonth() + | |||
| 1 + | |||
| '月' + | |||
| d.getDate() + | |||
| '日' + | |||
| d.getHours() + | |||
| '时' + | |||
| d.getMinutes() + | |||
| '分' | |||
| ) | |||
| } | |||
| } | |||
| /** | |||
| * @param {string} url | |||
| * @returns {Object} | |||
| */ | |||
| export function param2Obj(url) { | |||
| const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') | |||
| if (!search) { | |||
| return {} | |||
| } | |||
| const obj = {} | |||
| const searchArr = search.split('&') | |||
| searchArr.forEach(v => { | |||
| const index = v.indexOf('=') | |||
| if (index !== -1) { | |||
| const name = v.substring(0, index) | |||
| const val = v.substring(index + 1, v.length) | |||
| obj[name] = val | |||
| } | |||
| }) | |||
| return obj | |||
| } | |||
| export const Debounce = (fn, wait) => { | |||
| const delay = wait || 500 | |||
| let timer = null | |||
| return function() { | |||
| if (timer) { | |||
| clearTimeout(timer) | |||
| } | |||
| const callNow = !timer | |||
| timer = setTimeout(() => { | |||
| timer = null | |||
| }, delay) | |||
| if (callNow) fn.apply(this, arguments) | |||
| } | |||
| } | |||
| @@ -0,0 +1,83 @@ | |||
| import axios from 'axios' | |||
| import { MessageBox, Message } from 'element-ui' | |||
| import store from '@/store' | |||
| import { getToken } from '@/utils/auth' | |||
| // create an axios instance | |||
| const service = axios.create({ | |||
| baseURL: process.env.VUE_APP_BASE_API // url = base url + request url | |||
| // withCredentials: true, // send cookies when cross-domain requests | |||
| // timeout: 5000 // request timeout | |||
| }) | |||
| // request interceptor | |||
| service.interceptors.request.use( | |||
| config => { | |||
| // do something before request is sent | |||
| if (store.getters.token) { | |||
| // let each request carry token | |||
| // ['X-Token'] is a custom headers key | |||
| // please modify it according to the actual situation | |||
| config.headers['token'] = getToken() | |||
| } | |||
| return config | |||
| }, | |||
| error => { | |||
| // do something with request error | |||
| console.log(error) // for debug | |||
| return Promise.reject(error) | |||
| } | |||
| ) | |||
| // response interceptor | |||
| service.interceptors.response.use( | |||
| /** | |||
| * If you want to get http information such as headers or status | |||
| * Please return response => response | |||
| */ | |||
| /** | |||
| * Determine the request status by custom code | |||
| * Here is just an example | |||
| * You can also judge the status by HTTP Status Code | |||
| */ | |||
| response => { | |||
| const res = response.data | |||
| // if the custom code is not 20000, it is judged as an error. | |||
| if (res.code && res.code !== 0) { | |||
| Message({ | |||
| message: res.msg || 'Error', | |||
| type: 'error', | |||
| duration: 5 * 1000 | |||
| }) | |||
| // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; | |||
| if (res.code && res.code === 401) { | |||
| // to re-login | |||
| MessageBox.confirm('您已经退出登录,请重新重新登录', '退出登录', { | |||
| confirmButtonText: '重新登录', | |||
| // cancelButtonText: '返回', | |||
| showCancelButton: false, | |||
| type: 'warning' | |||
| }).then(() => { | |||
| store.dispatch('user/resetToken').then(() => { | |||
| location.reload() | |||
| }) | |||
| }) | |||
| } | |||
| return Promise.reject(new Error(res.msg || 'Error')) | |||
| } else { | |||
| return res | |||
| } | |||
| }, | |||
| error => { | |||
| Message({ | |||
| message: error.msg || error, | |||
| type: 'error', | |||
| duration: 5 * 1000 | |||
| }) | |||
| return Promise.reject(error) | |||
| } | |||
| ) | |||
| export default service | |||
| @@ -0,0 +1,20 @@ | |||
| /** | |||
| * Created by PanJiaChen on 16/11/18. | |||
| */ | |||
| /** | |||
| * @param {string} path | |||
| * @returns {Boolean} | |||
| */ | |||
| export function isExternal(path) { | |||
| return /^(https?:|mailto:|tel:)/.test(path) | |||
| } | |||
| /** | |||
| * @param {string} str | |||
| * @returns {Boolean} | |||
| */ | |||
| export function validUsername(str) { | |||
| const valid_map = ['admin', 'editor'] | |||
| return valid_map.indexOf(str.trim()) >= 0 | |||
| } | |||
| @@ -0,0 +1,228 @@ | |||
| <template> | |||
| <div class="wscn-http404-container"> | |||
| <div class="wscn-http404"> | |||
| <div class="pic-404"> | |||
| <img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404"> | |||
| <img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404"> | |||
| <img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404"> | |||
| <img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404"> | |||
| </div> | |||
| <div class="bullshit"> | |||
| <div class="bullshit__oops">OOPS!</div> | |||
| <div class="bullshit__info">All rights reserved | |||
| <a style="color:#20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a> | |||
| </div> | |||
| <div class="bullshit__headline">{{ message }}</div> | |||
| <div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div> | |||
| <a href="" class="bullshit__return-home">Back to home</a> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'Page404', | |||
| computed: { | |||
| message() { | |||
| return 'The webmaster said that you can not enter this page...' | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .wscn-http404-container{ | |||
| transform: translate(-50%,-50%); | |||
| position: absolute; | |||
| top: 40%; | |||
| left: 50%; | |||
| } | |||
| .wscn-http404 { | |||
| position: relative; | |||
| width: 1200px; | |||
| padding: 0 50px; | |||
| overflow: hidden; | |||
| .pic-404 { | |||
| position: relative; | |||
| float: left; | |||
| width: 600px; | |||
| overflow: hidden; | |||
| &__parent { | |||
| width: 100%; | |||
| } | |||
| &__child { | |||
| position: absolute; | |||
| &.left { | |||
| width: 80px; | |||
| top: 17px; | |||
| left: 220px; | |||
| opacity: 0; | |||
| animation-name: cloudLeft; | |||
| animation-duration: 2s; | |||
| animation-timing-function: linear; | |||
| animation-fill-mode: forwards; | |||
| animation-delay: 1s; | |||
| } | |||
| &.mid { | |||
| width: 46px; | |||
| top: 10px; | |||
| left: 420px; | |||
| opacity: 0; | |||
| animation-name: cloudMid; | |||
| animation-duration: 2s; | |||
| animation-timing-function: linear; | |||
| animation-fill-mode: forwards; | |||
| animation-delay: 1.2s; | |||
| } | |||
| &.right { | |||
| width: 62px; | |||
| top: 100px; | |||
| left: 500px; | |||
| opacity: 0; | |||
| animation-name: cloudRight; | |||
| animation-duration: 2s; | |||
| animation-timing-function: linear; | |||
| animation-fill-mode: forwards; | |||
| animation-delay: 1s; | |||
| } | |||
| @keyframes cloudLeft { | |||
| 0% { | |||
| top: 17px; | |||
| left: 220px; | |||
| opacity: 0; | |||
| } | |||
| 20% { | |||
| top: 33px; | |||
| left: 188px; | |||
| opacity: 1; | |||
| } | |||
| 80% { | |||
| top: 81px; | |||
| left: 92px; | |||
| opacity: 1; | |||
| } | |||
| 100% { | |||
| top: 97px; | |||
| left: 60px; | |||
| opacity: 0; | |||
| } | |||
| } | |||
| @keyframes cloudMid { | |||
| 0% { | |||
| top: 10px; | |||
| left: 420px; | |||
| opacity: 0; | |||
| } | |||
| 20% { | |||
| top: 40px; | |||
| left: 360px; | |||
| opacity: 1; | |||
| } | |||
| 70% { | |||
| top: 130px; | |||
| left: 180px; | |||
| opacity: 1; | |||
| } | |||
| 100% { | |||
| top: 160px; | |||
| left: 120px; | |||
| opacity: 0; | |||
| } | |||
| } | |||
| @keyframes cloudRight { | |||
| 0% { | |||
| top: 100px; | |||
| left: 500px; | |||
| opacity: 0; | |||
| } | |||
| 20% { | |||
| top: 120px; | |||
| left: 460px; | |||
| opacity: 1; | |||
| } | |||
| 80% { | |||
| top: 180px; | |||
| left: 340px; | |||
| opacity: 1; | |||
| } | |||
| 100% { | |||
| top: 200px; | |||
| left: 300px; | |||
| opacity: 0; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .bullshit { | |||
| position: relative; | |||
| float: left; | |||
| width: 300px; | |||
| padding: 30px 0; | |||
| overflow: hidden; | |||
| &__oops { | |||
| font-size: 32px; | |||
| font-weight: bold; | |||
| line-height: 40px; | |||
| color: #1482f0; | |||
| opacity: 0; | |||
| margin-bottom: 20px; | |||
| animation-name: slideUp; | |||
| animation-duration: 0.5s; | |||
| animation-fill-mode: forwards; | |||
| } | |||
| &__headline { | |||
| font-size: 20px; | |||
| line-height: 24px; | |||
| color: #222; | |||
| font-weight: bold; | |||
| opacity: 0; | |||
| margin-bottom: 10px; | |||
| animation-name: slideUp; | |||
| animation-duration: 0.5s; | |||
| animation-delay: 0.1s; | |||
| animation-fill-mode: forwards; | |||
| } | |||
| &__info { | |||
| font-size: 13px; | |||
| line-height: 21px; | |||
| color: grey; | |||
| opacity: 0; | |||
| margin-bottom: 30px; | |||
| animation-name: slideUp; | |||
| animation-duration: 0.5s; | |||
| animation-delay: 0.2s; | |||
| animation-fill-mode: forwards; | |||
| } | |||
| &__return-home { | |||
| display: block; | |||
| float: left; | |||
| width: 110px; | |||
| height: 36px; | |||
| background: #1482f0; | |||
| border-radius: 100px; | |||
| text-align: center; | |||
| color: #ffffff; | |||
| opacity: 0; | |||
| font-size: 14px; | |||
| line-height: 36px; | |||
| cursor: pointer; | |||
| animation-name: slideUp; | |||
| animation-duration: 0.5s; | |||
| animation-delay: 0.3s; | |||
| animation-fill-mode: forwards; | |||
| } | |||
| @keyframes slideUp { | |||
| 0% { | |||
| transform: translateY(60px); | |||
| opacity: 0; | |||
| } | |||
| 100% { | |||
| transform: translateY(0); | |||
| opacity: 1; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,85 @@ | |||
| <template> | |||
| <div class="new-container"> | |||
| <div class="new-text">最新视频</div> | |||
| <div class="new-content"> | |||
| <el-row :gutter="20"> | |||
| <el-col v-for="item in videoData.list" :key="item.id" :span="6"> | |||
| <public-video :media-data="item" @handleVideoClick="handleVideoClick" /> | |||
| </el-col> | |||
| </el-row> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import PublicVideo from '@/layout/components/PublicVideo' | |||
| import { mapGetters, mapActions } from 'vuex' | |||
| export default { | |||
| name: 'Dashboard', | |||
| components: { | |||
| PublicVideo | |||
| }, | |||
| data() { | |||
| return { | |||
| currentVideo: '' | |||
| } | |||
| }, | |||
| computed: { | |||
| ...mapGetters([ | |||
| 'videoData', | |||
| 'navMenuData' | |||
| ]) | |||
| }, | |||
| created() { | |||
| this.getVideos({ limit: 12, page: 1 }) | |||
| }, | |||
| methods: { | |||
| ...mapActions({ | |||
| getVideos: 'videos/fetchVideoList' | |||
| }), | |||
| handleVideoClick(videoObj) { | |||
| if (!this.currentVideo) { | |||
| this.currentVideo = videoObj | |||
| videoObj['video'].play() | |||
| } else { | |||
| if (this.currentVideo['id'] === videoObj['id']) { | |||
| if (!videoObj['playState']) { | |||
| videoObj['video'].play() | |||
| } else { | |||
| videoObj['video'].pause() | |||
| } | |||
| } else { | |||
| this.currentVideo['video'].pause() | |||
| this.currentVideo = videoObj | |||
| videoObj['video'].play() | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .new { | |||
| &-container { | |||
| padding: 30px; | |||
| height: 100vh; | |||
| overflow-y: auto; | |||
| background: #f2f4f8; | |||
| } | |||
| &-content { | |||
| margin-bottom: 30px; | |||
| } | |||
| &-text { | |||
| font-size: 30px; | |||
| line-height: 46px; | |||
| } | |||
| } | |||
| .el-row { | |||
| background: #f2f4f8; | |||
| } | |||
| .el-col { | |||
| border-radius: 4px; | |||
| margin: 10px auto; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,200 @@ | |||
| <template> | |||
| <div class="app-container"> | |||
| <public-header | |||
| placeholder-text="请输入部门名称" | |||
| button-text="添加部门" | |||
| @handleClickInput="handleClickInput" | |||
| @handleClickCreate="handleClickCreate" | |||
| /> | |||
| <div class="app-body"> | |||
| <public-table | |||
| :table-header="tableHeader" | |||
| :table-data="tableData" | |||
| @handleRemoveClick="handleRemoveClick" | |||
| @handleUpdateClick="handleUpdateClick" | |||
| /> | |||
| </div> | |||
| <public-dialog :dialog-title="dialogTitle" :dialog-form-visible="dialogFormVisible"> | |||
| <template #content> | |||
| <el-form | |||
| ref="ruleForm" | |||
| :model="ruleForm" | |||
| :rules="rules" | |||
| size="small" | |||
| label-width="100px" | |||
| > | |||
| <el-form-item label="部门名称" prop="name"> | |||
| <el-input v-model="ruleForm.name" placeholder="部门名称" /> | |||
| </el-form-item> | |||
| <el-form-item label="上级部门" prop="department"> | |||
| <el-select v-model="ruleForm.department" placeholder="上级部门" style="width: 220px;"> | |||
| <el-option label="行政部" value="1" /> | |||
| <el-option label="人事部" value="2" /> | |||
| </el-select> | |||
| </el-form-item> | |||
| <el-form-item label="部门主管" prop="leader"> | |||
| <el-select v-model="ruleForm.leader" placeholder="部门主管" style="width: 220px;"> | |||
| <el-option label="张三" value="1" /> | |||
| <el-option label="李四" value="2" /> | |||
| </el-select> | |||
| </el-form-item> | |||
| <el-form-item label="邮箱" prop="email"> | |||
| <el-input v-model="ruleForm.email" placeholder="邮箱" /> | |||
| </el-form-item> | |||
| <el-form-item label="手机号" prop="phone"> | |||
| <el-input v-model="ruleForm.phone" placeholder="手机号" /> | |||
| </el-form-item> | |||
| </el-form> | |||
| </template> | |||
| <template #footer> | |||
| <el-button @click="handleCheckVisible">取 消</el-button> | |||
| <el-button type="primary" @click="handleCheckVisible">确 定</el-button> | |||
| </template> | |||
| </public-dialog> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import PublicHeader from '@/layout/components/PublicHeader' | |||
| import PublicTable from '@/layout/components/PublicTable' | |||
| import PublicDialog from '@/layout/components/PublicDialog' | |||
| import { Debounce } from '@/utils/index' | |||
| export default { | |||
| components: { | |||
| PublicHeader, | |||
| PublicTable, | |||
| PublicDialog | |||
| }, | |||
| data() { | |||
| const checkName = (rule, value, callback) => { | |||
| if (!value) { | |||
| return callback(new Error('请输入账号')) | |||
| } | |||
| callback() | |||
| } | |||
| const checkEmail = (rule, value, callback) => { | |||
| if (!value) { | |||
| callback(new Error('请输入邮箱')) | |||
| } else { | |||
| const emailReg = /^([a-zA-Z]|[0-9])(\w|\-)+@[a-zA-Z0-9]+\.([a-zA-Z]{2,4})$/ | |||
| emailReg.test(value) ? callback() : callback(new Error('请输入正确的邮箱')) | |||
| } | |||
| } | |||
| const checkPhone = (rule, value, callback) => { | |||
| if (!value) { | |||
| callback(new Error('请输入手机号')) | |||
| } else { | |||
| const phoneReg = /^[1][3,4,5,7,8][0-9]{9}$/ | |||
| phoneReg.test(value) ? callback() : callback(new Error('请输入正确的手机号')) | |||
| } | |||
| } | |||
| return { | |||
| dialogFormVisible: false, | |||
| dialogTitle: '', | |||
| ruleForm: { | |||
| name: '', | |||
| department: '', | |||
| leader: '', | |||
| email: '', | |||
| phone: '' | |||
| }, | |||
| rules: { | |||
| name: [ | |||
| { required: true, validator: checkName, trigger: 'blur' } | |||
| ], | |||
| department: [ | |||
| { required: true, message: '请选择所属部门', trigger: 'blur' } | |||
| ], | |||
| leader: [ | |||
| { required: true, message: '请选择部门主管', trigger: 'blur' } | |||
| ], | |||
| email: [ | |||
| { required: true, validator: checkEmail, trigger: 'blur' } | |||
| ], | |||
| phone: [ | |||
| { validator: checkPhone, trigger: 'blur' } | |||
| ] | |||
| }, | |||
| tableHeader: [ | |||
| { label: '#', width: '60', prop: 'id' }, | |||
| { label: '部门名称', width: '', prop: 'name' }, | |||
| { label: '部门主管', width: '', prop: 'leader' }, | |||
| { label: '上级部门', width: '', prop: 'department' }, | |||
| { label: '邮箱', width: '', prop: 'email' }, | |||
| { label: '手机号', width: '', prop: 'phone' }, | |||
| { label: '创建时间', width: '', prop: 'date' } | |||
| ], | |||
| tableData: [ | |||
| { id: 1, date: '2020-06-24 11:12:29', name: '人事部', leader: '张三', department: '123344444444', email: '12333@163.com' }, | |||
| { id: 2, date: '2020-06-24 12:15:20', name: '行政部', leader: '李四', department: '123344444444', email: '12332@163.com' }, | |||
| { id: 3, date: '2020-06-24 13:16:43', name: '安保部', leader: '王五', department: '123344444444', email: '12331@163.com' }, | |||
| { id: 4, date: '2020-06-24 13:42:15', name: '后勤部', leader: '周六', department: '123344444444', email: '12334@163.com' }, | |||
| { id: 5, date: '2020-06-24 15:22:57', name: '商务部', leader: '赵七', department: '123344444444', email: '12353@163.com' }, | |||
| { id: 6, date: '2020-06-24 16:38:09', name: '销售部', leader: '孙八', department: '123344444444', email: '12336@163.com' } | |||
| ] | |||
| } | |||
| }, | |||
| methods: { | |||
| handleClickInput: Debounce(function(option) { | |||
| console.log(option, '查询') | |||
| }), | |||
| handleClickCreate: Debounce(function(option) { | |||
| console.log(option, '添加') | |||
| this.handleCheckVisible() | |||
| }), | |||
| handleRemoveClick: Debounce(function(option) { | |||
| console.log(option, '删除') | |||
| this.$confirm('此操作将永久删除该部门, 是否继续?', '删除部门', { | |||
| confirmButtonText: '确定', | |||
| cancelButtonText: '取消', | |||
| type: 'warning' | |||
| }).then(() => { | |||
| this.$message({ | |||
| type: 'success', | |||
| message: '删除成功!' | |||
| }) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| type: 'info', | |||
| message: '已取消删除!' | |||
| }) | |||
| }) | |||
| }), | |||
| handleUpdateClick: Debounce(function(option) { | |||
| console.log(option, '编辑') | |||
| this.dialogTitle = '修改部门' | |||
| this.ruleForm.name = option.name | |||
| this.ruleForm.department = option.department | |||
| this.ruleForm.leader = option.leader | |||
| this.ruleForm.email = option.email | |||
| this.ruleForm.phone = option.phone | |||
| this.dialogFormVisible = !this.dialogFormVisible | |||
| if (!this.dialogFormVisible) this.resetForm() | |||
| }), | |||
| handleCheckVisible: Debounce(function() { | |||
| this.dialogTitle = '添加部门' | |||
| this.dialogFormVisible = !this.dialogFormVisible | |||
| if (!this.dialogFormVisible) this.resetForm() | |||
| }), | |||
| resetForm() { | |||
| this.$refs['ruleForm'].resetFields() | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scope> | |||
| .app-container { | |||
| padding: 10px; | |||
| } | |||
| .el-message { | |||
| min-width: 120px; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,295 @@ | |||
| <template> | |||
| <div class="login-container"> | |||
| <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left"> | |||
| <div class="title-container"> | |||
| <h3 class="title">欢迎登录</h3> | |||
| </div> | |||
| <el-form-item prop="username"> | |||
| <span class="svg-container"> | |||
| <svg-icon icon-class="user" /> | |||
| </span> | |||
| <el-input | |||
| ref="username" | |||
| v-model="loginForm.username" | |||
| placeholder="账号" | |||
| name="username" | |||
| type="text" | |||
| tabindex="1" | |||
| auto-complete="on" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item prop="password"> | |||
| <span class="svg-container"> | |||
| <svg-icon icon-class="password" /> | |||
| </span> | |||
| <el-input | |||
| :key="passwordType" | |||
| ref="password" | |||
| v-model="loginForm.password" | |||
| :type="passwordType" | |||
| placeholder="密码" | |||
| name="password" | |||
| tabindex="2" | |||
| auto-complete="on" | |||
| /> | |||
| <span class="show-pwd" @click="showPwd"> | |||
| <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" /> | |||
| </span> | |||
| </el-form-item> | |||
| <el-form-item prop="captcha"> | |||
| <el-input | |||
| v-model="loginForm.captcha" | |||
| name="captcha" | |||
| placeholder="请输入验证码" | |||
| class="captcha-input" | |||
| @keyup.enter.native="handleLogin" | |||
| /> | |||
| <!-- 验证码 --> | |||
| <img | |||
| :src="`/video-admin/video-admin/captcha.jpg?uuid=${loginForm.uuid}`" | |||
| alt="验证码获取失败,请刷新重试~~" | |||
| style="height: 47px; vertical-align: middle;color: darkgray; font-size: 12px;" | |||
| @click="handleClickFreshCaptcha" | |||
| > | |||
| </el-form-item> | |||
| <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登录</el-button> | |||
| <!-- <div class="tips"> | |||
| <span style="margin-right:20px;">username: admin</span> | |||
| <span> password: any</span> | |||
| </div> --> | |||
| </el-form> | |||
| <div class="footer"> | |||
| <a href="https://beian.miit.gov.cn" target="_break">粤ICP备案号:粤ICP备13005027号</a> | |||
| <a href="http://www.beian.gov.cn" target="_break">粤公安备案号:44060502002237</a> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| // import { validUsername } from '@/utils/validate' | |||
| import { Debounce } from '@/utils' | |||
| export default { | |||
| name: 'Login', | |||
| data() { | |||
| const validateUsername = (rule, value, callback) => { | |||
| if (!value) { | |||
| callback(new Error('请输入账号!')) | |||
| } else { | |||
| callback() | |||
| } | |||
| } | |||
| const validatePassword = (rule, value, callback) => { | |||
| if (value.length < 5) { | |||
| callback(new Error('请输入正确密码!')) | |||
| } else { | |||
| callback() | |||
| } | |||
| } | |||
| return { | |||
| loginForm: { | |||
| username: '', | |||
| password: '', | |||
| captcha: '', | |||
| uuid: '' | |||
| }, | |||
| loginRules: { | |||
| username: [{ required: true, trigger: 'blur', validator: validateUsername }], | |||
| password: [{ required: true, trigger: 'blur', validator: validatePassword }], | |||
| captcha: [{ required: true, trigger: 'blur', message: '请输入验证码' }] | |||
| }, | |||
| loading: false, | |||
| passwordType: 'password', | |||
| redirect: undefined | |||
| } | |||
| }, | |||
| watch: { | |||
| $route: { | |||
| handler: function(route) { | |||
| this.redirect = route.query && route.query.redirect | |||
| }, | |||
| immediate: true | |||
| } | |||
| }, | |||
| created() { | |||
| this.loginForm.uuid = this.uuid() | |||
| }, | |||
| methods: { | |||
| uuid() { | |||
| const s = [] | |||
| const hexDigits = '0123456789abcdef' | |||
| for (let i = 0; i < 36; i++) { | |||
| s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1) | |||
| } | |||
| s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010 | |||
| s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01 | |||
| s[8] = s[13] = s[18] = s[23] = '-' | |||
| const uuid = s.join('') | |||
| return uuid | |||
| }, | |||
| showPwd() { | |||
| if (this.passwordType === 'password') { | |||
| this.passwordType = '' | |||
| } else { | |||
| this.passwordType = 'password' | |||
| } | |||
| this.$nextTick(() => { | |||
| this.$refs.password.focus() | |||
| }) | |||
| }, | |||
| handleLogin() { | |||
| this.$refs['loginForm'].validate(valid => { | |||
| if (valid) { | |||
| this.loading = true | |||
| this.$store.dispatch('user/login', this.loginForm).then(() => { | |||
| this.$router.push({ path: this.redirect || '/' }) | |||
| this.$store.dispatch('user/fetchNavMenus') | |||
| this.loading = false | |||
| }).catch(() => { | |||
| this.loading = false | |||
| this.handleClickFreshCaptcha() | |||
| }) | |||
| } else { | |||
| console.log('error submit!!') | |||
| return false | |||
| } | |||
| }) | |||
| }, | |||
| handleClickFreshCaptcha: Debounce(function() { | |||
| this.loginForm.uuid = this.uuid() | |||
| }) | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss"> | |||
| /* 修复input 背景不协调 和光标变色 */ | |||
| $bg:#283443; | |||
| $light_gray:#fff; | |||
| $cursor: #fff; | |||
| @supports (-webkit-mask: none) and (not (cater-color: $cursor)) { | |||
| .login-container .el-input input { | |||
| color: $cursor; | |||
| } | |||
| } | |||
| /* reset element-ui css */ | |||
| .login-container { | |||
| .el-input { | |||
| display: inline-block; | |||
| height: 47px; | |||
| width: 85%; | |||
| input { | |||
| background: transparent; | |||
| border: 0px; | |||
| -webkit-appearance: none; | |||
| border-radius: 0px; | |||
| padding: 12px 5px 12px 15px; | |||
| color: $light_gray; | |||
| height: 47px; | |||
| caret-color: $cursor; | |||
| &:-webkit-autofill { | |||
| box-shadow: 0 0 0px 1000px $bg inset !important; | |||
| -webkit-text-fill-color: $cursor !important; | |||
| } | |||
| } | |||
| } | |||
| .el-form-item { | |||
| border: 1px solid rgba(255, 255, 255, 0.1); | |||
| background: rgba(0, 0, 0, 0.1); | |||
| border-radius: 5px; | |||
| color: #454545; | |||
| } | |||
| } | |||
| </style> | |||
| <style lang="scss" scoped> | |||
| $bg:#2d3a4b; | |||
| $dark_gray:#889aa4; | |||
| $light_gray:#eee; | |||
| .login-container { | |||
| min-height: 100%; | |||
| width: 100%; | |||
| background-color: $bg; | |||
| overflow: hidden; | |||
| .login-form { | |||
| position: relative; | |||
| width: 520px; | |||
| max-width: 100%; | |||
| padding: 160px 35px 0; | |||
| margin: 0 auto; | |||
| overflow: hidden; | |||
| } | |||
| .tips { | |||
| font-size: 14px; | |||
| color: #fff; | |||
| margin-bottom: 10px; | |||
| span { | |||
| &:first-of-type { | |||
| margin-right: 16px; | |||
| } | |||
| } | |||
| } | |||
| .svg-container { | |||
| padding: 6px 5px 6px 15px; | |||
| color: $dark_gray; | |||
| vertical-align: middle; | |||
| width: 30px; | |||
| display: inline-block; | |||
| } | |||
| .title-container { | |||
| position: relative; | |||
| .title { | |||
| font-size: 26px; | |||
| color: $light_gray; | |||
| margin: 0px auto 40px auto; | |||
| text-align: center; | |||
| font-weight: bold; | |||
| } | |||
| } | |||
| .show-pwd { | |||
| position: absolute; | |||
| right: 10px; | |||
| top: 7px; | |||
| font-size: 16px; | |||
| color: $dark_gray; | |||
| cursor: pointer; | |||
| user-select: none; | |||
| } | |||
| .captcha-input { | |||
| width: calc(100% - 188px); | |||
| vertical-align: middle; | |||
| } | |||
| .footer { | |||
| width: 100%; | |||
| position: fixed; | |||
| bottom: 10px; | |||
| left: calc((100% - 220px) / 2); | |||
| font-size: 14px; | |||
| color: #c1c1c1; | |||
| a { | |||
| display: block; | |||
| margin-bottom: 2px; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,663 @@ | |||
| <template> | |||
| <div class="app-container"> | |||
| <public-header | |||
| :is-show="true" | |||
| :is-hide="false" | |||
| placeholder-text="请输入视频名称" | |||
| button-text="上传视频" | |||
| @handleClickInput="handleClickInput" | |||
| @handleClickCreate="handleClickCreate" | |||
| @handleOrderTopClick="handleOrderTopClick" | |||
| @handleClosePublicClick="handleClosePublicClick" | |||
| @handleQueryVideosByState="handleQueryVideosByState" | |||
| /> | |||
| <div class="app-body"> | |||
| <public-table | |||
| ref="refTable" | |||
| v-loading="isLoading" | |||
| :table-header="tableHeader" | |||
| :table-data="videoData.list" | |||
| :is-drop="true" | |||
| :is-video="true" | |||
| :page-data="{ | |||
| pageSize: videoData['pageSize'], | |||
| currPage: videoData['currPage'], | |||
| totalPage: videoData['totalPage'], | |||
| totalCount: videoData['totalCount'] | |||
| }" | |||
| @handleRemoveClick="handleRemoveClick" | |||
| @handleUpdateClick="handleUpdateClick" | |||
| @handlePageSizeChange="handlePageSizeChange" | |||
| @handlePageChange="handlePageChange" | |||
| @handleCurrentRowClick="handleCurrentRowClick" | |||
| @handleDropClick="handleDropClick" | |||
| @handleSelectionChange="handleSelectionChange" | |||
| /> | |||
| </div> | |||
| <public-dialog | |||
| dialog-title="上传视频" | |||
| :dialog-form-visible="dialogFormVisible" | |||
| :dialog-width="700" | |||
| > | |||
| <template #content> | |||
| <el-form | |||
| ref="ruleForm" | |||
| :model="ruleForm" | |||
| :rules="rules" | |||
| size="small" | |||
| label-width="100px" | |||
| > | |||
| <el-form-item label="视频名称" prop="title"> | |||
| <el-input v-model="ruleForm.title" placeholder="视频名称" style="width: 220px;" /> | |||
| </el-form-item> | |||
| <el-form-item label="视频专题" prop="subjectId"> | |||
| <el-select v-model="ruleForm.subjectId" multiple :collapse-tags="ruleForm.subjectId.length > 2" placeholder="视频专题" style="width: 220px;"> | |||
| <el-option v-for="item in allTypes" :key="item.id" :label="item.name" :value="item.id" /> | |||
| </el-select> | |||
| </el-form-item> | |||
| <el-form-item label="视频标签" prop="label"> | |||
| <!-- <el-input v-model="ruleForm.tag" placeholder="视频标签" style="width: 220px;" /> --> | |||
| <el-tag | |||
| v-for="(tag, index) in dynamicTags" | |||
| :key="`${tag}-${index}`" | |||
| :closable="!isCreate" | |||
| :disable-transitions="false" | |||
| @close="handleClose(tag)" | |||
| > | |||
| {{ tag }} | |||
| </el-tag> | |||
| <el-autocomplete | |||
| v-if="inputVisible" | |||
| ref="saveTagInput" | |||
| v-model="inputValue" | |||
| :fetch-suggestions="querySearchAsync" | |||
| size="small" | |||
| class="input-new-tag" | |||
| placeholder="回车键新增标签" | |||
| @select="handleSelect" | |||
| @keyup.enter.native="handleInputConfirm" | |||
| /> | |||
| <!-- @blur="handleInputConfirm" --> | |||
| <el-button v-else class="button-new-tag" type="primary" size="small" @click="showInput">+ 添加标签</el-button> | |||
| </el-form-item> | |||
| <el-form-item label="更新时间" prop="createTime"> | |||
| <el-date-picker | |||
| v-model="ruleForm.createTime" | |||
| type="datetime" | |||
| placeholder="选择更新时间" | |||
| default-time="12:00:00" | |||
| value-format="yyyy-MM-dd HH:mm:ss" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="视频上传" style="width: 450px;" prop="url"> | |||
| <!-- <el-radio-group v-model="isVideoFile" @change="handleChangeRadio"> | |||
| <el-radio :label="true">视频文件</el-radio> | |||
| <el-radio :label="false">视频链接</el-radio> | |||
| </el-radio-group> | |||
| <uploader | |||
| v-if="isVideoFile" | |||
| :options="options" | |||
| class="uploader-example" | |||
| :file-status-text="statusText" | |||
| @file-added="fileAdded" | |||
| @file-progress="onFileProgress" | |||
| @file-success="onFileSuccess" | |||
| @file-error="onFileError" | |||
| > | |||
| <uploader-unsupport /> | |||
| <uploader-btn :single="true">选择视频文件</uploader-btn> | |||
| <uploader-list ref="uploader_list" /> | |||
| </uploader> --> | |||
| <!-- <el-input v-else v-model="ruleForm.url" /> --> | |||
| <el-input v-model="ruleForm.url" /> | |||
| <!-- <video v-if="ruleForm.url && isVideoFile" :src="ruleForm.url" style="width: 100%;" /> --> | |||
| </el-form-item> | |||
| <el-form-item label="描述" prop="disc"> | |||
| <el-input v-model="ruleForm.disc" type="textarea" :rows="4" placeholder="描述" /> | |||
| </el-form-item> | |||
| <el-form-item label="发布" prop="state"> | |||
| <el-switch | |||
| v-model="ruleForm.state" | |||
| :active-value="0" | |||
| :inactive-value="1" | |||
| active-color="#13ce66" | |||
| /> | |||
| </el-form-item> | |||
| </el-form> | |||
| </template> | |||
| <template #footer> | |||
| <el-button @click="handleCloseDialog">取 消</el-button> | |||
| <el-button type="primary" @click="handleSubmitClick">确 定</el-button> | |||
| </template> | |||
| </public-dialog> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import PublicHeader from '@/layout/components/PublicHeader' | |||
| import PublicTable from '@/layout/components/PublicTable' | |||
| import PublicDialog from '@/layout/components/PublicDialog' | |||
| import { Debounce } from '@/utils/index' | |||
| import { mapGetters, mapActions } from 'vuex' | |||
| import SparkMD5 from 'spark-md5' | |||
| export default { | |||
| components: { | |||
| PublicHeader, | |||
| PublicTable, | |||
| PublicDialog | |||
| }, | |||
| data() { | |||
| const checkName = (rule, value, callback) => { | |||
| if (!value) { | |||
| return callback(new Error('请输入视频名称')) | |||
| } | |||
| callback() | |||
| } | |||
| return { | |||
| dialogFormVisible: false, | |||
| isLoading: false, | |||
| currentRowId: '', | |||
| isCreate: false, | |||
| pageSize: 10, | |||
| currentPage: 1, | |||
| isVideoFile: true, | |||
| inputVisible: false, | |||
| inputValue: '', | |||
| dynamicTags: [], | |||
| isSelect: false, | |||
| typeId: '', | |||
| selectVideo: '', | |||
| searchState: '', | |||
| searchArg: '', | |||
| ruleForm: { | |||
| url: '', | |||
| subjectId: [], | |||
| disc: '', | |||
| title: '', | |||
| label: '', | |||
| state: 0, | |||
| createTime: '' | |||
| }, | |||
| rules: { | |||
| title: [ | |||
| { required: true, validator: checkName, trigger: 'blur' } | |||
| ], | |||
| subjectId: [ | |||
| { required: true, message: '请选择视频专题', trigger: 'blur' } | |||
| ], | |||
| url: [ | |||
| { required: true, message: '请上传视频文件或视频链接', trigger: 'blur' } | |||
| ], | |||
| disc: [ | |||
| { max: 200, message: '最多不能超过200个字符!', trigger: 'blur' } | |||
| ] | |||
| }, | |||
| statusText: { | |||
| success: '上传成功', | |||
| error: '上传失败', | |||
| uploading: '上传中', | |||
| paused: '暂停', | |||
| waiting: '等待中' | |||
| }, | |||
| tableHeader: [ | |||
| { label: '视频名称', width: '200', prop: 'title' }, | |||
| { label: '视频专题', width: '240', prop: 'subjectId', formatter: (row) => { | |||
| return this.allTypes && this.allTypes.filter(item => row.subjectId.includes(item.id)).map(el => ( | |||
| <el-tag type='success' style='margin: 0 5px 2px 0;'> | |||
| { el.name } | |||
| </el-tag> | |||
| )) | |||
| } }, | |||
| { label: '视频标签', width: '240', prop: 'label' }, | |||
| { label: '更新时间', width: '240', prop: 'createTime' }, | |||
| { label: '发布状态', width: '120', prop: 'state', formatter: (row) => { | |||
| return row.state === 0 ? '已发布' : '未发布' | |||
| } }, | |||
| { label: '描述', width: '', prop: 'disc' } | |||
| ] | |||
| } | |||
| }, | |||
| computed: { | |||
| ...mapGetters([ | |||
| 'allTypes', | |||
| 'videoData', | |||
| 'videoMsg' | |||
| ]) | |||
| }, | |||
| created() { | |||
| this.loadMenuData() | |||
| }, | |||
| methods: { | |||
| ...mapActions({ | |||
| getTypesAll: 'types/fetchTypesAll', | |||
| getVideos: 'videos/fetchVideoList', | |||
| getVideoMsg: 'videos/fetchVideoMsg', | |||
| saveVideos: 'videos/saveVideos', | |||
| removeVideos: 'videos/removeVideos', | |||
| updateVideos: 'videos/updateVideos', | |||
| publicVideos: 'videos/publicVideos', | |||
| privateVideos: 'videos/privateVideos', | |||
| updateVideoByOrder: 'videos/updateOrderVideos', | |||
| clearVideoData: 'videos/clearVideoData', | |||
| saveTags: 'tags/saveTags', | |||
| removeTags: 'tags/removeTags', | |||
| getTags: 'tags/fetchTagList' | |||
| }), | |||
| loadMenuData() { | |||
| this.getTypesAll() | |||
| this.getVideos() | |||
| }, | |||
| // 格式化视频类型 | |||
| formatterVideoType(videoId) { | |||
| return this.allTypes && this.allTypes.filter(item => item.id === videoId).map(el => el.name).join() | |||
| }, | |||
| // 打开弹窗 | |||
| handleOpenDialog: Debounce(function(option) { | |||
| this.dialogFormVisible = true | |||
| this.isCreate = option | |||
| this.dialogTitle = option ? '上传视频' : '编辑视频' | |||
| }), | |||
| // 关闭弹窗 | |||
| handleCloseDialog() { | |||
| this.dialogFormVisible = false | |||
| this.$nextTick(() => { | |||
| this.$refs['ruleForm'].resetFields() | |||
| this.ruleForm['title'] = '' | |||
| this.ruleForm['disc'] = '' | |||
| this.ruleForm['subjectId'] = [] | |||
| this.ruleForm['url'] = '' | |||
| this.ruleForm['label'] = '' | |||
| this.ruleForm['state'] = 0 | |||
| this.ruleForm['createTime'] = '' | |||
| this.dynamicTags = [] | |||
| }) | |||
| }, | |||
| // 查询 | |||
| handleClickInput(option) { | |||
| this.searchArg = option | |||
| const options = { | |||
| page: this.currentPage, | |||
| limit: this.pageSize | |||
| } | |||
| if (option['inputText']) options['name'] = option['inputText'] | |||
| if (option['typeValue']) { | |||
| options['subjectId'] = option['typeValue'] | |||
| this.typeId = option['typeValue'] | |||
| } | |||
| if (option['videoStateValue']) options['state'] = option['videoStateValue'] | |||
| if (option['dateValue']) { | |||
| options['startTime'] = option['dateValue'][0] | |||
| options['endTime'] = option['dateValue'][1] | |||
| } | |||
| this.isLoading = true | |||
| this.clearVideoData() | |||
| this.getVideos(options).then(res => { | |||
| if (res.code === 0) { | |||
| this.isLoading = false | |||
| } | |||
| }) | |||
| }, | |||
| // 新增 | |||
| handleClickCreate(option) { | |||
| this.handleOpenDialog(true) | |||
| }, | |||
| // 删除 | |||
| handleRemoveClick: Debounce(function(option) { | |||
| console.log(option, '删除') | |||
| this.$confirm('此操作将永久删除该视频, 是否继续?', '删除视频', { | |||
| confirmButtonText: '确定', | |||
| cancelButtonText: '取消', | |||
| type: 'warning' | |||
| }).then(() => { | |||
| this.removeVideos([option.id]).then(res => { | |||
| this.$message({ | |||
| type: 'success', | |||
| message: '删除成功!' | |||
| }) | |||
| this.getVideos({ page: this.currentPage, limit: this.pageSize }) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '删除失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| type: 'info', | |||
| message: '已取消删除!' | |||
| }) | |||
| }) | |||
| }), | |||
| // 编辑 | |||
| handleUpdateClick: Debounce(function(option) { | |||
| this.getVideoMsg(option.id).then(res => { | |||
| this.$nextTick(() => { | |||
| this.ruleForm['title'] = this.videoMsg['title'] | |||
| this.ruleForm['disc'] = this.videoMsg['disc'] | |||
| this.ruleForm['subjectId'] = this.videoMsg['subjectId'].split(',').map(el => Number(el)) | |||
| this.ruleForm['url'] = this.videoMsg['url'] | |||
| this.ruleForm['state'] = this.videoMsg['state'] | |||
| this.ruleForm['createTime'] = this.videoMsg['createTime'] | |||
| this.dynamicTags = [...this.videoMsg['label']] | |||
| }) | |||
| this.handleOpenDialog(false) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '获取视频信息失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| }), | |||
| // 表单提交 | |||
| handleSubmitClick() { | |||
| this.ruleForm['label'] = this.dynamicTags.join(',') | |||
| this.ruleForm['subjectId'] = this.ruleForm['subjectId'].join(',') | |||
| this.$refs['ruleForm'].validate((valid) => { | |||
| if (valid) { | |||
| if (this.isCreate) { | |||
| this.saveVideos(this.ruleForm).then(res => { | |||
| this.$message({ | |||
| message: '创建成功!', | |||
| type: 'success' | |||
| }) | |||
| this.getVideos({ page: this.currentPage, limit: this.pageSize }) | |||
| this.handleCloseDialog() | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '创建失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| } else { | |||
| this.updateVideos({ ...this.ruleForm, id: this.videoMsg['id'] }).then(res => { | |||
| this.$message({ | |||
| message: '修改成功!', | |||
| type: 'success' | |||
| }) | |||
| this.getVideos({ page: this.currentPage, limit: this.pageSize }) | |||
| this.handleCloseDialog() | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '修改失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| } | |||
| } else { | |||
| console.log('error submit!!') | |||
| return false | |||
| } | |||
| }) | |||
| }, | |||
| handlePageChange(page) { | |||
| this.currentPage = page | |||
| this.getVideos({ page: page, limit: this.pageSize }) | |||
| }, | |||
| handlePageSizeChange(size) { | |||
| this.pageSize = size | |||
| this.getVideos({ page: this.currentPage, limit: size }) | |||
| }, | |||
| handleChangeRadio(val) { | |||
| this.isVideoFile = val | |||
| }, | |||
| // 上传单个文件 | |||
| fileAdded(file, event) { | |||
| if (/video/gi.test(file.fileType)) { | |||
| this.computeMD5(file) // 生成MD5 | |||
| } else { | |||
| this.$message({ message: '您上传的文件类型不正确,请上传视频文件', type: 'error' }) | |||
| return false | |||
| } | |||
| }, | |||
| // 计算MD5值 | |||
| computeMD5(file) { | |||
| const fileReader = new FileReader() | |||
| const time = new Date().getTime() | |||
| let md5 = '' | |||
| file.pause() | |||
| fileReader.readAsArrayBuffer(file.file) | |||
| fileReader.onload = (e) => { | |||
| if (file.size !== e.target.result.byteLength) { | |||
| this.error('Browser reported success but could not read the file until the end.') | |||
| console.log('文件读取失败') | |||
| return false | |||
| } | |||
| md5 = SparkMD5.ArrayBuffer.hash(e.target.result, false) | |||
| console.log(`MD5计算完毕:${file.id} ${file.name} MD5:${md5} 用时:${new Date().getTime() - time} ms`) | |||
| file.uniqueIdentifier = md5 | |||
| if (md5 !== '') { | |||
| file.resume() | |||
| } | |||
| // 添加额外的参数 | |||
| // this.uploader.opts.query = { | |||
| // ...that.params | |||
| // } | |||
| } | |||
| fileReader.onerror = () => { | |||
| this.error('generater md5 时FileReader异步读取文件出错了,FileReader onerror was triggered, maybe the browser aborted due to high memory usage.') | |||
| return false | |||
| } | |||
| }, | |||
| // 上传进度 | |||
| onFileProgress(rootFile, file, chunk) { | |||
| console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`) | |||
| }, | |||
| // 上传成功 | |||
| onFileSuccess(rootFile, file, response, chunk) { // 内部自动调用 | |||
| const res = JSON.parse(response) | |||
| console.log(res, '视频上传结果') | |||
| this.$message({ message: res.message, type: 'error' }) | |||
| if (res.code === 0) { | |||
| this.ruleForm['url'] = res.url | |||
| this.$message({ message: '上传成功!', type: 'success' }) | |||
| file.cancel() | |||
| } else { | |||
| this.$message({ message: res.message, type: 'error' }) | |||
| } | |||
| }, | |||
| // 上传失败 | |||
| onFileError(rootFile, file, response, chunk) { | |||
| this.$message({ | |||
| message: response, | |||
| type: 'error' | |||
| }) | |||
| }, | |||
| showInput() { | |||
| this.inputVisible = true | |||
| this.$nextTick(_ => { | |||
| this.$refs.saveTagInput.$refs.input.focus() | |||
| }) | |||
| }, | |||
| // 新增标签 | |||
| handleInputConfirm() { | |||
| const inputValue = this.inputValue | |||
| if (inputValue) { | |||
| if (inputValue.length > 6) { | |||
| this.$message({ | |||
| message: '标签最多6个字符,请勿超出长度~~', | |||
| type: 'warning' | |||
| }) | |||
| return | |||
| } | |||
| this.getTags({ name: inputValue, page: this.currentPage, limit: this.pageSize }).then(res => { | |||
| this.saveTags({ name: inputValue }).then(res => { | |||
| this.dynamicTags.push(inputValue) | |||
| this.$message({ | |||
| message: '新增标签成功!', | |||
| type: 'success' | |||
| }) | |||
| }) | |||
| }) | |||
| } | |||
| this.inputVisible = false | |||
| this.inputValue = '' | |||
| }, | |||
| // 选择标签 | |||
| handleSelect(item) { | |||
| if (this.dynamicTags.length > 3) { | |||
| this.$message({ | |||
| message: '最多可选择4个标签~~', | |||
| type: 'warning' | |||
| }) | |||
| this.inputVisible = false | |||
| this.inputValue = '' | |||
| return | |||
| } | |||
| if (this.dynamicTags.includes(item['value'])) { | |||
| this.$message({ | |||
| message: '标签已存在,请勿重复选择~~', | |||
| type: 'warning' | |||
| }) | |||
| this.inputVisible = false | |||
| this.inputValue = '' | |||
| return | |||
| } | |||
| this.dynamicTags.push(item['value']) | |||
| this.inputVisible = false | |||
| this.inputValue = '' | |||
| }, | |||
| // 删除标签 | |||
| handleClose(tag) { | |||
| this.dynamicTags.splice(this.dynamicTags.indexOf(tag), 1) | |||
| }, | |||
| // 获取标签 | |||
| querySearchAsync(queryString, cb) { | |||
| this.getTags({ name: queryString, page: this.currentPage, limit: 50 }).then(res => { | |||
| const data = res.list.map(item => ({ id: item['id'], value: item['name'] })) | |||
| cb(data) | |||
| }) | |||
| }, | |||
| handleDropClick(option) { | |||
| this.isLoading = true | |||
| if (this.typeId) option['subjectId'] = this.typeId | |||
| this.updateVideoByOrder(option).then(res => { | |||
| if (res.msg === 'success') { | |||
| this.handleClickInput(this.searchArg) | |||
| this.isLoading = false | |||
| } | |||
| }) | |||
| }, | |||
| handleCurrentRowClick(id) { | |||
| if (this.currentRowId === id) { | |||
| this.$refs.refTable.$children[0].setCurrentRow() | |||
| this.currentRowId = '' | |||
| } else { | |||
| this.currentRowId = id | |||
| } | |||
| }, | |||
| handleOrderTopClick() { | |||
| if (this.currentRowId) { | |||
| this.isLoading = true | |||
| const options = { | |||
| dragId: this.currentRowId, | |||
| objectId: 0 | |||
| } | |||
| if (this.typeId) options['subjectId'] = this.typeId | |||
| this.updateVideoByOrder(options).then(res => { | |||
| if (res.msg === 'success') { | |||
| this.handleClickInput(this.searchArg) | |||
| this.isLoading = false | |||
| } | |||
| }) | |||
| } else { | |||
| this.$message({ | |||
| message: '请先点击要置顶的视频!', | |||
| type: 'warning' | |||
| }) | |||
| } | |||
| }, | |||
| handleSelectionChange(val) { | |||
| this.selectVideo = val.map(el => el.id) | |||
| }, | |||
| handleClosePublicClick() { | |||
| if (!this.selectVideo) { | |||
| this.$message({ | |||
| message: '请先选择需要取消发布的视频~~', | |||
| type: 'warning' | |||
| }) | |||
| } else { | |||
| this.isLoading = true | |||
| if (this.searchState === '0') { | |||
| this.privateVideos(this.selectVideo).then(res => { | |||
| if (res.code === 0) { | |||
| this.isLoading = false | |||
| this.$message({ | |||
| type: 'success', | |||
| message: '批量取消发布成功!' | |||
| }) | |||
| this.handleClickInput({ videoStateValue: this.searchState }) | |||
| } | |||
| }) | |||
| } else { | |||
| this.publicVideos(this.selectVideo).then(res => { | |||
| this.isLoading = false | |||
| if (res.code === 0) { | |||
| this.$message({ | |||
| type: 'success', | |||
| message: '批量发布成功!' | |||
| }) | |||
| this.handleClickInput({ videoStateValue: this.searchState }) | |||
| } | |||
| }) | |||
| } | |||
| } | |||
| }, | |||
| handleQueryVideosByState(val) { | |||
| this.searchState = val | |||
| this.clearVideoData() | |||
| this.handleClickInput({ videoStateValue: val }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped> | |||
| .app-container { | |||
| padding: 10px; | |||
| } | |||
| .uploader-example { | |||
| width: 100%; | |||
| font-size: 12px; | |||
| /* box-shadow: 0 0 10px rgba(0, 0, 0, .4); */ | |||
| } | |||
| .uploader-example .uploader-btn { | |||
| margin-right: 4px; | |||
| border: 1px solid #DCDFE6; | |||
| } | |||
| .uploader-example .uploader-list { | |||
| max-height: 440px; | |||
| overflow: auto; | |||
| overflow-x: hidden; | |||
| overflow-y: auto; | |||
| } | |||
| .el-tag { | |||
| margin-right: 10px; | |||
| } | |||
| .button-new-tag { | |||
| /* margin-left: 10px; */ | |||
| height: 32px; | |||
| line-height: 30px; | |||
| padding-top: 0; | |||
| padding-bottom: 0; | |||
| vertical-align: middle; | |||
| } | |||
| .input-new-tag { | |||
| width: 130px; | |||
| /* margin-left: 10px; */ | |||
| vertical-align: bottom; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,578 @@ | |||
| <template> | |||
| <div class="app-container"> | |||
| <public-header | |||
| :is-type="true" | |||
| :is-hide="false" | |||
| placeholder-text="请输入专题名称" | |||
| button-text="添加专题" | |||
| @handleClickInput="handleClickInput" | |||
| @handleClickCreate="handleClickCreate" | |||
| @handleSearchMediaClick="handleSearchMediaClick" | |||
| @handleOrderTopClick="handleOrderTopClick" | |||
| /> | |||
| <div class="app-body"> | |||
| <public-table | |||
| ref="refTable" | |||
| v-loading="isLoading" | |||
| :table-header="tableHeader" | |||
| :table-data="typeData.list" | |||
| :is-drop="true" | |||
| :page-data="{ | |||
| pageSize: typeData['pageSize'], | |||
| currPage: typeData['currPage'], | |||
| totalPage: typeData['totalPage'], | |||
| totalCount: typeData['totalCount'] | |||
| }" | |||
| @handleRemoveClick="handleRemoveClick" | |||
| @handleUpdateClick="handleUpdateClick" | |||
| @handleCurrentRowClick="handleCurrentRowClick" | |||
| @handlePageSizeChange="handlePageSizeChange" | |||
| @handlePageChange="handlePageChange" | |||
| @handleDropClick="handleDropClick" | |||
| /> | |||
| </div> | |||
| <public-dialog | |||
| :dialog-title="dialogTitle" | |||
| :dialog-form-visible="dialogFormVisible" | |||
| :dialog-width="700" | |||
| > | |||
| <template #content> | |||
| <el-form | |||
| ref="ruleForm" | |||
| :model="ruleForm" | |||
| :rules="rules" | |||
| size="small" | |||
| label-width="100px" | |||
| > | |||
| <el-form-item label="专题名称" prop="name"> | |||
| <el-input v-model="ruleForm.name" placeholder="专题名称" style="width: 220px;" /> | |||
| </el-form-item> | |||
| <el-form-item label="专题封面" style="width: 450px;" prop="imageUrl"> | |||
| <el-upload | |||
| class="avatar-uploader" | |||
| action="/video-admin/video-admin/sys/oss/upload" | |||
| :show-file-list="false" | |||
| :headers="{token}" | |||
| :on-success="handleAvatarSuccess" | |||
| :before-upload="beforeAvatarUpload" | |||
| > | |||
| <img v-if="ruleForm.imageUrl" :src="ruleForm.imageUrl" class="avatar"> | |||
| <i v-else class="el-icon-plus avatar-uploader-icon" /> | |||
| </el-upload> | |||
| </el-form-item> | |||
| <el-form-item label="备注" prop="disc"> | |||
| <el-input v-model="ruleForm.disc" type="textarea" :rows="4" placeholder="备注" /> | |||
| </el-form-item> | |||
| <el-form-item label="启用" prop="state"> | |||
| <el-switch | |||
| v-model="ruleForm.state" | |||
| :active-value="0" | |||
| :inactive-value="1" | |||
| active-color="#13ce66" | |||
| /> | |||
| </el-form-item> | |||
| </el-form> | |||
| </template> | |||
| <template #footer> | |||
| <el-button @click="handleCloseDialog">取 消</el-button> | |||
| <el-button type="primary" @click="handleSubmitClick">确 定</el-button> | |||
| </template> | |||
| </public-dialog> | |||
| <el-dialog | |||
| title="视频列表" | |||
| class="videoDialog" | |||
| :visible.sync="videoListVisible" | |||
| @opened="handleOpenVideoClick" | |||
| > | |||
| <el-button | |||
| type="primary" | |||
| size="small" | |||
| style="margin-bottom: 5px;" | |||
| @click="handleOrderTopVideoClick" | |||
| > | |||
| 置顶 | |||
| </el-button> | |||
| <div class="video-table"> | |||
| <el-table | |||
| v-loading="isLoading" | |||
| :data="videoData.list" | |||
| :height="tableHeight" | |||
| border | |||
| highlight-current-row | |||
| @row-click="handleCurrentRowVideoClick" | |||
| > | |||
| <template v-for="(item, index) in videoHeader"> | |||
| <el-table-column | |||
| v-if="item.prop === 'label'" | |||
| :key="index" | |||
| :prop="item.prop" | |||
| :width="item.width" | |||
| :label="item.label" | |||
| align="center" | |||
| > | |||
| <template slot-scope="scope"> | |||
| <el-tag | |||
| v-for="(tag, i) in scope.row.label" | |||
| :key="`${tag}-${i}`" | |||
| :disable-transitions="false" | |||
| style="margin: 0 5px 2px 0;" | |||
| > | |||
| {{ tag }} | |||
| </el-tag> | |||
| </template> | |||
| </el-table-column> | |||
| <el-table-column | |||
| v-else | |||
| :key="index" | |||
| :prop="item.prop" | |||
| :width="item.width" | |||
| :label="item.label" | |||
| :formatter="item.formatter" | |||
| :show-overflow-tooltip="item.prop !== 'subjectId'" | |||
| align="center" | |||
| /> | |||
| </template> | |||
| </el-table> | |||
| <el-pagination | |||
| :current-page="videoData.currPage" | |||
| :page-sizes="[10, 20, 30, 40, 50]" | |||
| :page-size="videoData.pageSize" | |||
| :total="videoData.totalCount" | |||
| layout="total, sizes, prev, pager, next, jumper" | |||
| class="app-pagination" | |||
| @size-change="handlePageSizeChangeVideo" | |||
| @current-change="handlePageChangeVideo" | |||
| /> | |||
| </div> | |||
| <!-- <public-table | |||
| :isShow="false" | |||
| :table-header="videoHeader" | |||
| :table-data="videoData.list" | |||
| :page-data="{ | |||
| pageSize: videoData['pageSize'], | |||
| currPage: videoData['currPage'], | |||
| totalPage: videoData['totalPage'], | |||
| totalCount: videoData['totalCount'] | |||
| }" | |||
| @handlePageSizeChange="handlePageSizeChangeVideo" | |||
| @handlePageChange="handlePageChangeVideo" | |||
| /> --> | |||
| </el-dialog> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import PublicHeader from '@/layout/components/PublicHeader' | |||
| import PublicTable from '@/layout/components/PublicTable' | |||
| import PublicDialog from '@/layout/components/PublicDialog' | |||
| import Sortable from 'sortablejs' | |||
| import { Debounce } from '@/utils/index' | |||
| import { mapGetters, mapActions } from 'vuex' | |||
| export default { | |||
| components: { | |||
| PublicHeader, | |||
| PublicTable, | |||
| PublicDialog | |||
| }, | |||
| data() { | |||
| const checkName = (rule, value, callback) => { | |||
| if (!value) { | |||
| return callback(new Error('请输入专题名称')) | |||
| } | |||
| callback() | |||
| } | |||
| return { | |||
| isLoading: false, | |||
| dialogFormVisible: false, | |||
| videoListVisible: false, | |||
| dialogTitle: '', | |||
| isCreate: false, | |||
| pageSize: 10, | |||
| currentPage: 1, | |||
| pageSizeVideo: 10, | |||
| currentPageVideo: 1, | |||
| tableHeight: 540, | |||
| currentRowId: '', | |||
| currentVideoRowId: '', | |||
| ruleForm: { | |||
| name: '', | |||
| disc: '', | |||
| state: 0, | |||
| imageUrl: '' | |||
| }, | |||
| imageUrl: '', | |||
| rules: { | |||
| name: [ | |||
| { required: true, validator: checkName, trigger: 'blur' } | |||
| ], | |||
| disc: [ | |||
| { max: 100, message: '最多不超过100个字符', trigger: 'blur' } | |||
| ], | |||
| imageUrl: [ | |||
| { required: true, message: '请上传封面图片', trigger: 'blur' } | |||
| ] | |||
| }, | |||
| tableHeader: [ | |||
| { label: '专题名称', width: '', prop: 'name' }, | |||
| { label: '专题封面', width: '', prop: 'imageUrl' }, | |||
| { label: '状态', width: '', prop: 'state', formatter: (row) => (row.state !== 0 ? '停用' : '正常') }, | |||
| { label: '备注', width: '', prop: 'disc' } | |||
| ], | |||
| videoHeader: [ | |||
| { label: '视频名称', width: '200', prop: 'title' }, | |||
| { label: '视频专题', width: '180', prop: 'subjectId', formatter: (row) => { | |||
| return this.typeData.list && this.typeData.list.filter(item => row.subjectId.includes(item.id)).map(el => ( | |||
| <el-tag type='success' style='margin: 0 5px 2px 0;'> | |||
| { el.name } | |||
| </el-tag> | |||
| )) | |||
| } }, | |||
| { label: '视频标签', width: '', prop: 'label' }, | |||
| { label: '更新时间', width: '160', prop: 'createTime' }, | |||
| { label: '发布状态', width: '100', prop: 'state', formatter: (row) => { | |||
| return row.state === 0 ? '已发布' : '未发布' | |||
| } }, | |||
| { label: '描述', width: '', prop: 'disc' } | |||
| ] | |||
| } | |||
| }, | |||
| computed: { | |||
| ...mapGetters([ | |||
| 'typeData', | |||
| 'videoData', | |||
| 'typeMsg', | |||
| 'token' | |||
| ]) | |||
| }, | |||
| created() { | |||
| this.loadMenuData() | |||
| }, | |||
| methods: { | |||
| ...mapActions({ | |||
| getVideos: 'videos/fetchVideoList', | |||
| clearVideoData: 'videos/clearVideoData', | |||
| getTypes: 'types/fetchTypeList', | |||
| clearTypes: 'types/clearTypeList', | |||
| getTypeMsg: 'types/fetchTypeMsg', | |||
| saveTypes: 'types/saveTypes', | |||
| removeTypes: 'types/removeTypes', | |||
| updateTypes: 'types/updateTypes', | |||
| updateTypesByOrder: 'types/updateOrderTypes', | |||
| updateVideoByOrder: 'videos/updateOrderVideos' | |||
| }), | |||
| loadMenuData() { | |||
| this.getTypes() | |||
| }, | |||
| // 打开弹窗 | |||
| handleOpenDialog: Debounce(function(option) { | |||
| this.dialogFormVisible = true | |||
| this.isCreate = option | |||
| this.dialogTitle = option ? '添加专题' : '编辑专题' | |||
| }), | |||
| // 关闭弹窗 | |||
| handleCloseDialog() { | |||
| this.dialogFormVisible = false | |||
| this.$nextTick(() => { | |||
| this.$refs['ruleForm'].resetFields() | |||
| this.ruleForm['name'] = '' | |||
| this.ruleForm['disc'] = '' | |||
| this.ruleForm['state'] = 0 | |||
| this.ruleForm['imageUrl'] = '' | |||
| }) | |||
| }, | |||
| // 查询 | |||
| handleClickInput: Debounce(function(option) { | |||
| console.log(option, '查询') | |||
| this.getTypes({ name: option['inputText'] }) | |||
| }), | |||
| // 新增 | |||
| handleClickCreate: Debounce(function(option) { | |||
| console.log(option, '添加') | |||
| this.handleOpenDialog(true) | |||
| }), | |||
| // 删除 | |||
| handleRemoveClick: Debounce(function(option) { | |||
| console.log(option, '删除') | |||
| this.$confirm('此操作将永久删除该专题, 是否继续?', '删除专题', { | |||
| confirmButtonText: '确定', | |||
| cancelButtonText: '取消', | |||
| type: 'warning' | |||
| }).then(() => { | |||
| this.removeTypes([option.id]).then(res => { | |||
| this.$message({ | |||
| type: 'success', | |||
| message: '删除成功!' | |||
| }) | |||
| this.getTypes({ page: this.currentPage, limit: this.pageSize }) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '删除失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| type: 'info', | |||
| message: '已取消删除!' | |||
| }) | |||
| }) | |||
| }), | |||
| // 修改 | |||
| handleUpdateClick: Debounce(function(option) { | |||
| console.log(option, '编辑') | |||
| this.getTypeMsg(option.id).then(() => { | |||
| this.$nextTick(() => { | |||
| this.ruleForm['name'] = this.typeMsg['name'] | |||
| this.ruleForm['disc'] = this.typeMsg['disc'] | |||
| this.ruleForm['state'] = this.typeMsg['state'] | |||
| this.ruleForm['imageUrl'] = this.typeMsg['imageUrl'] | |||
| }) | |||
| this.handleOpenDialog(false) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '获取专题信息失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| }), | |||
| // 表单提交 | |||
| handleSubmitClick() { | |||
| this.$refs['ruleForm'].validate((valid) => { | |||
| if (valid) { | |||
| if (this.isCreate) { | |||
| this.saveTypes(this.ruleForm).then(res => { | |||
| this.$message({ | |||
| message: '创建成功!', | |||
| type: 'success' | |||
| }) | |||
| this.getTypes({ page: this.currentPage, limit: this.pageSize }) | |||
| this.handleCloseDialog() | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '创建失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| } else { | |||
| this.updateTypes({ ...this.ruleForm, id: this.typeMsg['id'] }).then(res => { | |||
| this.$message({ | |||
| message: '修改成功!', | |||
| type: 'success' | |||
| }) | |||
| this.getTypes({ page: this.currentPage, limit: this.pageSize }) | |||
| this.handleCloseDialog() | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '修改失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| } | |||
| } else { | |||
| console.log('error submit!!') | |||
| return false | |||
| } | |||
| }) | |||
| }, | |||
| handlePageChange(page) { | |||
| this.currentPage = page | |||
| this.getTypes({ page: page, limit: this.pageSize }) | |||
| }, | |||
| handlePageSizeChange(size) { | |||
| this.pageSize = size | |||
| this.getTypes({ page: this.currentPage, limit: size }) | |||
| }, | |||
| handlePageChangeVideo(page) { | |||
| this.currentPageVideo = page | |||
| this.getVideos({ page: page, limit: this.pageSizeVideo, subjectId: this.currentRowId }) | |||
| }, | |||
| handlePageSizeChangeVideo(size) { | |||
| this.pageSizeVideo = size | |||
| this.getVideos({ page: this.currentPageVideo, limit: size, subjectId: this.currentRowId }) | |||
| }, | |||
| handleAvatarSuccess(res, file) { | |||
| // this.imageUrl = URL.createObjectURL(file.raw) | |||
| this.ruleForm['imageUrl'] = res['url'] | |||
| }, | |||
| beforeAvatarUpload(file) { | |||
| const type = ['image/jpeg', 'image/png'] | |||
| const isJPG = type.includes(file.type) | |||
| const isLt2M = file.size / 1024 / 1024 < 2 | |||
| if (!isJPG) { | |||
| this.$message.error('上传图片只能是 JPG 或 PNG 格式!') | |||
| } | |||
| if (!isLt2M) { | |||
| this.$message.error('上传图片大小不能超过 2MB!') | |||
| } | |||
| return isJPG && isLt2M | |||
| }, | |||
| handleSearchMediaClick() { | |||
| if (this.currentRowId) { | |||
| this.videoListVisible = true | |||
| const options = { | |||
| subjectId: this.currentRowId, | |||
| page: this.currentPageVideo, | |||
| limit: this.pageSizeVideo | |||
| } | |||
| this.clearVideoData() | |||
| this.getVideos(options) | |||
| } else { | |||
| this.$message({ | |||
| message: '请先选择要查看的视频专题!', | |||
| type: 'warning' | |||
| }) | |||
| } | |||
| }, | |||
| handleCurrentRowClick(id) { | |||
| if (this.currentRowId === id) { | |||
| this.$refs.refTable.$children[0].setCurrentRow() | |||
| this.currentRowId = '' | |||
| } else { | |||
| this.currentRowId = id | |||
| } | |||
| }, | |||
| handleCurrentRowVideoClick(video) { | |||
| const { id } = video | |||
| if (this.currentVideoRowId === id) { | |||
| this.$refs.refTable.$children[0].setCurrentRow() | |||
| this.currentVideoRowId = '' | |||
| } else { | |||
| this.currentVideoRowId = id | |||
| } | |||
| }, | |||
| // 弹窗打开,创建拖拽实例 | |||
| handleOpenVideoClick() { | |||
| this.videoRowDrop() | |||
| }, | |||
| videoRowDrop() { | |||
| const tbody = document.querySelector('.video-table .el-table__body-wrapper tbody') | |||
| const _this = this | |||
| Sortable.create(tbody, { | |||
| onEnd(ev) { | |||
| const { newIndex, oldIndex } = ev | |||
| const newData = [..._this.videoData.list] | |||
| const currRow = newData.splice(oldIndex, 1)[0] | |||
| let nextRow = '' | |||
| if (newIndex !== oldIndex) { | |||
| if (newIndex > oldIndex) { | |||
| nextRow = newData.splice(newIndex - 1, 1)[0] | |||
| } else { | |||
| nextRow = newData.splice(newIndex, 1)[0] | |||
| } | |||
| _this.isLoading = true | |||
| _this.updateVideoByOrder({ | |||
| dragId: currRow['id'], | |||
| objectId: nextRow['id'], | |||
| subjectId: _this.currentRowId | |||
| }).then(res => { | |||
| if (res.msg === 'success') { | |||
| _this.handleSearchMediaClick() | |||
| _this.isLoading = false | |||
| } | |||
| }) | |||
| } | |||
| } | |||
| }) | |||
| }, | |||
| handleDropClick(option) { | |||
| this.isLoading = true | |||
| this.clearTypes() | |||
| this.updateTypesByOrder(option).then(res => { | |||
| if (res.msg === 'success') { | |||
| this.getTypes() | |||
| this.isLoading = false | |||
| } | |||
| }) | |||
| }, | |||
| handleOrderTopClick() { | |||
| if (this.currentRowId) { | |||
| this.isLoading = true | |||
| const options = { | |||
| dragId: this.currentRowId, | |||
| objectId: 0 | |||
| } | |||
| this.updateTypesByOrder(options).then(res => { | |||
| if (res.msg === 'success') { | |||
| this.getTypes() | |||
| this.isLoading = false | |||
| } | |||
| }) | |||
| } else { | |||
| this.$message({ | |||
| message: '请先点击要置顶的专题!', | |||
| type: 'warning' | |||
| }) | |||
| } | |||
| }, | |||
| handleOrderTopVideoClick() { | |||
| if (this.currentVideoRowId) { | |||
| this.isLoading = true | |||
| const options = { | |||
| dragId: this.currentVideoRowId, | |||
| objectId: 0, | |||
| subjectId: this.currentRowId | |||
| } | |||
| this.updateVideoByOrder(options).then(res => { | |||
| if (res.msg === 'success') { | |||
| this.handleSearchMediaClick() | |||
| this.isLoading = false | |||
| } | |||
| }) | |||
| } else { | |||
| this.$message({ | |||
| message: '请先选择要置顶的视频!', | |||
| type: 'warning' | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scope> | |||
| .app-container { | |||
| padding: 10px; | |||
| } | |||
| .avatar-uploader .el-upload { | |||
| border: 1px dashed #d9d9d9; | |||
| border-radius: 6px; | |||
| cursor: pointer; | |||
| position: relative; | |||
| overflow: hidden; | |||
| } | |||
| .avatar-uploader .el-upload:hover { | |||
| border-color: #409EFF; | |||
| } | |||
| .avatar-uploader-icon { | |||
| font-size: 28px; | |||
| color: #8c939d; | |||
| width: 100px; | |||
| height: 100px; | |||
| line-height: 100px; | |||
| text-align: center; | |||
| } | |||
| .avatar { | |||
| width: 100px; | |||
| height: 100px; | |||
| display: block; | |||
| } | |||
| .videoDialog .el-dialog__body { | |||
| padding: 10px 20px; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,278 @@ | |||
| <template> | |||
| <div class="app-container"> | |||
| <public-header | |||
| placeholder-text="请输入角色名" | |||
| button-text="添加角色" | |||
| @handleClickInput="handleClickInput" | |||
| @handleClickCreate="handleClickCreate" | |||
| /> | |||
| <div class="app-body"> | |||
| <public-table | |||
| :table-header="tableHeader" | |||
| :table-data="roleData.list" | |||
| :page-data="{ | |||
| pageSize: roleData['pageSize'], | |||
| currPage: roleData['currPage'], | |||
| totalPage: roleData['totalPage'], | |||
| totalCount: roleData['totalCount'] | |||
| }" | |||
| @handleRemoveClick="handleRemoveClick" | |||
| @handleUpdateClick="handleUpdateClick" | |||
| @handlePageSizeChange="handlePageSizeChange" | |||
| @handlePageChange="handlePageChange" | |||
| /> | |||
| </div> | |||
| <public-dialog | |||
| :dialog-title="dialogTitle" | |||
| :dialog-form-visible="dialogFormVisible" | |||
| :dialog-width="700" | |||
| > | |||
| <template #content> | |||
| <el-form | |||
| ref="ruleForm" | |||
| :model="ruleForm" | |||
| :rules="rules" | |||
| size="small" | |||
| label-width="100px" | |||
| > | |||
| <el-form-item label="角色名" prop="roleName"> | |||
| <el-input v-model="ruleForm.roleName" placeholder="角色名称" style="width: 220px;" /> | |||
| </el-form-item> | |||
| <el-form-item label="备注" prop="remark"> | |||
| <el-input v-model="ruleForm.remark" type="textarea" :rows="4" placeholder="备注" /> | |||
| </el-form-item> | |||
| <el-form-item label="功能权限" prop="menuIdList"> | |||
| <el-tree | |||
| :data="menuData" | |||
| show-checkbox | |||
| node-key="menuId" | |||
| :default-expanded-keys="[1]" | |||
| :default-checked-keys="ruleForm.menuIdList" | |||
| :props="defaultProps" | |||
| style="max-height: 350px; overflow: auto;" | |||
| @check="handleNodeClick" | |||
| /> | |||
| </el-form-item> | |||
| </el-form> | |||
| </template> | |||
| <template #footer> | |||
| <el-button @click="handleCloseDialog">取 消</el-button> | |||
| <el-button type="primary" @click="handleSubmitClick">确 定</el-button> | |||
| </template> | |||
| </public-dialog> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import PublicHeader from '@/layout/components/PublicHeader' | |||
| import PublicTable from '@/layout/components/PublicTable' | |||
| import PublicDialog from '@/layout/components/PublicDialog' | |||
| import { Debounce } from '@/utils/index' | |||
| import { mapGetters, mapActions } from 'vuex' | |||
| export default { | |||
| components: { | |||
| PublicHeader, | |||
| PublicTable, | |||
| PublicDialog | |||
| }, | |||
| data() { | |||
| const checkName = (rule, value, callback) => { | |||
| if (!value) { | |||
| return callback(new Error('请输入角色名称')) | |||
| } | |||
| callback() | |||
| } | |||
| return { | |||
| dialogFormVisible: false, | |||
| dialogTitle: '', | |||
| isCreate: false, | |||
| pageSize: 10, | |||
| currentPage: 1, | |||
| ruleForm: { | |||
| roleName: '', | |||
| remark: '', | |||
| menuIdList: [] | |||
| }, | |||
| rules: { | |||
| roleName: [ | |||
| { required: true, validator: checkName, trigger: 'blur' } | |||
| ], | |||
| remark: [ | |||
| { max: 100, message: '最多不超过100个字符', trigger: 'blur' } | |||
| ], | |||
| menuIdList: [ | |||
| { type: 'array', required: true, message: '至少选择一个权限', trigger: 'blur' } | |||
| ] | |||
| }, | |||
| defaultProps: { | |||
| children: 'children', | |||
| label: 'name' | |||
| }, | |||
| tableHeader: [ | |||
| { label: '角色名', width: '', prop: 'roleName' }, | |||
| { label: '创建时间', width: '', prop: 'createTime' }, | |||
| { label: '备注', width: '', prop: 'remark' } | |||
| ] | |||
| } | |||
| }, | |||
| computed: { | |||
| ...mapGetters([ | |||
| 'menuData', | |||
| 'roleData', | |||
| 'roleMsg' | |||
| ]) | |||
| }, | |||
| created() { | |||
| this.loadMenuData() | |||
| }, | |||
| methods: { | |||
| ...mapActions({ | |||
| getMenu: 'roles/fetchMenuList', | |||
| getRoles: 'roles/fetchRoleList', | |||
| getRoleMsg: 'roles/fetchRoleMsg', | |||
| saveRoles: 'roles/saveRoles', | |||
| removeRoles: 'roles/removeRoles', | |||
| updateRoles: 'roles/updateRoles' | |||
| }), | |||
| loadMenuData() { | |||
| this.getRoles() | |||
| if (this.menuData.length < 1) { | |||
| this.getMenu() | |||
| } | |||
| }, | |||
| // 打开弹窗 | |||
| handleOpenDialog: Debounce(function(option) { | |||
| this.dialogFormVisible = true | |||
| this.isCreate = option | |||
| this.dialogTitle = option ? '添加角色' : '编辑角色' | |||
| }), | |||
| // 关闭弹窗 | |||
| handleCloseDialog() { | |||
| this.dialogFormVisible = false | |||
| this.$nextTick(() => { | |||
| this.$refs['ruleForm'].resetFields() | |||
| this.ruleForm['menuIdList'] = [] | |||
| }) | |||
| }, | |||
| // 查询 | |||
| handleClickInput: Debounce(function(option) { | |||
| console.log(option, '查询') | |||
| this.getRoles({ roleName: option['inputText'] }) | |||
| }), | |||
| // 新增 | |||
| handleClickCreate: Debounce(function(option) { | |||
| this.handleOpenDialog(true) | |||
| }), | |||
| // 删除 | |||
| handleRemoveClick: Debounce(function(option) { | |||
| console.log(option, '删除') | |||
| this.$confirm('此操作将永久删除该角色, 是否继续?', '删除角色', { | |||
| confirmButtonText: '确定', | |||
| cancelButtonText: '取消', | |||
| type: 'warning' | |||
| }).then(() => { | |||
| this.removeRoles([option.roleId]).then(res => { | |||
| this.$message({ | |||
| type: 'success', | |||
| message: '删除成功!' | |||
| }) | |||
| this.getRoles({ page: this.currentPage, limit: this.pageSize }) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '删除失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| type: 'info', | |||
| message: '已取消删除!' | |||
| }) | |||
| }) | |||
| }), | |||
| // 编辑 | |||
| handleUpdateClick: Debounce(function(option) { | |||
| this.getRoleMsg(option.roleId).then(res => { | |||
| this.$nextTick(() => { | |||
| this.ruleForm['roleName'] = this.roleMsg['roleName'] | |||
| this.ruleForm['remark'] = this.roleMsg['remark'] | |||
| this.ruleForm['menuIdList'] = this.roleMsg['menuIdList'] | |||
| }) | |||
| this.handleOpenDialog(false) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '获取角色信息失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| }), | |||
| // 提交表单 | |||
| handleSubmitClick() { | |||
| this.$refs['ruleForm'].validate((valid) => { | |||
| if (valid) { | |||
| if (this.isCreate) { | |||
| this.saveRoles(this.ruleForm).then(res => { | |||
| this.$message({ | |||
| message: '创建成功!', | |||
| type: 'success' | |||
| }) | |||
| this.getRoles({ page: this.currentPage, limit: this.pageSize }) | |||
| this.handleCloseDialog() | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '创建失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| } else { | |||
| this.updateRoles({ ...this.ruleForm, roleId: this.roleMsg['roleId'] }).then(res => { | |||
| this.$message({ | |||
| message: '修改成功!', | |||
| type: 'success' | |||
| }) | |||
| this.getRoles({ page: this.currentPage, limit: this.pageSize }) | |||
| this.handleCloseDialog() | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '修改失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| } | |||
| } else { | |||
| console.log('error submit!!') | |||
| return false | |||
| } | |||
| }) | |||
| }, | |||
| // 权限选择 | |||
| handleNodeClick(data, { checkedKeys }) { | |||
| this.ruleForm['menuIdList'] = checkedKeys | |||
| }, | |||
| handlePageChange(page) { | |||
| this.currentPage = page | |||
| this.getRoles({ page: page, limit: this.pageSize }) | |||
| }, | |||
| handlePageSizeChange(size) { | |||
| this.pageSize = size | |||
| this.getRoles({ page: this.currentPage, limit: size }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scope> | |||
| .app-container { | |||
| padding: 10px; | |||
| } | |||
| .el-message { | |||
| min-width: 120px; | |||
| } | |||
| /* .el-checkbox { | |||
| width: 160px; | |||
| } */ | |||
| </style> | |||
| @@ -0,0 +1,269 @@ | |||
| <template> | |||
| <div class="app-container"> | |||
| <public-header | |||
| placeholder-text="请输入标签名称" | |||
| button-text="添加标签" | |||
| @handleClickInput="handleClickInput" | |||
| @handleClickCreate="handleClickCreate" | |||
| /> | |||
| <div class="app-body"> | |||
| <public-table | |||
| :table-header="tableHeader" | |||
| :table-data="tagData.list" | |||
| :page-data="{ | |||
| pageSize: tagData['pageSize'], | |||
| currPage: tagData['currPage'], | |||
| totalPage: tagData['totalPage'], | |||
| totalCount: tagData['totalCount'] | |||
| }" | |||
| @handleRemoveClick="handleRemoveClick" | |||
| @handleUpdateClick="handleUpdateClick" | |||
| @handlePageSizeChange="handlePageSizeChange" | |||
| @handlePageChange="handlePageChange" | |||
| /> | |||
| </div> | |||
| <public-dialog | |||
| :dialog-title="dialogTitle" | |||
| :dialog-form-visible="dialogFormVisible" | |||
| > | |||
| <template #content> | |||
| <el-form | |||
| ref="ruleForm" | |||
| :model="ruleForm" | |||
| :rules="rules" | |||
| size="small" | |||
| label-width="100px" | |||
| > | |||
| <el-form-item label="标签名称" prop="name"> | |||
| <el-input v-model="ruleForm.name" placeholder="标签名称" style="width: 220px;" /> | |||
| </el-form-item> | |||
| </el-form> | |||
| </template> | |||
| <template #footer> | |||
| <el-button @click="handleCloseDialog">取 消</el-button> | |||
| <el-button type="primary" @click="handleSubmitClick">确 定</el-button> | |||
| </template> | |||
| </public-dialog> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import PublicHeader from '@/layout/components/PublicHeader' | |||
| import PublicTable from '@/layout/components/PublicTable' | |||
| import PublicDialog from '@/layout/components/PublicDialog' | |||
| import { Debounce } from '@/utils/index' | |||
| import { mapGetters, mapActions } from 'vuex' | |||
| export default { | |||
| components: { | |||
| PublicHeader, | |||
| PublicTable, | |||
| PublicDialog | |||
| }, | |||
| data() { | |||
| const checkName = (rule, value, callback) => { | |||
| if (!value) { | |||
| return callback(new Error('请输入标签名称')) | |||
| } | |||
| if (value.length > 6) { | |||
| return callback(new Error('最多输入6个字符~~')) | |||
| } | |||
| callback() | |||
| } | |||
| return { | |||
| dialogFormVisible: false, | |||
| dialogTitle: '', | |||
| isCreate: false, | |||
| pageSize: 10, | |||
| currentPage: 1, | |||
| ruleForm: { | |||
| name: '' | |||
| }, | |||
| rules: { | |||
| name: [ | |||
| { required: true, validator: checkName, trigger: 'blur' } | |||
| ] | |||
| }, | |||
| tableHeader: [ | |||
| { label: '标签名称', width: '', prop: 'name' }, | |||
| { label: '创建时间', width: '', prop: 'createTime' } | |||
| ] | |||
| } | |||
| }, | |||
| computed: { | |||
| ...mapGetters([ | |||
| 'tagData', | |||
| 'tagMsg', | |||
| 'token' | |||
| ]) | |||
| }, | |||
| created() { | |||
| this.loadMenuData() | |||
| }, | |||
| methods: { | |||
| ...mapActions({ | |||
| getTags: 'tags/fetchTagList', | |||
| getTagMsg: 'tags/fetchTagMsg', | |||
| saveTags: 'tags/saveTags', | |||
| removeTags: 'tags/removeTags', | |||
| updateTags: 'tags/updateTags' | |||
| }), | |||
| loadMenuData() { | |||
| this.getTags() | |||
| }, | |||
| // 打开弹窗 | |||
| handleOpenDialog: Debounce(function(option) { | |||
| this.dialogFormVisible = true | |||
| this.isCreate = option | |||
| this.dialogTitle = option ? '添加标签' : '编辑标签' | |||
| }), | |||
| // 关闭弹窗 | |||
| handleCloseDialog() { | |||
| this.dialogFormVisible = false | |||
| this.$nextTick(() => { | |||
| this.$refs['ruleForm'].resetFields() | |||
| this.ruleForm['name'] = '' | |||
| }) | |||
| }, | |||
| // 查询 | |||
| handleClickInput: Debounce(function(option) { | |||
| console.log(option, '查询') | |||
| this.getTags({ name: option['inputText'], page: this.currentPage, limit: this.pageSize }) | |||
| }), | |||
| // 新增 | |||
| handleClickCreate: Debounce(function(option) { | |||
| console.log(option, '添加') | |||
| this.handleOpenDialog(true) | |||
| }), | |||
| // 删除 | |||
| handleRemoveClick: Debounce(function(option) { | |||
| console.log(option, '删除') | |||
| this.$confirm('此操作将永久删除该标签, 是否继续?', '删除标签', { | |||
| confirmButtonText: '确定', | |||
| cancelButtonText: '取消', | |||
| type: 'warning' | |||
| }).then(() => { | |||
| this.removeTags([option.id]).then(res => { | |||
| this.$message({ | |||
| type: 'success', | |||
| message: '删除成功!' | |||
| }) | |||
| this.getTags({ page: this.currentPage, limit: this.pageSize }) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '删除失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| type: 'info', | |||
| message: '已取消删除!' | |||
| }) | |||
| }) | |||
| }), | |||
| // 修改 | |||
| handleUpdateClick: Debounce(function(option) { | |||
| console.log(option, '编辑') | |||
| this.getTagMsg(option.id).then(() => { | |||
| this.$nextTick(() => { | |||
| console.log(this.tagMsg) | |||
| this.ruleForm['name'] = this.tagMsg['name'] | |||
| }) | |||
| this.handleOpenDialog(false) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '获取标签信息失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| }), | |||
| // 表单提交 | |||
| handleSubmitClick() { | |||
| this.$refs['ruleForm'].validate((valid) => { | |||
| if (valid) { | |||
| if (this.isCreate) { | |||
| this.getTags({ name: this.ruleForm['name'], page: this.currentPage, limit: this.pageSize }).then(res => { | |||
| if (res['list'].length === 0) { | |||
| this.saveTags(this.ruleForm).then(res => { | |||
| this.$message({ | |||
| message: '创建成功!', | |||
| type: 'success' | |||
| }) | |||
| this.getTags({ page: this.currentPage, limit: this.pageSize }) | |||
| this.handleCloseDialog() | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '创建失败,请重试~~', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| } else { | |||
| this.$message({ | |||
| message: '标签已存在,请勿重复添加~~', | |||
| type: 'warning' | |||
| }) | |||
| } | |||
| }) | |||
| } else { | |||
| this.updateTags({ ...this.ruleForm, id: this.tagMsg['id'] }).then(res => { | |||
| this.$message({ | |||
| message: '修改成功!', | |||
| type: 'success' | |||
| }) | |||
| this.getTags({ page: this.currentPage, limit: this.pageSize }) | |||
| this.handleCloseDialog() | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '修改失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| } | |||
| } else { | |||
| console.log('error submit!!') | |||
| return false | |||
| } | |||
| }) | |||
| }, | |||
| handlePageChange(page) { | |||
| this.currentPage = page | |||
| this.getTags({ page: page, limit: this.pageSize }) | |||
| }, | |||
| handlePageSizeChange(size) { | |||
| this.pageSize = size | |||
| this.getTags({ page: this.currentPage, limit: size }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scope> | |||
| .app-container { | |||
| padding: 10px; | |||
| } | |||
| .avatar-uploader .el-upload { | |||
| border: 1px dashed #d9d9d9; | |||
| border-radius: 6px; | |||
| cursor: pointer; | |||
| position: relative; | |||
| overflow: hidden; | |||
| } | |||
| .avatar-uploader .el-upload:hover { | |||
| border-color: #409EFF; | |||
| } | |||
| .avatar-uploader-icon { | |||
| font-size: 28px; | |||
| color: #8c939d; | |||
| width: 100px; | |||
| height: 100px; | |||
| line-height: 100px; | |||
| text-align: center; | |||
| } | |||
| .avatar { | |||
| width: 100px; | |||
| height: 100px; | |||
| display: block; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,322 @@ | |||
| <template> | |||
| <div class="app-container"> | |||
| <public-header | |||
| placeholder-text="请输入用户名" | |||
| button-text="添加用户" | |||
| @handleClickInput="handleClickInput" | |||
| @handleClickCreate="handleClickCreate" | |||
| /> | |||
| <div class="app-body"> | |||
| <public-table | |||
| :table-header="tableHeader" | |||
| :table-data="userData.list" | |||
| :page-data="{ | |||
| pageSize: userData['pageSize'], | |||
| currPage: userData['currPage'], | |||
| totalPage: userData['totalPage'], | |||
| totalCount: userData['totalCount'] | |||
| }" | |||
| @handleRemoveClick="handleRemoveClick" | |||
| @handleUpdateClick="handleUpdateClick" | |||
| @handlePageSizeChange="handlePageSizeChange" | |||
| @handlePageChange="handlePageChange" | |||
| /> | |||
| </div> | |||
| <public-dialog :dialog-title="dialogTitle" :dialog-form-visible="dialogFormVisible"> | |||
| <template #content> | |||
| <el-form | |||
| ref="ruleForm" | |||
| :model="ruleForm" | |||
| :rules="rules" | |||
| size="small" | |||
| label-width="100px" | |||
| > | |||
| <el-form-item label="用户名" prop="username"> | |||
| <el-input v-model="ruleForm.username" :disabled="!isCreate" placeholder="登录账号" /> | |||
| </el-form-item> | |||
| <el-form-item label="邮箱" prop="email"> | |||
| <el-input v-model="ruleForm.email" placeholder="邮箱" /> | |||
| </el-form-item> | |||
| <el-form-item label="手机号" prop="mobile"> | |||
| <el-input v-model="ruleForm.mobile" placeholder="手机号" /> | |||
| </el-form-item> | |||
| <el-form-item v-if="isSelf" label="密码" prop="password"> | |||
| <el-input v-model="ruleForm.password" placeholder="默认密码(888888)" /> | |||
| </el-form-item> | |||
| <el-form-item v-if="isSelf" label="确认密码" prop="checkPass"> | |||
| <el-input v-model="ruleForm.checkPass" placeholder="请再次输入密码" /> | |||
| </el-form-item> | |||
| <el-form-item v-if="isAdmin" label="角色权限" prop="roleIdList"> | |||
| <el-checkbox-group v-model="ruleForm.roleIdList" style="max-height: 240px; overflow-y: auto;"> | |||
| <el-checkbox v-for="item in userRoleData" :key="item.roleId" :label="item.roleId" name="roleIdList" style="width: 80px;"> | |||
| {{ item.roleName }} | |||
| </el-checkbox> | |||
| </el-checkbox-group> | |||
| </el-form-item> | |||
| <!-- <el-col v-if="isCreate" :span="14" :offset="4" style="font-weight: bold;"> | |||
| <span>注:默认密码 (888888)</span> | |||
| </el-col> --> | |||
| </el-form> | |||
| </template> | |||
| <template #footer> | |||
| <el-button @click="handleCloseDialog">取 消</el-button> | |||
| <el-button type="primary" @click="handleSubmitClick">确 定</el-button> | |||
| </template> | |||
| </public-dialog> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import PublicHeader from '@/layout/components/PublicHeader' | |||
| import PublicTable from '@/layout/components/PublicTable' | |||
| import PublicDialog from '@/layout/components/PublicDialog' | |||
| import { Debounce } from '@/utils/index' | |||
| import { mapGetters, mapActions } from 'vuex' | |||
| export default { | |||
| components: { | |||
| PublicHeader, | |||
| PublicTable, | |||
| PublicDialog | |||
| }, | |||
| data() { | |||
| const checkName = (rule, value, callback) => { | |||
| if (!value) { | |||
| return callback(new Error('请输入账号')) | |||
| } | |||
| callback() | |||
| } | |||
| const checkEmail = (rule, value, callback) => { | |||
| if (!value) { | |||
| callback(new Error('请输入邮箱')) | |||
| } else { | |||
| const emailReg = /^([a-zA-Z]|[0-9])(\w|\-)+@[a-zA-Z0-9]+\.([a-zA-Z]{2,4})$/ | |||
| emailReg.test(value) ? callback() : callback(new Error('请输入正确的邮箱')) | |||
| } | |||
| } | |||
| const checkPhone = (rule, value, callback) => { | |||
| if (!value) { | |||
| callback(new Error('请输入手机号')) | |||
| } else { | |||
| const phoneReg = /^[1][3,4,5,7,8][0-9]{9}$/ | |||
| phoneReg.test(value) ? callback() : callback(new Error('请输入正确的手机号')) | |||
| } | |||
| } | |||
| const checkPass = (rule, value, callback) => { | |||
| if (value === '') { | |||
| callback(new Error('请再次输入密码')) | |||
| } else if (value !== this.ruleForm.password) { | |||
| callback(new Error('两次输入密码不一致!')) | |||
| } else { | |||
| callback() | |||
| } | |||
| } | |||
| return { | |||
| dialogFormVisible: false, | |||
| dialogTitle: '', | |||
| isCreate: false, | |||
| isAdmin: false, | |||
| isSelf: false, | |||
| pageSize: 10, | |||
| currentPage: 1, | |||
| ruleForm: { | |||
| username: '', | |||
| password: '', | |||
| checkPass: '', | |||
| email: '', | |||
| mobile: '', | |||
| roleIdList: [] | |||
| }, | |||
| rules: { | |||
| username: [ | |||
| { required: true, validator: checkName, trigger: 'blur' } | |||
| ], | |||
| password: [ | |||
| { message: '请输入密码', trigger: 'blur' } | |||
| ], | |||
| checkPass: [ | |||
| { validator: checkPass, trigger: 'blur' } | |||
| ], | |||
| email: [ | |||
| { required: true, validator: checkEmail, trigger: 'blur' } | |||
| ], | |||
| mobile: [ | |||
| { required: true, validator: checkPhone, trigger: 'blur' } | |||
| ], | |||
| roleIdList: [ | |||
| { required: true, message: '请选择角色权限', trigger: 'blur' } | |||
| ] | |||
| }, | |||
| tableHeader: [ | |||
| { label: '用户名', width: '', prop: 'username' }, | |||
| { label: '创建日期', width: '', prop: 'createTime' }, | |||
| { label: '邮箱', width: '', prop: 'email' }, | |||
| { label: '联系方式', width: '', prop: 'mobile' } | |||
| ] | |||
| } | |||
| }, | |||
| computed: { | |||
| ...mapGetters([ | |||
| 'userInfo', | |||
| 'userData', | |||
| 'userMsg', | |||
| 'userRoleData' | |||
| ]) | |||
| }, | |||
| created() { | |||
| this.loadMenuData() | |||
| }, | |||
| methods: { | |||
| ...mapActions({ | |||
| getRolesByUserId: 'roles/fetchRoleListByUserId', | |||
| getUsers: 'user/fetchUserList', | |||
| getUserMsg: 'user/fetchUserMsg', | |||
| saveUsers: 'user/saveUsers', | |||
| removeUsers: 'user/removeUsers', | |||
| updateUsers: 'user/updateUsers' | |||
| }), | |||
| loadMenuData() { | |||
| this.getRolesByUserId() | |||
| this.getUsers() | |||
| }, | |||
| // 打开弹窗 | |||
| handleOpenDialog: Debounce(function(option) { | |||
| this.isCreate = option | |||
| this.dialogTitle = option ? '添加用户' : '编辑用户' | |||
| this.dialogFormVisible = true | |||
| }), | |||
| // 关闭弹窗 | |||
| handleCloseDialog() { | |||
| this.dialogFormVisible = false | |||
| this.$nextTick(() => { | |||
| this.$refs['ruleForm'].resetFields() | |||
| this.ruleForm['roleIdList'] = [] | |||
| }) | |||
| }, | |||
| // 查询 | |||
| handleClickInput: Debounce(function(option) { | |||
| console.log(option, '查询') | |||
| this.getUsers({ username: option['inputText'] }) | |||
| }), | |||
| // 创建 | |||
| handleClickCreate: Debounce(function(option) { | |||
| console.log(option, '添加') | |||
| this.handleOpenDialog(true) | |||
| this.isAdmin = true | |||
| }), | |||
| // 删除 | |||
| handleRemoveClick: Debounce(function(option) { | |||
| console.log(option, '删除') | |||
| this.$confirm('此操作将永久删除该用户, 是否继续?', '删除用户', { | |||
| confirmButtonText: '确定', | |||
| cancelButtonText: '取消', | |||
| type: 'warning' | |||
| }).then(() => { | |||
| this.removeUsers([option.userId]).then(res => { | |||
| this.$message({ | |||
| type: 'success', | |||
| message: '删除成功!' | |||
| }) | |||
| this.getUsers({ page: this.currentPage, limit: this.pageSize }) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '删除失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| type: 'info', | |||
| message: '已取消删除!' | |||
| }) | |||
| }) | |||
| }), | |||
| // 编辑 | |||
| handleUpdateClick: Debounce(function(option) { | |||
| console.log(option, '编辑') | |||
| this.handleOpenDialog(false) | |||
| this.getUserMsg(option.userId).then(res => { | |||
| this.$nextTick(() => { | |||
| this.isAdmin = !!this.userMsg['roleIdList'].length === 0 | |||
| this.isSelf = this.userMsg['userId'] !== this.userInfo['userId'] | |||
| this.ruleForm['username'] = this.userMsg['username'] | |||
| this.ruleForm['email'] = this.userMsg['email'] | |||
| this.ruleForm['mobile'] = this.userMsg['mobile'] | |||
| this.ruleForm['roleIdList'] = this.userMsg['roleIdList'] | |||
| }) | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '获取用户信息失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| }), | |||
| // 提交表单 | |||
| handleSubmitClick() { | |||
| if (!this.ruleForm['password']) this.ruleForm['password'] = '888888' | |||
| this.$refs['ruleForm'].validate((valid) => { | |||
| if (valid) { | |||
| if (this.isCreate) { | |||
| this.saveUsers(this.ruleForm).then(res => { | |||
| this.$message({ | |||
| message: '创建成功!', | |||
| type: 'success' | |||
| }) | |||
| this.getUsers({ page: this.currentPage, limit: this.pageSize }) | |||
| this.handleCloseDialog() | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '创建失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| } else { | |||
| this.updateUsers({ ...this.ruleForm, userId: this.userMsg['userId'] }).then(res => { | |||
| this.$message({ | |||
| message: '修改成功!', | |||
| type: 'success' | |||
| }) | |||
| this.getUsers({ page: this.currentPage, limit: this.pageSize }) | |||
| this.handleCloseDialog() | |||
| }).catch(() => { | |||
| this.$message({ | |||
| message: '修改失败,请重试!', | |||
| type: 'error' | |||
| }) | |||
| }) | |||
| } | |||
| } else { | |||
| console.log('error submit!!') | |||
| return false | |||
| } | |||
| }) | |||
| }, | |||
| handlePageChange(page) { | |||
| this.currentPage = page | |||
| this.getUsers({ page: page, limit: this.pageSize }) | |||
| }, | |||
| handlePageSizeChange(size) { | |||
| this.pageSize = size | |||
| this.getUsers({ page: this.currentPage, limit: size }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scope> | |||
| .app-container { | |||
| padding: 10px; | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| .app-body { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| .el-message { | |||
| min-width: 120px; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,5 @@ | |||
| module.exports = { | |||
| env: { | |||
| jest: true | |||
| } | |||
| } | |||
| @@ -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) | |||
| }) | |||
| }) | |||