Bladeren bron

短视频小程序后台管理平台

master
libx 4 jaren geleden
bovenliggende
commit
eab0f0946a
100 gewijzigde bestanden met toevoegingen van 7721 en 0 verwijderingen
  1. +14
    -0
      .editorconfig
  2. +5
    -0
      .env.development
  3. +6
    -0
      .env.production
  4. +8
    -0
      .env.staging
  5. +4
    -0
      .eslintignore
  6. +198
    -0
      .eslintrc.js
  7. +16
    -0
      .gitignore
  8. +5
    -0
      .travis.yml
  9. +21
    -0
      LICENSE
  10. +102
    -0
      README-zh.md
  11. +14
    -0
      babel.config.js
  12. +35
    -0
      build/index.js
  13. +24
    -0
      jest.config.js
  14. +9
    -0
      jsconfig.json
  15. +59
    -0
      mock/index.js
  16. +81
    -0
      mock/mock-server.js
  17. +136
    -0
      mock/roles.js
  18. +29
    -0
      mock/table.js
  19. +84
    -0
      mock/user.js
  20. +25
    -0
      mock/utils.js
  21. +67
    -0
      package.json
  22. +8
    -0
      postcss.config.js
  23. BIN
      public/favicon.ico
  24. +18
    -0
      public/index.html
  25. BIN
      public/logo.png
  26. +11
    -0
      src/App.vue
  27. +62
    -0
      src/api/roles.js
  28. +45
    -0
      src/api/tags.js
  29. +63
    -0
      src/api/types.js
  30. +88
    -0
      src/api/user.js
  31. +92
    -0
      src/api/videos.js
  32. BIN
      src/assets/404_images/404.png
  33. BIN
      src/assets/404_images/404_cloud.png
  34. BIN
      src/assets/logo.png
  35. +78
    -0
      src/components/Breadcrumb/index.vue
  36. +44
    -0
      src/components/Hamburger/index.vue
  37. +62
    -0
      src/components/SvgIcon/index.vue
  38. +9
    -0
      src/icons/index.js
  39. +1
    -0
      src/icons/svg/dashboard.svg
  40. +1
    -0
      src/icons/svg/example.svg
  41. +1
    -0
      src/icons/svg/eye-open.svg
  42. +1
    -0
      src/icons/svg/eye.svg
  43. +1
    -0
      src/icons/svg/form.svg
  44. +1
    -0
      src/icons/svg/link.svg
  45. +1
    -0
      src/icons/svg/nested.svg
  46. +1
    -0
      src/icons/svg/password.svg
  47. +1
    -0
      src/icons/svg/table.svg
  48. +1
    -0
      src/icons/svg/tree.svg
  49. +1
    -0
      src/icons/svg/user.svg
  50. +22
    -0
      src/icons/svgo.yml
  51. +40
    -0
      src/layout/components/AppMain.vue
  52. +230
    -0
      src/layout/components/Navbar.vue
  53. +51
    -0
      src/layout/components/PublicDialog.vue
  54. +244
    -0
      src/layout/components/PublicHeader.vue
  55. +224
    -0
      src/layout/components/PublicTable.vue
  56. +144
    -0
      src/layout/components/PublicVideo.vue
  57. +26
    -0
      src/layout/components/Sidebar/FixiOSBug.js
  58. +41
    -0
      src/layout/components/Sidebar/Item.vue
  59. +43
    -0
      src/layout/components/Sidebar/Link.vue
  60. +83
    -0
      src/layout/components/Sidebar/Logo.vue
  61. +95
    -0
      src/layout/components/Sidebar/SidebarItem.vue
  62. +56
    -0
      src/layout/components/Sidebar/index.vue
  63. +3
    -0
      src/layout/components/index.js
  64. +93
    -0
      src/layout/index.vue
  65. +45
    -0
      src/layout/mixin/ResizeHandler.js
  66. +64
    -0
      src/main.js
  67. +64
    -0
      src/permission.js
  68. +147
    -0
      src/router/index.js
  69. +16
    -0
      src/settings.js
  70. +23
    -0
      src/store/getters.js
  71. +28
    -0
      src/store/index.js
  72. +48
    -0
      src/store/modules/app.js
  73. +111
    -0
      src/store/modules/roles.js
  74. +32
    -0
      src/store/modules/settings.js
  75. +74
    -0
      src/store/modules/tags.js
  76. +101
    -0
      src/store/modules/types.js
  77. +187
    -0
      src/store/modules/user.js
  78. +130
    -0
      src/store/modules/videos.js
  79. +49
    -0
      src/styles/element-ui.scss
  80. +66
    -0
      src/styles/index.scss
  81. +28
    -0
      src/styles/mixin.scss
  82. +226
    -0
      src/styles/sidebar.scss
  83. +48
    -0
      src/styles/transition.scss
  84. +25
    -0
      src/styles/variables.scss
  85. +15
    -0
      src/utils/auth.js
  86. +10
    -0
      src/utils/get-page-title.js
  87. +132
    -0
      src/utils/index.js
  88. +83
    -0
      src/utils/request.js
  89. +20
    -0
      src/utils/validate.js
  90. +228
    -0
      src/views/404.vue
  91. +85
    -0
      src/views/dashboard/index.vue
  92. +200
    -0
      src/views/departmentManager/index.vue
  93. +295
    -0
      src/views/login/index.vue
  94. +663
    -0
      src/views/mediaManager/index.vue
  95. +578
    -0
      src/views/mediaTypeManager/index.vue
  96. +278
    -0
      src/views/rolesManager/index.vue
  97. +269
    -0
      src/views/tagsManager/index.vue
  98. +322
    -0
      src/views/userManager/index.vue
  99. +5
    -0
      tests/unit/.eslintrc.js
  100. +98
    -0
      tests/unit/components/Breadcrumb.spec.js

