| @@ -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) | |||||
| }) | |||||
| }) | |||||