+ 14
- 0
.editorconfig Bestand weergeven

@@ -0,0 +1,14 @@
# http://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

+ 5
- 0
.env.development Bestand weergeven

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

# base api
VUE_APP_BASE_API = '/video-admin'

+ 6
- 0
.env.production Bestand weergeven

@@ -0,0 +1,6 @@
# just a flag
ENV = 'production'

# base api
VUE_APP_BASE_API = 'video-admin'


+ 8
- 0
.env.staging Bestand weergeven

@@ -0,0 +1,8 @@
NODE_ENV = production

# just a flag
ENV = 'staging'

# base api
VUE_APP_BASE_API = '/stage-api'


+ 4
- 0
.eslintignore Bestand weergeven

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

+ 198
- 0
.eslintrc.js Bestand weergeven

@@ -0,0 +1,198 @@
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true,
},
extends: ['plugin:vue/recommended', 'eslint:recommended'],

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

+ 16
- 0
.gitignore Bestand weergeven

@@ -0,0 +1,16 @@
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
tests/**/coverage/

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln

+ 5
- 0
.travis.yml Bestand weergeven

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

+ 21
- 0
LICENSE Bestand weergeven

@@ -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.

+ 102
- 0
README-zh.md Bestand weergeven

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

![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)

## 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

+ 14
- 0
babel.config.js Bestand weergeven

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

+ 35
- 0
build/index.js Bestand weergeven

@@ -0,0 +1,35 @@
const { run } = require('runjs')
const chalk = require('chalk')
const config = require('../vue.config.js')
const rawArgv = process.argv.slice(2)
const args = rawArgv.join(' ')

if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
const report = rawArgv.includes('--report')

run(`vue-cli-service build ${args}`)

const port = 9526
const publicPath = config.publicPath

var connect = require('connect')
var serveStatic = require('serve-static')
const app = connect()

app.use(
publicPath,
serveStatic('./dist', {
index: ['index.html', '/']
})
)

app.listen(port, function () {
console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
if (report) {
console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
}

})
} else {
run(`vue-cli-service build ${args}`)
}

+ 24
- 0
jest.config.js Bestand weergeven

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

+ 9
- 0
jsconfig.json Bestand weergeven

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

+ 59
- 0
mock/index.js Bestand weergeven

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


+ 81
- 0
mock/mock-server.js Bestand weergeven

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

+ 136
- 0
mock/roles.js Bestand weergeven

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

+ 29
- 0
mock/table.js Bestand weergeven

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

+ 84
- 0
mock/user.js Bestand weergeven

@@ -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'
}
}
}
]

+ 25
- 0
mock/utils.js Bestand weergeven

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

+ 67
- 0
package.json Bestand weergeven

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

+ 8
- 0
postcss.config.js Bestand weergeven

@@ -0,0 +1,8 @@
// https://github.com/michael-ciniawsky/postcss-load-config

module.exports = {
'plugins': {
// to edit target browsers: use "browserslist" field in package.json
'autoprefixer': {}
}
}

BIN
public/favicon.ico Bestand weergeven

Before After

+ 18
- 0
public/index.html Bestand weergeven

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

BIN
public/logo.png Bestand weergeven

Before After
Width: 250  |  Height: 250  |  Size: 5.0KB

+ 11
- 0
src/App.vue Bestand weergeven

@@ -0,0 +1,11 @@
<template>
<div id="app">
<router-view />
</div>
</template>

<script>
export default {
name: 'App'
}
</script>

+ 62
- 0
src/api/roles.js Bestand weergeven

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

+ 45
- 0
src/api/tags.js Bestand weergeven

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

+ 63
- 0
src/api/types.js Bestand weergeven

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

+ 88
- 0
src/api/user.js Bestand weergeven

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

+ 92
- 0
src/api/videos.js Bestand weergeven

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

BIN
src/assets/404_images/404.png Bestand weergeven

Before After
Width: 1014  |  Height: 556  |  Size: 96KB

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

Before After
Width: 152  |  Height: 138  |  Size: 4.7KB

BIN
src/assets/logo.png Bestand weergeven

Before After
Width: 250  |  Height: 250  |  Size: 5.0KB

+ 78
- 0
src/components/Breadcrumb/index.vue Bestand weergeven

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

+ 44
- 0
src/components/Hamburger/index.vue Bestand weergeven

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

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

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

+ 9
- 0
src/icons/index.js Bestand weergeven

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

+ 1
- 0
src/icons/svg/dashboard.svg Bestand weergeven

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

+ 1
- 0
src/icons/svg/example.svg Bestand weergeven

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

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

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

+ 1
- 0
src/icons/svg/eye.svg Bestand weergeven

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

+ 1
- 0
src/icons/svg/form.svg Bestand weergeven

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

+ 1
- 0
src/icons/svg/link.svg Bestand weergeven

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

+ 1
- 0
src/icons/svg/nested.svg Bestand weergeven

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

+ 1
- 0
src/icons/svg/password.svg Bestand weergeven

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

+ 1
- 0
src/icons/svg/table.svg Bestand weergeven

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

+ 1
- 0
src/icons/svg/tree.svg Bestand weergeven

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

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

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

+ 22
- 0
src/icons/svgo.yml Bestand weergeven

@@ -0,0 +1,22 @@
# replace default config

# multipass: true
# full: true

plugins:

# - name
#
# or:
# - name: false
# - name: true
#
# or:
# - name:
# param1: 1
# param2: 2

- removeAttrs:
attrs:
- 'fill'
- 'fill-rule'

+ 40
- 0
src/layout/components/AppMain.vue Bestand weergeven

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

+ 230
- 0
src/layout/components/Navbar.vue Bestand weergeven

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

+ 51
- 0
src/layout/components/PublicDialog.vue Bestand weergeven

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

+ 244
- 0
src/layout/components/PublicHeader.vue Bestand weergeven

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

+ 224
- 0
src/layout/components/PublicTable.vue Bestand weergeven

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

+ 144
- 0
src/layout/components/PublicVideo.vue Bestand weergeven

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

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

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

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

@@ -0,0 +1,41 @@
<script>
export default {
name: 'MenuItem',
functional: true,
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
render(h, context) {
const { icon, title } = context.props
const vnodes = []

if (icon) {
if (icon.includes('el-icon')) {
vnodes.push(<i class={[icon, 'sub-el-icon']} />)
} else {
vnodes.push(<svg-icon icon-class={icon}/>)
}
}

if (title) {
vnodes.push(<span slot='title'>{(title)}</span>)
}
return vnodes
}
}
</script>

<style scoped>
.sub-el-icon {
color: currentColor;
width: 1em;
height: 1em;
}
</style>

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

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

+ 83
- 0
src/layout/components/Sidebar/Logo.vue Bestand weergeven

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

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

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

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

@@ -0,0 +1,56 @@
<template>
<div :class="{'has-logo':showLogo}">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
</template>

<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'

export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'sidebar'
]),
routes() {
return this.$router.options.routes
},
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>

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

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

+ 93
- 0
src/layout/index.vue Bestand weergeven

@@ -0,0 +1,93 @@
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<sidebar class="sidebar-container" />
<div class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<navbar />
</div>
<app-main />
</div>
</div>
</template>

<script>
import { Navbar, Sidebar, AppMain } from './components'
import ResizeMixin from './mixin/ResizeHandler'

export default {
name: 'Layout',
components: {
Navbar,
Sidebar,
AppMain
},
mixins: [ResizeMixin],
computed: {
sidebar() {
return this.$store.state.app.sidebar
},
device() {
return this.$store.state.app.device
},
fixedHeader() {
return this.$store.state.settings.fixedHeader
},
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
}
}
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
}
</script>

<style lang="scss" scoped>
@import "~@/styles/mixin.scss";
@import "~@/styles/variables.scss";

.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar{
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}

.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$sideBarWidth});
transition: width 0.28s;
}

.hideSidebar .fixed-header {
width: calc(100% - 54px)
}

.mobile .fixed-header {
width: 100%;
}
</style>

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

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

+ 64
- 0
src/main.js Bestand weergeven

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

+ 64
- 0
src/permission.js Bestand weergeven

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

+ 147
- 0
src/router/index.js Bestand weergeven

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

+ 16
- 0
src/settings.js Bestand weergeven

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

+ 23
- 0
src/store/getters.js Bestand weergeven

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

+ 28
- 0
src/store/index.js Bestand weergeven

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

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

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

+ 111
- 0
src/store/modules/roles.js Bestand weergeven

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

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

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


+ 74
- 0
src/store/modules/tags.js Bestand weergeven

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

+ 101
- 0
src/store/modules/types.js Bestand weergeven

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

+ 187
- 0
src/store/modules/user.js Bestand weergeven

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


+ 130
- 0
src/store/modules/videos.js Bestand weergeven

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

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

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

+ 66
- 0
src/styles/index.scss Bestand weergeven

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

+ 28
- 0
src/styles/mixin.scss Bestand weergeven

@@ -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%;
}

+ 226
- 0
src/styles/sidebar.scss Bestand weergeven

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

+ 48
- 0
src/styles/transition.scss Bestand weergeven

@@ -0,0 +1,48 @@
// global transition css

/* fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.28s;
}

.fade-enter,
.fade-leave-active {
opacity: 0;
}

/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .5s;
}

.fade-transform-enter {
opacity: 0;
transform: translateX(-30px);
}

.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}

/* breadcrumb transition */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all .5s;
}

.breadcrumb-enter,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}

.breadcrumb-move {
transition: all .5s;
}

.breadcrumb-leave-active {
position: absolute;
}

+ 25
- 0
src/styles/variables.scss Bestand weergeven

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

+ 15
- 0
src/utils/auth.js Bestand weergeven

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

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

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

+ 132
- 0
src/utils/index.js Bestand weergeven

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

+ 83
- 0
src/utils/request.js Bestand weergeven

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

+ 20
- 0
src/utils/validate.js Bestand weergeven

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

+ 228
- 0
src/views/404.vue Bestand weergeven

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

+ 85
- 0
src/views/dashboard/index.vue Bestand weergeven

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

+ 200
- 0
src/views/departmentManager/index.vue Bestand weergeven

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

+ 295
- 0
src/views/login/index.vue Bestand weergeven

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

+ 663
- 0
src/views/mediaManager/index.vue Bestand weergeven

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

+ 578
- 0
src/views/mediaTypeManager/index.vue Bestand weergeven

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

+ 278
- 0
src/views/rolesManager/index.vue Bestand weergeven

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

+ 269
- 0
src/views/tagsManager/index.vue Bestand weergeven

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

+ 322
- 0
src/views/userManager/index.vue Bestand weergeven

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

+ 5
- 0
tests/unit/.eslintrc.js Bestand weergeven

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

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

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

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

Laden…
Annuleren
Opslaan