chutingting пре 1 година
комит
af5c55ab53
97 измењених фајлова са 6841 додато и 0 уклоњено
  1. +14
    -0
      .editorconfig
  2. +5
    -0
      .env.development
  3. +6
    -0
      .env.int
  4. +7
    -0
      .env.production
  5. +4
    -0
      .eslintignore
  6. +200
    -0
      .eslintrc.js
  7. +21
    -0
      .gitignore
  8. +36
    -0
      README.md
  9. +19
    -0
      babel.config.js
  10. +35
    -0
      build/index.js
  11. +65
    -0
      package.json
  12. +8
    -0
      postcss.config.js
  13. BIN
      public/favicon.ico
  14. +17
    -0
      public/index.html
  15. +16
    -0
      src/App.vue
  16. +75
    -0
      src/api/car.js
  17. +17
    -0
      src/api/common.js
  18. +18
    -0
      src/api/data.js
  19. +71
    -0
      src/api/homePage.js
  20. +164
    -0
      src/api/login.js
  21. +42
    -0
      src/api/nested/demo.js
  22. +24
    -0
      src/api/user.js
  23. BIN
      src/assets/404_images/404.png
  24. BIN
      src/assets/404_images/404_cloud.png
  25. BIN
      src/assets/icon/iconfont.ttf
  26. BIN
      src/assets/icon/iconfont.woff
  27. BIN
      src/assets/icon/iconfont.woff2
  28. +63
    -0
      src/assets/icon/icons.css
  29. BIN
      src/assets/img/home.png
  30. +35
    -0
      src/assets/styles/btn.scss
  31. +121
    -0
      src/assets/styles/eladmin.scss
  32. +70
    -0
      src/assets/styles/element-ui.scss
  33. +31
    -0
      src/assets/styles/element-variables.scss
  34. +81
    -0
      src/assets/styles/index.scss
  35. +28
    -0
      src/assets/styles/mixin.scss
  36. +226
    -0
      src/assets/styles/sidebar.scss
  37. +48
    -0
      src/assets/styles/transition.scss
  38. +25
    -0
      src/assets/styles/variables.scss
  39. +78
    -0
      src/components/Breadcrumb/index.vue
  40. +268
    -0
      src/components/Crud/CRUD.operation.vue
  41. +18
    -0
      src/components/Crud/Pagination.vue
  42. +20
    -0
      src/components/Crud/RR.operation.vue
  43. +71
    -0
      src/components/Crud/UD.operation.vue
  44. +864
    -0
      src/components/Crud/crud.js
  45. +35
    -0
      src/components/Hamburger/index.vue
  46. +13
    -0
      src/components/Permission/index.js
  47. +21
    -0
      src/components/Permission/permission.js
  48. +208
    -0
      src/components/ueditor/index.vue
  49. +96
    -0
      src/components/uploadFile/index.vue
  50. +40
    -0
      src/layout/components/AppMain.vue
  51. +139
    -0
      src/layout/components/Navbar.vue
  52. +26
    -0
      src/layout/components/Sidebar/FixiOSBug.js
  53. +41
    -0
      src/layout/components/Sidebar/Item.vue
  54. +43
    -0
      src/layout/components/Sidebar/Link.vue
  55. +82
    -0
      src/layout/components/Sidebar/Logo.vue
  56. +95
    -0
      src/layout/components/Sidebar/SidebarItem.vue
  57. +56
    -0
      src/layout/components/Sidebar/index.vue
  58. +3
    -0
      src/layout/components/index.js
  59. +97
    -0
      src/layout/index.vue
  60. +45
    -0
      src/layout/mixin/ResizeHandler.js
  61. +85
    -0
      src/main.js
  62. +335
    -0
      src/mixins/crud.js
  63. +7
    -0
      src/public-path.js
  64. +114
    -0
      src/router/index.js
  65. +16
    -0
      src/settings.js
  66. +15
    -0
      src/store/getters.js
  67. +19
    -0
      src/store/index.js
  68. +48
    -0
      src/store/modules/app.js
  69. +32
    -0
      src/store/modules/settings.js
  70. +114
    -0
      src/store/modules/user.js
  71. +15
    -0
      src/utils/auth.js
  72. +36
    -0
      src/utils/clipboard.js
  73. +17
    -0
      src/utils/get-page-title.js
  74. +171
    -0
      src/utils/index.js
  75. +23
    -0
      src/utils/permission.js
  76. +71
    -0
      src/utils/request.js
  77. +36
    -0
      src/utils/validate.js
  78. +228
    -0
      src/views/404.vue
  79. +115
    -0
      src/views/articleManage/index.vue
  80. +112
    -0
      src/views/articleManage/save.vue
  81. +30
    -0
      src/views/home/index.vue
  82. +115
    -0
      src/views/homePage/banner1.vue
  83. +115
    -0
      src/views/homePage/banner2.vue
  84. +54
    -0
      src/views/homePage/carHot.vue
  85. +171
    -0
      src/views/homePage/carousel.vue
  86. +0
    -0
      src/views/homePage/link.vue
  87. +118
    -0
      src/views/homePage/yyzHot.vue
  88. +289
    -0
      src/views/login/index.vue
  89. +115
    -0
      src/views/nested/demo/index.vue
  90. +112
    -0
      src/views/nested/demo/save.vue
  91. +7
    -0
      src/views/nested/menu1/index.vue
  92. +7
    -0
      src/views/nested/menu1/menu1-1/index.vue
  93. +7
    -0
      src/views/nested/menu1/menu1-2/index.vue
  94. +5
    -0
      src/views/nested/menu1/menu1-2/menu1-2-1/index.vue
  95. +5
    -0
      src/views/nested/menu1/menu1-2/menu1-2-2/index.vue
  96. +5
    -0
      src/views/nested/menu1/menu1-3/index.vue
  97. +126
    -0
      vue.config.js

+ 14
- 0
.editorconfig Прегледај датотеку

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

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

[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

+ 5
- 0
.env.development Прегледај датотеку

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

# base api
VUE_APP_BASE_API = 'https://admin.meta-autotv.com/homeA/'

+ 6
- 0
.env.int Прегледај датотеку

@@ -0,0 +1,6 @@
NODE_ENV = 'production'
OUTPUT_DIR = 'int'
VUE_APP_ENV = 'int'

# base api
VUE_APP_BASE_API = '/adcop_cloud/'

+ 7
- 0
.env.production Прегледај датотеку

@@ -0,0 +1,7 @@
NODE_ENV = 'production'
OUTPUT_DIR = 'pro'
VUE_APP_ENV = 'production'

# base api
VUE_APP_BASE_API = '/'


+ 4
- 0
.eslintignore Прегледај датотеку

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

+ 200
- 0
.eslintrc.js Прегледај датотеку

@@ -0,0 +1,200 @@
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/html-closing-bracket-spacing': 'off',
'vue/no-multi-spaces': 'off',
'no-useless-catch': 'off',
'vue/require-default-prop': 'off',
'vue/require-prop-types': 'off',
'vue/html-self-closing': "off",
"vue/max-attributes-per-line": "off",
"vue/attributes-order": "off",
"vue/html-indent": "off",
"vue/html-quotes": "off",
"vue/html-closing-bracket-newline": "off",
"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
}], */
'indent': "off",
'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': 'off',
'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': 0,
'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': [0, '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': 0,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never']
}
}

+ 21
- 0
.gitignore Прегледај датотеку

@@ -0,0 +1,21 @@
.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

/int
/stg
/pro
/uat

+ 36
- 0
README.md Прегледај датотеку

@@ -0,0 +1,36 @@
# 后台管理系统

#### 前端模板

初始模板基于: [https://github.com/PanJiaChen/vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)

模板文档: [https://panjiachen.github.io/vue-element-admin-site/zh/guide/](https://panjiachen.github.io/vue-element-admin-site/zh/guide/)

#### Build Setup
``` bash

# 安装依赖
npm install

# 启动服务
npm run dev

# 构建INT、Stage、生产环境
npm run int
npm run stg
npm run pro
```

#### 常见问题

1、linux 系统在安装依赖的时候会出现 node-sass 无法安装的问题

解决方案:
```
1. 单独安装:npm install --unsafe-perm node-sass
2. 直接使用:npm install --unsafe-perm
```

2、加速node-sass安装

https://www.ydyno.com/archives/1219.html

+ 19
- 0
babel.config.js Прегледај датотеку

@@ -0,0 +1,19 @@
const prodPlugins = []
if (process.env.VUE_APP_ENV === 'production') {
prodPlugins.push('transform-remove-console')
}
module.exports = {
plugins: [...prodPlugins],
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 Прегледај датотеку

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

+ 65
- 0
package.json Прегледај датотеку

@@ -0,0 +1,65 @@
{
"name": "admin-manage",
"version": "1.0.0",
"description": "后台管理系统",
"author": "",
"scripts": {
"dev": "vue-cli-service serve",
"pro": "vue-cli-service build",
"int": "vue-cli-service build --mode int",
"lint": "eslint --ext .js,.vue src"
},
"dependencies": {
"axios": "0.18.1",
"clipboard": "^2.0.8",
"core-js": "3.6.5",
"element-ui": "2.13.2",
"js-cookie": "2.2.0",
"moment": "^2.29.1",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"vue": "2.6.10",
"vue-router": "3.0.6",
"vue-ueditor-wrap": "^2.5.6",
"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.0.1",
"babel-jest": "23.6.0",
"babel-plugin-dynamic-import-node": "2.3.3",
"babel-plugin-transform-remove-console": "^6.9.4",
"chalk": "2.4.2",
"connect": "3.6.6",
"eslint": "^6.7.2",
"eslint-plugin-vue": "6.2.2",
"html-webpack-plugin": "3.2.0",
"husky": "^4.3.8",
"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",
"vue-template-compiler": "2.6.10"
},
"browserslist": [
"> 1%",
"last 2 versions"
],
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"license": "MIT"
}

+ 8
- 0
postcss.config.js Прегледај датотеку

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

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

BIN
public/favicon.ico Прегледај датотеку

Before After

+ 17
- 0
public/index.html Прегледај датотеку

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= webpackConfig.name %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

+ 16
- 0
src/App.vue Прегледај датотеку

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

<script>
export default {
name: 'App',
methods: {
test() {
const a = 0
}
}
}
</script>

+ 75
- 0
src/api/car.js Прегледај датотеку

@@ -0,0 +1,75 @@
/*
* @Date: 2021-08-24 14:51:18
* @LastEditTime: 2022-07-27 10:47:43
* @Description: 汽车资讯
*/

import request from '@/utils/request'

/** 轮播 */
export function saveLunbo(data) {
return request({
url: '/car/saveLunbo',
method: 'post',
data
})
}

export function delLunbo(id) {
return request({
url: '/index/delLunbo',
method: 'post',
data: { id }
})
}
/** 热点新闻配置(是否显示) */
export function hotNewsConfig() {
return request({
url: '/car/hotNewsConfig',
method: 'get'
})
}

export function saveHotNewsConfig(data) {
return request({
url: '/index/saveHotNewsConfig',
method: 'post',
data
})
}

/** 广告位1 */
export function guanggao1() {
return request({
url: '/car/guanggao1',
method: 'get'
})
}

export function saveGuangGao(data) {
return request({
url: '/index/saveGuangGao',
method: 'post',
data
})
}


/** 视频轮播 */

export function videoSave(data) {
return request({
url: '/car/videoSave',
method: 'post',
data
})
}


/** 广告位2 */
export function guanggao2() {
return request({
url: '/car/guanggao2',
method: 'get'
})
}

+ 17
- 0
src/api/common.js Прегледај датотеку

@@ -0,0 +1,17 @@
import request from '@/utils/request'

export function upload(data) {
var formData = new FormData()
formData.append('file', data.file)
console.log('upload:', data)
return request({
url: '/upload/awsImgUpload',
method: 'post',
data: formData,
timeout: 1000 * 60 * 2, // 超时时间两分钟
params: {
width: 200,
hight: 200
}
})
}

+ 18
- 0
src/api/data.js Прегледај датотеку

@@ -0,0 +1,18 @@
import request from '@/utils/request'
import qs from 'qs'

export function initData(url, params) {
// console.log(qs.stringify(params, { indices: false }))
return request({
url: url + '?' + qs.stringify(params, { indices: false }),
method: 'get'
})
}

export function download(url, params) {
return request({
url: url + '?' + qs.stringify(params, { indices: false }),
method: 'get',
responseType: 'blob'
})
}

+ 71
- 0
src/api/homePage.js Прегледај датотеку

@@ -0,0 +1,71 @@
/*
* @Date: 2021-08-24 14:51:18
* @LastEditTime: 2022-07-27 10:47:43
* @Description: 汽车资讯
*/

import request from '@/utils/request'

/** 轮播 */
export function saveLunbo(data) {
return request({
url: '/index/saveLunbo',
method: 'post',
data
})
}

export function delLunbo(id) {
return request({
url: '/index/delLunbo',
method: 'post',
data: { id }
})
}
/** 热点新闻配置(是否显示) */
export function carNewsHotSave(newsIdList) {
return request({
url: ' /index/carNewsHotSave',
method: 'post',
data: { newsIdList }
})
}

export function hotNewsConfig() {
return request({
url: '/index/hotNewsConfig',
method: 'get'
})
}

export function saveHotNewsConfig(data) {
return request({
url: '/index/saveHotNewsConfig',
method: 'post',
data
})
}

/** 广告位1 */
export function guanggao1() {
return request({
url: '/index/guanggao1',
method: 'get'
})
}

export function saveGuangGao(data) {
return request({
url: '/index/saveGuangGao',
method: 'post',
data
})
}

/** 广告位2 */
export function guanggao2() {
return request({
url: '/index/guanggao2',
method: 'get'
})
}

+ 164
- 0
src/api/login.js Прегледај датотеку

@@ -0,0 +1,164 @@
import request from '@/utils/request.js'

const baseURL = process.env.VUE_APP_BASE_API

/**
* @description 登录
* @params email,password,code
* @returns data
*/
export function loginApi(data) {
return request({
url: `/doLogin`,
method: 'post',
data
})
}

/**
* @description 退出登录
* @params email,password,code
* @returns data
*/
export function logoutApi(data) {
return request({
// url: `/api/user/loginByEmail`,
url: `/logout`,
method: 'get'
})
}

/**
/**
* @description:重新发送邮件
* @param {string} data
* @return: data
*/
export function sendRegisterEmail(email) {
return request({
url: `/api/user/sendRegisterEmail`,
method: 'post',
data: {
email
}
})
}

/**
/**
* @description:忘记密码发送邮件
* @param {string} data
* @return: data
*/
// export function sendUpdPwdEmailApi(email) {
// return request({
// url: `/api/user/sendUpdPwdEmail`,
// method: 'post',
// data: {
// email
// }
// })
// }
export function sendUpdPwdEmailApi(data) {
return request({
url: `/api/user/sendPhoneCode?phone=${data.phone}&code=${data.code}`,
method: 'get'
})
}

/** `/api/user/sendLoginPhoneCode?phone=${phone}`
* @description 忘记密码第二步,短信验证码和新密码
* @param {object} loginParams (phone,code,pwd)
* @returns data
*/
export function doUpdPassApi(loginParams) {
return request({
url: `/api/user/updPass`,
method: 'post',
data: loginParams
})
}

/**
* @description 获取手机验证码
* @param {number} phone
* @returns data
*/
export function getSMSToLoginApi(phone) {
return request({
url: `/api/user/sendLoginPhoneCode?phone=${phone}`,
method: 'get'
})
}

/**
* @description 短信验证码登录
* @param {object} loginParams (phone,code)
* @returns data
*/
export function loginByMSMApi(loginParams) {
return request({
url: `/api/user/doLoginByPhone`,
method: 'post',
data: loginParams
})
}

/**
* @description 查询用户邀请码状态
* @returns data
*/
export function doFindInviteCode() {
return request({
url: `/api/inviteCode/find`,
method: 'get'
})
}

/**
* @description 更新邀请码
* @param {object} inviteCodeData
* @returns data
*/
export function doUpdateInviteCode(inviteCodeData) {
return request({
url: `/api/inviteCode/update`,
method: 'post',
data: inviteCodeData
})
}

/**
/**
* @description:邮件重置密码,
* @param pwd, ticket
* @return: data
*/
export function resetPwdEmailApi(data) {
return request({
url: `/api/user/doUpdPass`,
method: 'post',
data
})
}

/**
/**
* @description:邮件重置密码,
* @param oldpwd, newpwd
* @return: data
*/
export function resetPwdFormApi(data) {
return request({
url: `/api/user/oldUpdPass`,
method: 'post',
data
})
}

/**
* @description:获取图形验证码
* @return: 验证码地址
*/
export const getCodeImgUrl = baseURL + '/captcha.jpg'


+ 42
- 0
src/api/nested/demo.js Прегледај датотеку

@@ -0,0 +1,42 @@
/*
* @Date: 2021-08-24 14:51:18
* @LastEditTime: 2022-07-27 10:47:43
* @Description: 列表demo API
*/

import request from '@/utils/request'

export function del(data) {
return request({
url: '',
method: 'post',
data: { data }
})
}

/* 导出 */
export function exportFile(data) {
return request({
url: '',
method: 'get',
params: data,
responseType: 'blob'
})
}

export function getData(id) {
return request({
url: '',
method: 'post',
data: { id }
})
}

export function save(data) {
return request({
url: '',
method: 'post',
data: { data }
})
}
export default { del, getData, save, exportFile }

+ 24
- 0
src/api/user.js Прегледај датотеку

@@ -0,0 +1,24 @@
import request from '@/utils/request'

export function login(data) {
return request({
url: '/vue-admin-template/user/login',
method: 'post',
data
})
}

export function getInfo(token) {
return request({
url: '/vue-admin-template/user/info',
method: 'get',
params: { token }
})
}

export function logout() {
return request({
url: '/vue-admin-template/user/logout',
method: 'post'
})
}

BIN
src/assets/404_images/404.png Прегледај датотеку

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

BIN
src/assets/404_images/404_cloud.png Прегледај датотеку

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

BIN
src/assets/icon/iconfont.ttf Прегледај датотеку


BIN
src/assets/icon/iconfont.woff Прегледај датотеку


BIN
src/assets/icon/iconfont.woff2 Прегледај датотеку


+ 63
- 0
src/assets/icon/icons.css Прегледај датотеку

@@ -0,0 +1,63 @@
@font-face {
font-family: "iconfont"; /* Project id 2589074 */
src: url('./iconfont.woff2?t=1622700365894') format('woff2'),
url('./iconfont.woff?t=1622700365894') format('woff'),
url('./iconfont.ttf?t=1622700365894') format('truetype');
}

.icon {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

.icon-table:before {
content: "\e652";
}

.icon-form:before {
content: "\e653";
}

.icon-eye-open:before {
content: "\e654";
}

.icon-dashboard:before {
content: "\e655";
}

.icon-nested:before {
content: "\e64b";
}

.icon-example:before {
content: "\e64c";
}

.icon-link:before {
content: "\e64d";
}

.icon-eye:before {
content: "\e67d";
}

.icon-hamburger:before {
content: "\e615";
}

.icon-tree:before {
content: "\e64f";
}

.icon-user:before {
content: "\e650";
}

.icon-password:before {
content: "\e651";
}


BIN
src/assets/img/home.png Прегледај датотеку

Before After
Width: 600  |  Height: 400  |  Size: 12 KiB

+ 35
- 0
src/assets/styles/btn.scss Прегледај датотеку

@@ -0,0 +1,35 @@
// base color
$blue:#324157;
$light-blue:#3A71A8;
$red:#C03639;
$pink: #E65D6E;
$green: #30B08F;
$tiffany: #4AB7BD;
$yellow:#FEC171;
$panGreen: #30B08F;

// sidebar
$menuText:#bfcbd9;
$menuActiveText:#409EFF;
$subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951

$menuBg:#304156;
$menuHover:#263445;

$subMenuBg:#1f2d3d;
$subMenuHover:#001528;

$sideBarWidth: 205px;

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

+ 121
- 0
src/assets/styles/eladmin.scss Прегледај датотеку

@@ -0,0 +1,121 @@
.head-container {
padding-bottom: 10px;
.filter-item {
width: 150px;
display: inline-block;
vertical-align: middle;
margin: 0 3px 10px 0;
input {
height: 30.5px;
line-height: 30.5px;
}
}
.el-form-item-label {
margin: 0 3px 9px 0;
display: inline-block;
text-align: right;
vertical-align: middle;
font-size: 14px;
color: #606266;
line-height: 30.5px;
padding: 0 7px 0 7px;
}
.el-button{
width: auto;
}
.el-button+.el-button {
margin-left: 0 !important;
}
.el-select__caret.el-input__icon.el-icon-arrow-up{
line-height: 30.5px;
}
.date-item {
display: inline-block;
vertical-align: middle;
margin: 0 3px 10px 0;
height: 30.5px !important;
width: 230px !important;
}
}
.el-avatar {
display: inline-block;
text-align: center;
background: #ccc;
color: #fff;
white-space: nowrap;
position: relative;
overflow: hidden;
vertical-align: middle;
width: 32px;
height: 32px;
line-height: 32px;
border-radius: 16px;
}

.logo-con{
height: 60px;
padding: 13px 0 0;
img{
height: 32px;
width: 135px;
display: block;
//margin: 0 auto;
}
}

#el-login-footer {
height: 40px;
line-height: 40px;
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
color: #fff;
font-family: Arial, serif;
font-size: 12px;
letter-spacing: 1px;
}

#el-main-footer {
background: none repeat scroll 0 0 white;
border-top: 1px solid #e7eaec;
overflow: hidden;
padding: 10px 6px 0 6px;
height: 33px;
font-size: 0.7rem !important;
color: #7a8b9a;
letter-spacing: 0.8px;
font-family: Arial, sans-serif !important;
position: fixed;
bottom: 0;
z-index: 99;
width: 100%;
}
.eladmin-upload {
border: 1px dashed #c0ccda;
border-radius: 5px;
height: 45px;
line-height: 45px;
width: 368px;
}
.my-blockquote{
margin: 0 0 10px;
padding: 15px;
line-height: 22px;
border-left: 5px solid #00437B;
border-radius: 0 2px 2px 0;
background-color: #f2f2f2;
}
.my-code{
position: relative;
padding: 15px;
line-height: 20px;
border-left: 5px solid #ddd;
color: #333;
font-family: Courier New, serif;
font-size: 12px
}

.el-tabs{
margin-bottom: 25px;
}

+ 70
- 0
src/assets/styles/element-ui.scss Прегледај датотеку

@@ -0,0 +1,70 @@
// 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;
}

// fix date-picker ui bug in filter-item
.el-range-editor.el-input__inner {
display: inline-flex !important;
}

//全局覆盖样式
//用于解决select标签无法独占一行的情况
.resetform {
width: 100%;
height: auto;
.el-select {
width: 100%;
}
.el-input__inner{
width: 100%;
}
.el-cascader{
width: 100%;
}
}

+ 31
- 0
src/assets/styles/element-variables.scss Прегледај датотеку

@@ -0,0 +1,31 @@
/**
* I think element-ui's default theme color is too light for long-term use.
* So I modified the default color and you can modify it to your liking.
**/

/* theme color */
$--color-primary: #1890ff;
$--color-success: #13ce66;
$--color-warning: #FFBA00;
$--color-danger: #ff4949;
// $--color-info: #1E1E1E;

$--button-font-weight: 400;

// $--color-text-regular: #1f2d3d;

$--border-color-light: #dfe4ed;
$--border-color-lighter: #e6ebf5;

$--table-border:1px solid#dfe6ec;

/* icon font path, required */
$--font-path: '~element-ui/lib/theme-chalk/fonts';

@import "../../../node_modules/element-ui/packages/theme-chalk/src/index";

// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
theme: $--color-primary;
}

+ 81
- 0
src/assets/styles/index.scss Прегледај датотеку

@@ -0,0 +1,81 @@
@import './variables.scss';
@import './mixin.scss';
@import './transition.scss';
@import './element-ui.scss';
@import './sidebar.scss';
@import './eladmin.scss';
@import './btn.scss';

body {
height: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}

label {
font-weight: 700;
}

html {
height: 100%;
box-sizing: border-box;
}

#app {
height: 100%;
}
*,
*:before,
*:after {
box-sizing: inherit;
}

a:focus,
a:active {
outline: none;
}

a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}

div:focus {
outline: none;
}

.clearfix {
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
}

// main-container global css
.app-container {
padding: 20px 20px 70px 20px;
height: calc(100vh - 50px);
overflow: auto;
}

//竖向自适应布局 start
.flex_fixed {
flex: none;
}
.flex_auto {
flex: 1;
}
.flex_parent {
display: flex;
flex-direction: column;
}
//竖向自适应布局 end

+ 28
- 0
src/assets/styles/mixin.scss Прегледај датотеку

@@ -0,0 +1,28 @@
@mixin clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}

@mixin scrollBar {
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}

&::-webkit-scrollbar {
width: 6px;
}

&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}

@mixin relative {
position: relative;
width: 100%;
height: 100%;
}

+ 226
- 0
src/assets/styles/sidebar.scss Прегледај датотеку

@@ -0,0 +1,226 @@
#app {
overflow: hidden;
.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, .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, .icon {
margin-left: 19px;
}
}
}

.el-submenu {
overflow: hidden;

&>.el-submenu__title {
padding: 0 !important;

.svg-icon {
margin-left: 20px;
}

.sub-el-icon, .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, .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/assets/styles/transition.scss Прегледај датотеку

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

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

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

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

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

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

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

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

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

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

+ 25
- 0
src/assets/styles/variables.scss Прегледај датотеку

@@ -0,0 +1,25 @@
// sidebar
$menuText:#bfcbd9;
$menuActiveText:#409EFF;
$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951

$menuBg:#304156;
$menuHover:#263445;

$subMenuBg:#1f2d3d;
$subMenuHover:#001528;

$sideBarWidth: 210px;

// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
menuText: $menuText;
menuActiveText: $menuActiveText;
subMenuActiveText: $subMenuActiveText;
menuBg: $menuBg;
menuHover: $menuHover;
subMenuBg: $subMenuBg;
subMenuHover: $subMenuHover;
sideBarWidth: $sideBarWidth;
}

+ 78
- 0
src/components/Breadcrumb/index.vue Прегледај датотеку

@@ -0,0 +1,78 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>

<script>
import pathToRegexp from 'path-to-regexp'

export default {
data() {
return {
levelList: null
}
},
watch: {
$route() {
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
// only show routes with meta.title
const matched = this.$route.matched.filter(item => item.meta && item.meta.title)
/* const first = matched[0]

if (!this.isDashboard(first)) {
matched = [{ path: '/home', 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() === 'Home'.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>

+ 268
- 0
src/components/Crud/CRUD.operation.vue Прегледај датотеку

@@ -0,0 +1,268 @@
<template>
<div class="crud-opts">
<span class="crud-opts-left">
<!--左侧插槽-->
<slot name="left" />
<el-button
v-if="crud.optShow.add"
v-permission="permission.add"
class="filter-item"
size="mini"
type="primary"
icon="el-icon-plus"
@click="crud.toAdd"
>
新增
</el-button>
<el-button
v-if="crud.optShow.edit"
v-permission="permission.edit"
class="filter-item"
size="mini"
type="success"
icon="el-icon-edit"
:disabled="crud.selections.length !== 1"
@click="crud.toEdit(crud.selections[0])"
>
修改
</el-button>
<el-button
v-if="crud.optShow.del"
slot="reference"
v-permission="permission.del"
class="filter-item"
type="danger"
icon="el-icon-delete"
size="mini"
:loading="crud.delAllLoading"
:disabled="crud.selections.length === 0"
@click="toDelete(crud.selections)"
>
删除
</el-button>
<el-button
v-if="crud.optShow.download"
:loading="crud.downloadLoading"
:disabled="!crud.data.length"
class="filter-item"
size="mini"
type="warning"
icon="el-icon-download"
@click="crud.doExport"
>导出</el-button>
<!--右侧-->
<slot name="right" />
</span>
<el-button-group class="crud-opts-right">
<el-button
size="mini"
plain
type="info"
icon="el-icon-search"
@click="toggleSearch()"
/>
<el-button
size="mini"
icon="el-icon-refresh"
@click="crud.refresh()"
/>
<el-popover
placement="bottom-end"
width="150"
trigger="click"
>
<el-button
slot="reference"
size="mini"
icon="el-icon-s-grid"
>
<i
class="fa fa-caret-down"
aria-hidden="true"
/>
</el-button>
<el-checkbox
v-model="allColumnsSelected"
:indeterminate="allColumnsSelectedIndeterminate"
@change="handleCheckAllChange"
>
全选
</el-checkbox>
<el-checkbox
v-for="item in tableColumns"
:key="item.property"
v-model="item.visible"
@change="handleCheckedTableColumnsChange(item)"
>
{{ item.label }}
</el-checkbox>
</el-popover>
</el-button-group>
</div>
</template>
<script>
import CRUD, { crud } from '@crud/crud'

function sortWithRef(src, ref) {
const result = Object.assign([], ref)
let cursor = -1
src.forEach(e => {
const idx = result.indexOf(e)
if (idx === -1) {
cursor += 1
result.splice(cursor, 0, e)
} else {
cursor = idx
}
})
return result
}

export default {
mixins: [crud()],
props: {
permission: {
type: Object,
default: () => { return {} }
},
hiddenColumns: {
type: Array,
default: () => { return [] }
},
ignoreColumns: {
type: Array,
default: () => { return [] }
}
},
data() {
return {
tableColumns: [],
allColumnsSelected: true,
allColumnsSelectedIndeterminate: false,
tableUnwatcher: null,
// 忽略下次表格列变动
ignoreNextTableColumnsChange: false
}
},
watch: {
'crud.props.table'() {
this.updateTableColumns()
this.tableColumns.forEach(column => {
if (this.hiddenColumns.indexOf(column.property) !== -1) {
column.visible = false
this.updateColumnVisible(column)
}
})
},
'crud.props.table.store.states.columns'() {
this.updateTableColumns()
}
},
created() {
this.crud.updateProp('searchToggle', true)
},
methods: {
updateTableColumns() {
const table = this.crud.getTable()
if (!table) {
this.tableColumns = []
return
}
let cols = null
const columnFilter = e => e && e.type === 'default' && e.property && this.ignoreColumns.indexOf(e.property) === -1
const refCols = table.columns.filter(columnFilter)
if (this.ignoreNextTableColumnsChange) {
this.ignoreNextTableColumnsChange = false
return
}
this.ignoreNextTableColumnsChange = false
const columns = []
const fullTableColumns = table.$children.map(e => e.columnConfig).filter(columnFilter)
cols = sortWithRef(fullTableColumns, refCols)
cols.forEach(config => {
const column = {
property: config.property,
label: config.label,
visible: refCols.indexOf(config) !== -1
}
columns.push(column)
})
this.tableColumns = columns
},
toDelete(datas) {
this.$confirm(`确认删除选中的${datas.length}条数据?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.crud.delAllLoading = true
this.crud.doDelete(datas)
}).catch(() => {
})
},
handleCheckAllChange(val) {
if (val === false) {
this.allColumnsSelected = true
return
}
this.tableColumns.forEach(column => {
if (!column.visible) {
column.visible = true
this.updateColumnVisible(column)
}
})
this.allColumnsSelected = val
this.allColumnsSelectedIndeterminate = false
},
handleCheckedTableColumnsChange(item) {
let totalCount = 0
let selectedCount = 0
this.tableColumns.forEach(column => {
++totalCount
selectedCount += column.visible ? 1 : 0
})
if (selectedCount === 0) {
this.crud.notify('请至少选择一列', CRUD.NOTIFICATION_TYPE.WARNING)
this.$nextTick(function() {
item.visible = true
})
return
}
this.allColumnsSelected = selectedCount === totalCount
this.allColumnsSelectedIndeterminate = selectedCount !== totalCount && selectedCount !== 0
this.updateColumnVisible(item)
},
updateColumnVisible(item) {
const table = this.crud.props.table
const vm = table.$children.find(e => e.prop === item.property)
const columnConfig = vm.columnConfig
if (item.visible) {
// 找出合适的插入点
const columnIndex = this.tableColumns.indexOf(item)
vm.owner.store.commit('insertColumn', columnConfig, columnIndex + 1, null)
} else {
vm.owner.store.commit('removeColumn', columnConfig, null)
}
this.ignoreNextTableColumnsChange = true
},
toggleSearch() {
this.crud.props.searchToggle = !this.crud.props.searchToggle
}
}
}
</script>

<style>
.crud-opts {
padding: 4px 0;
display: -webkit-flex;
display: flex;
align-items: center;
}
.crud-opts .crud-opts-right {
margin-left: auto;
}
.crud-opts .crud-opts-right span {
float: left;
}
</style>

+ 18
- 0
src/components/Crud/Pagination.vue Прегледај датотеку

@@ -0,0 +1,18 @@
<!--分页-->
<template>
<el-pagination
:page-size.sync="page.size"
:total="page.total"
:current-page.sync="page.page"
style="margin-top: 8px;"
layout="total, prev, pager, next, sizes"
@size-change="crud.sizeChangeHandler($event)"
@current-change="crud.pageChangeHandler"
/>
</template>
<script>
import { pagination } from '@crud/crud'
export default {
mixins: [pagination()]
}
</script>

+ 20
- 0
src/components/Crud/RR.operation.vue Прегледај датотеку

@@ -0,0 +1,20 @@
<!--搜索与重置-->
<template>
<span>
<el-button class="filter-item" size="mini" type="success" icon="el-icon-search" @click="crud.toQuery">搜索</el-button>
<el-button v-if="crud.optShow.reset" class="filter-item" size="mini" type="warning" icon="el-icon-refresh-left" @click="crud.resetQuery()">重置</el-button>
</span>
</template>
<script>
import { crud } from '@crud/crud'
export default {
mixins: [crud()],
props: {
itemClass: {
type: String,
required: false,
default: ''
}
}
}
</script>

+ 71
- 0
src/components/Crud/UD.operation.vue Прегледај датотеку

@@ -0,0 +1,71 @@
<template>
<div>
<el-button v-permission="permission.edit" :loading="crud.status.cu === 2" :disabled="disabledEdit" size="mini" type="primary" icon="el-icon-edit" @click="crud.toEdit(data)" />
<el-popover v-model="pop" v-permission="permission.del" placement="top" width="180" trigger="manual" @show="onPopoverShow" @hide="onPopoverHide">
<p>{{ msg }}</p>
<div style="text-align: right; margin: 0">
<el-button size="mini" type="text" @click="doCancel">取消</el-button>
<el-button :loading="crud.dataStatus[crud.getDataId(data)].delete === 2" type="primary" size="mini" @click="crud.doDelete(data)">确定</el-button>
</div>
<el-button slot="reference" :disabled="disabledDle" type="danger" icon="el-icon-delete" size="mini" @click="toDelete" />
</el-popover>
</div>
</template>
<script>
import CRUD, { crud } from '@crud/crud'
export default {
mixins: [crud()],
props: {
data: {
type: Object,
required: true
},
permission: {
type: Object,
required: true
},
disabledEdit: {
type: Boolean,
default: false
},
disabledDle: {
type: Boolean,
default: false
},
msg: {
type: String,
default: '确定删除本条数据吗?'
}
},
data() {
return {
pop: false
}
},
methods: {
doCancel() {
this.pop = false
this.crud.cancelDelete(this.data)
},
toDelete() {
this.pop = true
},
[CRUD.HOOK.afterDelete](crud, data) {
if (data === this.data) {
this.pop = false
}
},
onPopoverShow() {
setTimeout(() => {
document.addEventListener('click', this.handleDocumentClick)
}, 0)
},
onPopoverHide() {
document.removeEventListener('click', this.handleDocumentClick)
},
handleDocumentClick(event) {
this.pop = false
}
}
}
</script>

+ 864
- 0
src/components/Crud/crud.js Прегледај датотеку

@@ -0,0 +1,864 @@
import { initData, download } from '@/api/data'
import { parseTime, downloadFile } from '@/utils/index'
import Vue from 'vue'

/**
* CRUD配置
* @author moxun
* @param {*} options <br>
* @return crud instance.
* @example
* 要使用多crud时,请在关联crud的组件处使用crud-tag进行标记,如:<jobForm :job-status="dict.job_status" crud-tag="job" />
*/
function CRUD(options) {
const defaultOptions = {
tag: 'default',
// id字段名
idField: 'id',
// 标题
title: '',
// 请求数据的url
url: '',
// 表格数据
data: [],
// 选择项
selections: [],
// 待查询的对象
query: {},
// 查询数据的参数
params: {},
// Form 表单
form: {},
// 重置表单
defaultForm: () => {},
// 排序规则,默认 id 降序, 支持多字段排序 ['id,desc', 'createTime,asc']
sort: ['id,desc'],
// 等待时间
time: 50,
// CRUD Method
crudMethod: {
add: (form) => {},
del: (id) => {},
edit: (form) => {},
get: (id) => {}
},
// 主页操作栏显示哪些按钮
optShow: {
add: true,
edit: true,
del: true,
download: true,
reset: true
},
// 自定义一些扩展属性
props: {},
// 在主页准备
queryOnPresenterCreated: true,
// 调试开关
debug: false
}
options = mergeOptions(defaultOptions, options)
const data = {
...options,
// 记录数据状态
dataStatus: {},
status: {
add: CRUD.STATUS.NORMAL,
edit: CRUD.STATUS.NORMAL,
// 添加或编辑状态
get cu() {
if (this.add === CRUD.STATUS.NORMAL && this.edit === CRUD.STATUS.NORMAL) {
return CRUD.STATUS.NORMAL
} else if (this.add === CRUD.STATUS.PREPARED || this.edit === CRUD.STATUS.PREPARED) {
return CRUD.STATUS.PREPARED
} else if (this.add === CRUD.STATUS.PROCESSING || this.edit === CRUD.STATUS.PROCESSING) {
return CRUD.STATUS.PROCESSING
}
throw new Error('wrong crud\'s cu status')
},
// 标题
get title() {
return this.add > CRUD.STATUS.NORMAL ? `新增${crud.title}` : this.edit > CRUD.STATUS.NORMAL ? `编辑${crud.title}` : crud.title
}
},
msg: {
submit: '提交成功',
add: '新增成功',
edit: '编辑成功',
del: '删除成功'
},
page: {
// 页码
page: 0,
// 每页数据条数
size: 10,
// 总数据条数
total: 0
},
// 整体loading
loading: false,
// 导出的 Loading
downloadLoading: false,
// 删除的 Loading
delAllLoading: false
}
const methods = {
/**
* 通用的提示
*/
submitSuccessNotify() {
crud.notify(crud.msg.submit, CRUD.NOTIFICATION_TYPE.SUCCESS)
},
addSuccessNotify() {
crud.notify(crud.msg.add, CRUD.NOTIFICATION_TYPE.SUCCESS)
},
editSuccessNotify() {
crud.notify(crud.msg.edit, CRUD.NOTIFICATION_TYPE.SUCCESS)
},
delSuccessNotify() {
crud.notify(crud.msg.del, CRUD.NOTIFICATION_TYPE.SUCCESS)
},
// 搜索
toQuery() {
crud.page.page = 1
crud.refresh()
},
// 刷新
refresh() {
if (!callVmHook(crud, CRUD.HOOK.beforeRefresh)) {
return
}
return new Promise((resolve, reject) => {
crud.loading = true
// 请求数据
initData(crud.url, crud.getQueryParams()).then(data => {
const table = crud.getTable()
if (table && table.lazy) { // 懒加载子节点数据,清掉已加载的数据
table.store.states.treeData = {}
table.store.states.lazyTreeNodeMap = {}
}
crud.page.total = Number(data.total)
crud.data = data.list
crud.resetDataStatus()
// time 毫秒后显示表格
setTimeout(() => {
crud.loading = false
callVmHook(crud, CRUD.HOOK.afterRefresh)
}, crud.time)
resolve(data)
}).catch(err => {
crud.loading = false
reject(err)
})
})
},
/**
* 启动添加
*/
toAdd() {
crud.resetForm()
if (!(callVmHook(crud, CRUD.HOOK.beforeToAdd, crud.form) && callVmHook(crud, CRUD.HOOK.beforeToCU, crud.form))) {
return
}
crud.status.add = CRUD.STATUS.PREPARED
callVmHook(crud, CRUD.HOOK.afterToAdd, crud.form)
callVmHook(crud, CRUD.HOOK.afterToCU, crud.form)
},
/**
* 启动编辑
* @param {*} data 数据项
*/
toEdit(data) {
crud.resetForm(JSON.parse(JSON.stringify(data)))
if (!(callVmHook(crud, CRUD.HOOK.beforeToEdit, crud.form) && callVmHook(crud, CRUD.HOOK.beforeToCU, crud.form))) {
return
}
crud.status.edit = CRUD.STATUS.PREPARED
crud.getDataStatus(crud.getDataId(data)).edit = CRUD.STATUS.PREPARED
callVmHook(crud, CRUD.HOOK.afterToEdit, crud.form)
callVmHook(crud, CRUD.HOOK.afterToCU, crud.form)
},
/**
* 启动删除
* @param {*} data 数据项
*/
toDelete(data) {
crud.getDataStatus(crud.getDataId(data)).delete = CRUD.STATUS.PREPARED
},
/**
* 取消删除
* @param {*} data 数据项
*/
cancelDelete(data) {
if (!callVmHook(crud, CRUD.HOOK.beforeDeleteCancel, data)) {
return
}
crud.getDataStatus(crud.getDataId(data)).delete = CRUD.STATUS.NORMAL
callVmHook(crud, CRUD.HOOK.afterDeleteCancel, data)
},
/**
* 取消新增/编辑
*/
cancelCU() {
const addStatus = crud.status.add
const editStatus = crud.status.edit
if (addStatus === CRUD.STATUS.PREPARED) {
if (!callVmHook(crud, CRUD.HOOK.beforeAddCancel, crud.form)) {
return
}
crud.status.add = CRUD.STATUS.NORMAL
}
if (editStatus === CRUD.STATUS.PREPARED) {
if (!callVmHook(crud, CRUD.HOOK.beforeEditCancel, crud.form)) {
return
}
crud.status.edit = CRUD.STATUS.NORMAL
crud.getDataStatus(crud.getDataId(crud.form)).edit = CRUD.STATUS.NORMAL
}
crud.resetForm()
if (addStatus === CRUD.STATUS.PREPARED) {
callVmHook(crud, CRUD.HOOK.afterAddCancel, crud.form)
}
if (editStatus === CRUD.STATUS.PREPARED) {
callVmHook(crud, CRUD.HOOK.afterEditCancel, crud.form)
}
// 清除表单验证
if (crud.findVM('form').$refs['form']) {
crud.findVM('form').$refs['form'].clearValidate()
}
},
/**
* 提交新增/编辑
*/
submitCU() {
if (!callVmHook(crud, CRUD.HOOK.beforeValidateCU)) {
return
}
crud.findVM('form').$refs['form'].validate(valid => {
if (!valid) {
return
}
if (!callVmHook(crud, CRUD.HOOK.afterValidateCU)) {
return
}
if (crud.status.add === CRUD.STATUS.PREPARED) {
crud.doAdd()
} else if (crud.status.edit === CRUD.STATUS.PREPARED) {
crud.doEdit()
}
})
},
/**
* 执行添加
*/
doAdd() {
if (!callVmHook(crud, CRUD.HOOK.beforeSubmit)) {
return
}
crud.status.add = CRUD.STATUS.PROCESSING
crud.crudMethod.add(crud.form).then(() => {
crud.status.add = CRUD.STATUS.NORMAL
crud.resetForm()
crud.addSuccessNotify()
callVmHook(crud, CRUD.HOOK.afterSubmit)
crud.toQuery()
}).catch(() => {
crud.status.add = CRUD.STATUS.PREPARED
callVmHook(crud, CRUD.HOOK.afterAddError)
})
},
/**
* 执行编辑
*/
doEdit() {
if (!callVmHook(crud, CRUD.HOOK.beforeSubmit)) {
return
}
crud.status.edit = CRUD.STATUS.PROCESSING
crud.crudMethod.edit(crud.form).then(() => {
crud.status.edit = CRUD.STATUS.NORMAL
crud.getDataStatus(crud.getDataId(crud.form)).edit = CRUD.STATUS.NORMAL
crud.editSuccessNotify()
crud.resetForm()
callVmHook(crud, CRUD.HOOK.afterSubmit)
crud.refresh()
}).catch(() => {
crud.status.edit = CRUD.STATUS.PREPARED
callVmHook(crud, CRUD.HOOK.afterEditError)
})
},
/**
* 执行删除
* @param {*} data 数据项
*/
doDelete(data) {
let delAll = false
let dataStatus
const ids = []
if (data instanceof Array) {
delAll = true
data.forEach(val => {
ids.push(this.getDataId(val))
})
} else {
ids.push(this.getDataId(data))
dataStatus = crud.getDataStatus(this.getDataId(data))
}
if (!callVmHook(crud, CRUD.HOOK.beforeDelete, data)) {
return
}
if (!delAll) {
dataStatus.delete = CRUD.STATUS.PROCESSING
}
return crud.crudMethod.del(ids).then(() => {
if (delAll) {
crud.delAllLoading = false
} else dataStatus.delete = CRUD.STATUS.PREPARED
crud.dleChangePage(1)
crud.delSuccessNotify()
callVmHook(crud, CRUD.HOOK.afterDelete, data)
crud.refresh()
}).catch(() => {
if (delAll) {
crud.delAllLoading = false
} else dataStatus.delete = CRUD.STATUS.PREPARED
})
},
/**
* 通用导出
*/
doExport() {
crud.downloadLoading = true
download(crud.url + '/download', crud.getQueryParams()).then(result => {
downloadFile(result, crud.title + '数据', 'xlsx')
crud.downloadLoading = false
}).catch(() => {
crud.downloadLoading = false
})
},
/**
* 获取查询参数
*/
getQueryParams: function() {
// 清除参数无值的情况
Object.keys(crud.query).length !== 0 && Object.keys(crud.query).forEach(item => {
if (crud.query[item] === null || crud.query[item] === '') crud.query[item] = undefined
})
Object.keys(crud.params).length !== 0 && Object.keys(crud.params).forEach(item => {
if (crud.params[item] === null || crud.params[item] === '') crud.params[item] = undefined
})
return {
pageNum: crud.page.page,
pageSize: crud.page.size,
...crud.query,
...crud.params
}
},
// 当前页改变
pageChangeHandler(e) {
crud.page.page = e
crud.refresh()
},
// 每页条数改变
sizeChangeHandler(e) {
crud.page.size = e
crud.page.page = 1
crud.refresh()
},
// 预防删除第二页最后一条数据时,或者多选删除第二页的数据时,页码错误导致请求无数据
dleChangePage(size) {
if (crud.data.length === size && crud.page.page !== 1) {
crud.page.page -= 1
}
},
// 选择改变
selectionChangeHandler(val) {
crud.selections = val
},
/**
* 重置查询参数
* @param {Boolean} toQuery 重置后进行查询操作
*/
resetQuery(toQuery = true) {
const defaultQuery = JSON.parse(JSON.stringify(crud.defaultQuery))
const query = crud.query
Object.keys(query).forEach(key => {
query[key] = defaultQuery[key]
})
// 重置参数
this.params = {}
if (toQuery) {
crud.toQuery()
}
},
/**
* 重置表单
* @param {Array} data 数据
*/
resetForm(data) {
const form = data || (typeof crud.defaultForm === 'object' ? JSON.parse(JSON.stringify(crud.defaultForm)) : crud.defaultForm.apply(crud.findVM('form')))
const crudFrom = crud.form
for (const key in form) {
// eslint-disable-next-line no-prototype-builtins
if (crudFrom.hasOwnProperty(key)) {
crudFrom[key] = form[key]
} else {
Vue.set(crudFrom, key, form[key])
}
}
// add by ghl 2020-10-04 页面重复添加信息时,下拉框的校验会存在,需要找工取消
if (crud.findVM('form').$refs['form']) {
crud.findVM('form').$refs['form'].clearValidate()
}
},
/**
* 重置数据状态
*/
resetDataStatus() {
const dataStatus = {}
function resetStatus(datas) {
datas.forEach(e => {
dataStatus[crud.getDataId(e)] = {
delete: 0,
edit: 0
}
if (e.children) {
resetStatus(e.children)
}
})
}
resetStatus(crud.data)
crud.dataStatus = dataStatus
},
/**
* 获取数据状态
* @param {Number | String} id 数据项id
*/
getDataStatus(id) {
return crud.dataStatus[id]
},
/**
* 用于树形表格多选, 选中所有
* @param selection
*/
selectAllChange(selection) {
// 如果选中的数目与请求到的数目相同就选中子节点,否则就清空选中
if (selection && selection.length === crud.data.length) {
selection.forEach(val => {
crud.selectChange(selection, val)
})
} else {
crud.getTable().clearSelection()
}
},
/**
* 用于树形表格多选,单选的封装
* @param selection
* @param row
*/
selectChange(selection, row) {
// 如果selection中存在row代表是选中,否则是取消选中
if (selection.find(val => { return crud.getDataId(val) === crud.getDataId(row) })) {
if (row.children) {
row.children.forEach(val => {
crud.getTable().toggleRowSelection(val, true)
selection.push(val)
if (val.children) {
crud.selectChange(selection, val)
}
})
}
} else {
crud.toggleRowSelection(selection, row)
}
},
/**
* 切换选中状态
* @param selection
* @param data
*/
toggleRowSelection(selection, data) {
if (data.children) {
data.children.forEach(val => {
crud.getTable().toggleRowSelection(val, false)
if (val.children) {
crud.toggleRowSelection(selection, val)
}
})
}
},
findVM(type) {
return crud.vms.find(vm => vm && vm.type === type).vm
},
notify(title, type = CRUD.NOTIFICATION_TYPE.INFO) {
crud.vms[0].vm.$notify({
title,
type,
duration: 2500
})
},
updateProp(name, value) {
Vue.set(crud.props, name, value)
},
getDataId(data) {
return data[this.idField]
},
getTable() {
return this.findVM('presenter').$refs.table
},
attchTable() {
const table = this.getTable()
this.updateProp('table', table)
const that = this
table.$on('expand-change', (row, expanded) => {
if (!expanded) {
return
}
const lazyTreeNodeMap = table.store.states.lazyTreeNodeMap
row.children = lazyTreeNodeMap[crud.getDataId(row)]
if (row.children) {
row.children.forEach(ele => {
const id = crud.getDataId(ele)
if (that.dataStatus[id] === undefined) {
that.dataStatus[id] = {
delete: 0,
edit: 0
}
}
})
}
})
}
}
const crud = Object.assign({}, data)
// 可观测化
Vue.observable(crud)
// 附加方法
Object.assign(crud, methods)
// 记录初始默认的查询参数,后续重置查询时使用
Object.assign(crud, {
defaultQuery: JSON.parse(JSON.stringify(data.query)),
// 预留4位存储:组件 主页、头部、分页、表单,调试查看也方便找
vms: Array(4),
/**
* 注册组件实例
* @param {String} type 类型
* @param {*} vm 组件实例
* @param {Number} index 该参数内部使用
*/
registerVM(type, vm, index = -1) {
const vmObj = {
type,
vm: vm
}
if (index < 0) {
this.vms.push(vmObj)
return
}
if (index < 4) { // 内置预留vm数
this.vms[index] = vmObj
return
}
this.vms.length = Math.max(this.vms.length, index)
this.vms.splice(index, 1, vmObj)
},
/**
* 取消注册组件实例
* @param {*} vm 组件实例
*/
unregisterVM(type, vm) {
for (let i = this.vms.length - 1; i >= 0; i--) {
if (this.vms[i] === undefined) {
continue
}
if (this.vms[i].type === type && this.vms[i].vm === vm) {
if (i < 4) { // 内置预留vm数
this.vms[i] = undefined
} else {
this.vms.splice(i, 1)
}
break
}
}
}
})
// 冻结处理,需要扩展数据的话,使用crud.updateProp(name, value),以crud.props.name形式访问,这个是响应式的,可以做数据绑定
Object.freeze(crud)
return crud
}

// hook VM
function callVmHook(crud, hook) {
if (crud.debug) {
console.log('callVmHook: ' + hook)
}
const tagHook = crud.tag ? hook + '$' + crud.tag : null
let ret = true
const nargs = [crud]
for (let i = 2; i < arguments.length; ++i) {
nargs.push(arguments[i])
}
// 有些组件扮演了多个角色,调用钩子时,需要去重
const vmSet = new Set()
crud.vms.forEach(vm => vm && vmSet.add(vm.vm))
vmSet.forEach(vm => {
if (vm[hook]) {
ret = vm[hook].apply(vm, nargs) !== false && ret
}
if (tagHook && vm[tagHook]) {
ret = vm[tagHook].apply(vm, nargs) !== false && ret
}
})
return ret
}

function mergeOptions(src, opts) {
const optsRet = {
...src
}
for (const key in src) {
// eslint-disable-next-line no-prototype-builtins
if (opts.hasOwnProperty(key)) {
optsRet[key] = opts[key]
}
}
return optsRet
}

/**
* 查找crud
* @param {*} vm
* @param {string} tag
*/
function lookupCrud(vm, tag) {
tag = tag || vm.$attrs['crud-tag'] || 'default'
// function lookupCrud(vm, tag) {
if (vm.$crud) {
const ret = vm.$crud[tag]
if (ret) {
return ret
}
}
return vm.$parent ? lookupCrud(vm.$parent, tag) : undefined
}

/**
* crud主页
*/
function presenter(crud) {
if (crud) {
console.warn('[CRUD warn]: ' + 'please use $options.cruds() { return CRUD(...) or [CRUD(...), ...] }')
}
return {
data() {
// 在data中返回crud,是为了将crud与当前实例关联,组件观测crud相关属性变化
return {
crud: this.crud
}
},
beforeCreate() {
this.$crud = this.$crud || {}
let cruds = this.$options.cruds instanceof Function ? this.$options.cruds() : crud
if (!(cruds instanceof Array)) {
cruds = [cruds]
}
cruds.forEach(ele => {
if (this.$crud[ele.tag]) {
console.error('[CRUD error]: ' + 'crud with tag [' + ele.tag + ' is already exist')
}
this.$crud[ele.tag] = ele
ele.registerVM('presenter', this, 0)
})
this.crud = this.$crud['defalut'] || cruds[0]
},
methods: {
parseTime
},
created() {
for (const k in this.$crud) {
if (this.$crud[k].queryOnPresenterCreated) {
this.$crud[k].toQuery()
}
}
},
destroyed() {
for (const k in this.$crud) {
this.$crud[k].unregisterVM('presenter', this)
}
},
mounted() {
// 如果table未实例化(例如使用了v-if),请稍后在适当时机crud.attchTable刷新table信息
if (this.$refs.table !== undefined) {
this.crud.attchTable()
}
}
}
}

/**
* 头部
*/
function header() {
return {
data() {
return {
crud: this.crud,
query: this.crud.query
}
},
beforeCreate() {
this.crud = lookupCrud(this)
this.crud.registerVM('header', this, 1)
},
destroyed() {
this.crud.unregisterVM('header', this)
}
}
}

/**
* 分页
*/
function pagination() {
return {
data() {
return {
crud: this.crud,
page: this.crud.page
}
},
beforeCreate() {
this.crud = lookupCrud(this)
this.crud.registerVM('pagination', this, 2)
},
destroyed() {
this.crud.unregisterVM('pagination', this)
}
}
}

/**
* 表单
*/
function form(defaultForm) {
return {
data() {
return {
crud: this.crud,
form: this.crud.form
}
},
beforeCreate() {
this.crud = lookupCrud(this)
this.crud.registerVM('form', this, 3)
},
created() {
this.crud.defaultForm = defaultForm
this.crud.resetForm()
},
destroyed() {
this.crud.unregisterVM('form', this)
}
}
}

/**
* crud
*/
function crud(options = {}) {
const defaultOptions = {
type: undefined
}
options = mergeOptions(defaultOptions, options)
return {
data() {
return {
crud: this.crud
}
},
beforeCreate() {
this.crud = lookupCrud(this)
this.crud.registerVM(options.type, this)
},
destroyed() {
this.crud.unregisterVM(options.type, this)
}
}
}

/**
* CRUD钩子
*/
CRUD.HOOK = {
/** 刷新 - 之前 */
beforeRefresh: 'beforeCrudRefresh',
/** 刷新 - 之后 */
afterRefresh: 'afterCrudRefresh',
/** 删除 - 之前 */
beforeDelete: 'beforeCrudDelete',
/** 删除 - 之后 */
afterDelete: 'afterCrudDelete',
/** 删除取消 - 之前 */
beforeDeleteCancel: 'beforeCrudDeleteCancel',
/** 删除取消 - 之后 */
afterDeleteCancel: 'afterCrudDeleteCancel',
/** 新建 - 之前 */
beforeToAdd: 'beforeCrudToAdd',
/** 新建 - 之后 */
afterToAdd: 'afterCrudToAdd',
/** 编辑 - 之前 */
beforeToEdit: 'beforeCrudToEdit',
/** 编辑 - 之后 */
afterToEdit: 'afterCrudToEdit',
/** 开始 "新建/编辑" - 之前 */
beforeToCU: 'beforeCrudToCU',
/** 开始 "新建/编辑" - 之后 */
afterToCU: 'afterCrudToCU',
/** "新建/编辑" 验证 - 之前 */
beforeValidateCU: 'beforeCrudValidateCU',
/** "新建/编辑" 验证 - 之后 */
afterValidateCU: 'afterCrudValidateCU',
/** 添加取消 - 之前 */
beforeAddCancel: 'beforeCrudAddCancel',
/** 添加取消 - 之后 */
afterAddCancel: 'afterCrudAddCancel',
/** 编辑取消 - 之前 */
beforeEditCancel: 'beforeCrudEditCancel',
/** 编辑取消 - 之后 */
afterEditCancel: 'afterCrudEditCancel',
/** 提交 - 之前 */
beforeSubmit: 'beforeCrudSubmitCU',
/** 提交 - 之后 */
afterSubmit: 'afterCrudSubmitCU',
afterAddError: 'afterCrudAddError',
afterEditError: 'afterCrudEditError'
}

/**
* CRUD状态
*/
CRUD.STATUS = {
NORMAL: 0,
PREPARED: 1,
PROCESSING: 2
}

/**
* CRUD通知类型
*/
CRUD.NOTIFICATION_TYPE = {
SUCCESS: 'success',
WARNING: 'warning',
INFO: 'info',
ERROR: 'error'
}

export default CRUD

export {
presenter,
header,
form,
pagination,
crud
}

+ 35
- 0
src/components/Hamburger/index.vue Прегледај датотеку

@@ -0,0 +1,35 @@
<template>
<div style="padding: 0 15px;" @click="toggleClick">
<i class="icon icon-hamburger hamburger" :class="{'is-active': !isActive}" />
</div>
</template>

<script>
export default {
name: 'Hamburger',
props: {
isActive: {
type: Boolean,
default: false
}
},
methods: {
toggleClick() {
this.$emit('toggleClick')
}
}
}
</script>

<style scoped>
.hamburger {
vertical-align: middle;
font-size: 20px;
display:inline-block;
transform: rotate(180deg);
}

.hamburger.is-active {
transform: rotate(0deg);
}
</style>

+ 13
- 0
src/components/Permission/index.js Прегледај датотеку

@@ -0,0 +1,13 @@
import permission from './permission'

const install = function(Vue) {
Vue.directive('permission', permission)
}

if (window.Vue) {
window['permission'] = permission
Vue.use(install); // eslint-disable-line
}

permission.install = install
export default permission

+ 21
- 0
src/components/Permission/permission.js Прегледај датотеку

@@ -0,0 +1,21 @@
import store from '@/store'

export default {
inserted(el, binding) {
const { value } = binding
const roles = store.getters && store.getters.roles
if (value && value instanceof Array) {
if (value.length > 0) {
const permissionRoles = value
const hasPermission = roles.some(role => {
return permissionRoles.includes(role)
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
}
} else {
throw new Error(`使用方式: v-permission="['admin','editor']"`)
}
}
}

+ 208
- 0
src/components/ueditor/index.vue Прегледај датотеку

@@ -0,0 +1,208 @@
<template>
<div>
<!-- @beforeInit="addCustomButtom" -->
<VueUeditorWrap
class="ueditor"
:id="setSessionid()"
@ready="readyCustom"
v-model="content"
:config="config"
></VueUeditorWrap>
</div>
</template>

<script>
import VueUeditorWrap from 'vue-ueditor-wrap'
export default {
components: {
VueUeditorWrap
},
props: {
btnShow: {
type: Boolean,
default: false
},
comeContent: {
type: String,
default: ''
},
zIndex: {
type: Number,
default: 999
},
isDisabled: {
type: Number,
default: 0
}
},
data() {
return {
content: '',
editor: null,
config: {
toolbars: [
[
'source', // 源代码
'undo', // 撤销
'redo', // 重做
'bold', // 加粗
'indent', // 首行缩进
// 'snapscreen', //截 图
'italic', // 斜体
'underline', // 下划线
'strikethrough', // 删除线
'subscript', // 下标
'fontborder', // 字符边框
'superscript', // 上标
'formatmatch', // 格式刷
'blockquote', // 引用
// 'pasteplain', // 纯文本粘贴模式
'selectall', // 全选
// 'preview', // 预览
'horizontal', // 分隔线
'removeformat', // 清除格式
// 'time', // 时间
// 'date', // 日期
'unlink', // 取消链接
/* 'insertrow', // 前插入行
'insertcol', // 前插入列
'mergeright', // 右合并单元格
'mergedown', // 下合并单元格
'deleterow', // 删除行
'deletecol', // 删除列
'splittorows', // 拆分成行
'splittocols', // 拆分成列
'splittocells', // 完全拆分单元格
'deletecaption', // 删除表格标题
'inserttitle', // 插入标题
'mergecells', // 合并多个单元格
'deletetable', // 删除表格
'cleardoc', // 清空文档
'insertparagraphbeforetable', //" 表格前插入行"
'insertcode', // 代码语言 */
'fontfamily', // 字体
'fontsize', // 字号
'paragraph', // 段落格式
// 'edittable', // 表格属性
// 'edittd', // 单元格属性
'link', // 超链接
// 'emotion', // 表情
'spechars', // 特殊字符
// 'searchreplace', // 查询替换
'justifyleft', // 居左对齐
'justifyright', // 居右对齐
'justifycenter', // 居中对齐
'justifyjustify', // 两端对齐
'forecolor', // 字体颜色
'backcolor', // 背景色
'lineheight' // 行间距
/* 'insertorderedlist', // 有序列表
'insertunorderedlist', // 无序列表
'directionalityltr', // 从左向右输入
'directionalityrtl', // 从右向左输入
'rowspacingtop', // 段前距
'rowspacingbottom', // 段后距
'pagebreak', // 分页
'imagenone', // 默认
'imageleft', // 左浮动
'imageright', // 右浮动
'imagecenter', // 居中
'wordimage', // 图片转存
'edittip ', // 编辑提示
'customstyle', // 自定义标题
'touppercase', // 字母大写
'tolowercase', // 字母小写
'inserttable' // 插入表格 */
]
],
zIndex: this.zIndex,
autoHeightEnabled: false, // 编译器不自动被内容撑高
autoFloatEnabled: false,
initialFrameHeight: 300, // 初始容器高度
initialFrameWith: '100%',
postParams: this.postParams,
// serverUrl: process.env.baseUrl + '/product/uploadFiles', // 上传文件地址
UEDITOR_HOME_URL: process.env.VUE_APP_BASE_API + '/editor/UEditor/' // UEditor资源文件的存放路径
}
}
},
watch: {
content(val) {
this.$emit('editorChange', val)
},
comeContent: {
handler(newVal, oldVal) {
this.content = newVal
},
immediate: true,
deep: true
}
},
mounted() {

},
methods: {
readyCustom(editor) {
this.editor = editor
console.log(this.isDisabled)
if (this.isDisabled) this.editor.options.readonly = true
},
addCustomButtom(editorId) {
this.editorId = editorId
this.divID = 'key' + editorId
var _ = this
window.UE.registerUI(
'test-button',
function(editor, uiName) {
// 注册按钮执行时的 command 命令,使用命令默认就会带有回退操作
editor.registerCommand(uiName, {
execCommand: function() {
_.editorHandler = editor
}
})
// 创建一个 button
var btn = new window.UE.ui.Button({
// 按钮的名字
name: uiName,
// 提示
title: '上传图片',
// 需要添加的额外样式,可指定 icon 图标,图标路径参考常见问题 2
// cssRules: `background-image: url("${UploadImg}") !important;background-size: cover;`,
// 点击时执行的命令
onclick: function(res) {
// 点击新图标时,触发到点击input按钮的事件
editor.execCommand(uiName)
document.getElementById('key' + editorId).click()
}
})
// 当点到编辑内容上时,按钮要做的状态反射
editor.addListener('selectionchange', function() { })

// 因为你是添加 button,所以需要返回这个 button
return btn
}
// 0 /* 指定添加到工具栏上的哪个位置,默认时追加到最后 */
// editorId /* 指定这个 UI 是哪个编辑器实例上的,默认是页面上所有的编辑器都会添加这个按钮 */
)
},
setSessionid(num) {
var len = num || 32
var chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
var maxPos = chars.length
var pwd = ''
for (var i = 0; i < len; i++) {
pwd += chars.charAt(Math.floor(Math.random() * maxPos))
}
return pwd
}
}
}
</script>

<style lang="scss" scoped>
::v-deep.ueditor{
.edui-editor{
width: 100% !important;
}
}
</style>

+ 96
- 0
src/components/uploadFile/index.vue Прегледај датотеку

@@ -0,0 +1,96 @@
<!--
* @Autor: Chuting
* @Date: 2022-11-02 10:03:47
* @LastEditors: Chuting
* @LastEditTime: 2022-11-02 13:56:17
* @Description: 图片上传
-->
<template>
<el-upload
class="avatar-uploader"
action="#"
:show-file-list="false"
:before-upload="beforeAvatarUpload"
:http-request="uploadHttpRequest">
<template v-if="url">
<video v-if="type === 'video'" :src="url" class="avatar" controls="controls">
您的浏览器不支持视频播放
</video>
<img v-else :src="url" class="avatar">
</template>
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</template>
<script>
import { upload } from '@/api/common.js'
export default {
props: {
url: {
type: String,
default: ''
},
type: {
type: String,
default: ''
}
},
methods: {
beforeAvatarUpload(file) {
const isType = this.type === 'video' ? (file.type === ('video/mp4' || 'video/avi' || 'video/wmv' || 'video/mov')) : (file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg' || file.type === 'image/gif')
const isLt2M = this.type === 'video' ? file.size / 1024 / 1024 < 50 : file.size / 1024 < 500

if (!isType) {
let txt = this.type === 'video' ? '请上传正确的视频格式!' : '上传头像图片只能是 JPG/JPEG/PNG/GIF 格式!'
this.$message.error(txt)
return false
}
if (!isLt2M) {
let txt = this.type === 'video' ? '上传视频大小不能超过 50MB!!' : '上传图片大小不能超过 500KB!'
this.$message.error(txt)
return false
}
},
uploadHttpRequest(file) {
console.log('pid:', this.$route.params.pid)
let obj = {
'page': 'OPERATE',
'template': 'FORMWORK'
}
let _data = {
file: file.file
}
// OPERATE FORMWORK
upload(_data).then(res => {
if (res) {
this.$emit('update:url', res.url)
}
})
}
}
}
</script>
<style lang="scss" scoped>
::v-deep.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
::v-deep.avatar-uploader .el-upload:hover {
border-color: #409EFF;
}
::v-deep.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 150px;
height: 150px;
line-height: 150px;
text-align: center;
}
::v-deep.avatar {
width: 150px;
height: 150px;
display: block;
}
</style>

+ 40
- 0
src/layout/components/AppMain.vue Прегледај датотеку

@@ -0,0 +1,40 @@
<template>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<router-view :key="key" />
</transition>
</section>
</template>

<script>
export default {
name: 'AppMain',
computed: {
key() {
return this.$route.path
}
}
}
</script>

<style scoped>
.app-main {
/*50 = navbar */
min-height: calc(100vh - 50px);
width: 100%;
position: relative;
overflow: auto;
}
.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>

+ 139
- 0
src/layout/components/Navbar.vue Прегледај датотеку

@@ -0,0 +1,139 @@
<template>
<div class="navbar">
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />

<breadcrumb class="breadcrumb-container" />

<div class="right-menu">
<el-dropdown class="avatar-container" trigger="click">
<div class="avatar-wrapper">
<img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
<i class="el-icon-caret-bottom" />
</div>
<el-dropdown-menu slot="dropdown" class="user-dropdown">
<router-link to="/">
<el-dropdown-item>
Home
</el-dropdown-item>
</router-link>
<!-- <a target="_blank" href="https://github.com/PanJiaChen/vue-admin-template/">
<el-dropdown-item>Github</el-dropdown-item>
</a>
<a target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/#/">
<el-dropdown-item>Docs</el-dropdown-item>
</a> -->
<el-dropdown-item divided @click.native="logout">
<span style="display:block;">Log Out</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>

<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'

export default {
components: {
Breadcrumb,
Hamburger
},
computed: {
...mapGetters([
'sidebar',
'avatar'
])
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
async logout() {
// await this.$store.dispatch('user/logout')
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
}
}
}
</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;
}

.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: 30px;

.avatar-wrapper {
margin-top: 5px;
position: relative;

.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 10px;
}

.el-icon-caret-bottom {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
}
</style>

+ 26
- 0
src/layout/components/Sidebar/FixiOSBug.js Прегледај датотеку

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

+ 41
- 0
src/layout/components/Sidebar/Item.vue Прегледај датотеку

@@ -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(<i class={'icon icon-' + 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 Прегледај датотеку

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

+ 82
- 0
src/layout/components/Sidebar/Logo.vue Прегледај датотеку

@@ -0,0 +1,82 @@
<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="logo" 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="logo" class="sidebar-logo">
<h1 class="sidebar-title">{{ title }} </h1>
</router-link>
</transition>
</div>
</template>

<script>
export default {
name: 'SidebarLogo',
props: {
collapse: {
type: Boolean,
required: true
}
},
data() {
return {
title: '后台管理系统',
logo: 'https://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 Прегледај датотеку

@@ -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 Прегледај датотеку

@@ -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 '@/assets/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 Прегледај датотеку

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

+ 97
- 0
src/layout/index.vue Прегледај датотеку

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

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

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'
}
},
...mapGetters([
'isChild'
])
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
}
</script>

<style lang="scss" scoped>
@import "~@/assets/styles/mixin.scss";
@import "~@/assets/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 Прегледај датотеку

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

+ 85
- 0
src/main.js Прегледај датотеку

@@ -0,0 +1,85 @@
/*
* @Date: 2021-07-01 15:30:29
* @LastEditTime: 2022-07-27 10:47:36
* @Description: 增加微应用配置
*/
import './public-path'
import Vue from 'vue'

import 'normalize.css/normalize.css' // A modern alternative to CSS resets

import ElementUI from 'element-ui'
import './assets/styles/element-variables.scss'
import '@/assets/styles/index.scss' // global css

import App from './App'
import store from './store'
import router from './router'

import '@/assets/icon/icons.css'
// 权限控制
import checkPer from '@/utils/permission'
import permission from './components/Permission'
import { setToken } from '@/utils/auth'

Vue.use(checkPer)
Vue.use(permission)
Vue.use(ElementUI, {
size: 'small'
})

Vue.config.productionTip = false

let instance = null

function render({ container } = {}) {
instance = new Vue({
router,
store,
render: h => h(App)
}).$mount(container ? container.querySelector('#app') : '#app')
// index.html 里面的 id 需要改成 appVueHash,否则子项目无法独立运行
}

if (!window.__POWERED_BY_QIANKUN__) {
// 全局变量来判断环境
render()
}

/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('现在进入子应用content-cloud的bootstraped阶段')
}

/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
console.log('现在进入子应用content-cloud的mount周期', props)
setToken(props.token)
store.dispatch('user/setChild', false )
store.dispatch('user/setRoles', props.roles )
store.dispatch('user/setToken', props.token )
render(props)
}

/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
console.log('现在进入子应用content-cloud的unmount阶段')
instance.$destroy()
instance.$el.innerHTML = ''
instance = null
// router = null
}

/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props)
}

+ 335
- 0
src/mixins/crud.js Прегледај датотеку

@@ -0,0 +1,335 @@
import {
initData,
download
} from '@/api/data'
import {
parseTime,
downloadFile
} from '@/utils/index'

export default {
data() {
return {
// 表格数据
data: [],
// 排序规则,默认 id 降序, 支持多字段排序 ['id,desc', 'createTime,asc']
sort: ['id,desc'],
// 页码
page: 0,
// 每页数据条数
size: 10,
// 总数据条数
total: 0,
// 请求数据的url
url: '',
// 查询数据的参数
params: {},
// 待查询的对象
query: {},
// 等待时间
time: 50,
// 是否为新增类型的表单
isAdd: false,
// 导出的 Loading
downloadLoading: false,
// 表格 Loading 属性
loading: true,
// 删除 Loading 属性
delLoading: false,
delAllLoading: false,
// 弹窗属性
dialog: false,
// Form 表单
form: {},
// 重置表单
resetForm: {},
// 标题
title: ''
}
},
methods: {
parseTime,
downloadFile,
async init() {
if (!await this.beforeInit()) {
return
}
return new Promise((resolve, reject) => {
this.loading = true
// 请求数据
initData(this.url, this.getQueryParame()).then(data => {
this.total = data.totalElements
this.data = data.content
// time 毫秒后显示表格
setTimeout(() => {
this.loading = false
}, this.time)
resolve(data)
}).catch(err => {
this.loading = false
reject(err)
})
})
},
beforeInit() {
return true
},
getQueryParame: function() {
return {
pageNum: this.page,
pageSize: this.size,
sort: this.sort,
...this.query,
...this.params
}
},
// 改变页码
pageChange(e) {
this.page = e - 1
this.init()
},
// 改变每页显示数
sizeChange(e) {
this.page = 0
this.size = e
this.init()
},
// 预防删除第二页最后一条数据时,或者多选删除第二页的数据时,页码错误导致请求无数据
dleChangePage(size) {
if (size === undefined) {
size = 1
}
if (this.data.length === size && this.page !== 0) {
this.page = this.page - 1
}
},
// 查询方法
toQuery() {
this.page = 0
this.init()
},
/**
* 通用的提示封装
*/
submitSuccessNotify() {
this.$notify({
title: '提交成功',
type: 'success',
duration: 2500
})
},
addSuccessNotify() {
this.$notify({
title: '新增成功',
type: 'success',
duration: 2500
})
},
editSuccessNotify() {
this.$notify({
title: '编辑成功',
type: 'success',
duration: 2500
})
},
delSuccessNotify() {
this.$notify({
title: '删除成功',
type: 'success',
duration: 2500
})
},
notify(title, type) {
this.$notify({
title: title,
type: type,
duration: 2500
})
},
/**
* 删除前可以调用 beforeDelMethod 做一些操作
*/
beforeDelMethod() {
return true
},
/**
* 通用的删除
*/
delMethod(id) {
if (!this.beforeDelMethod()) {
return
}
this.delLoading = true
this.crudMethod.del(id).then(() => {
this.delLoading = false
this.$refs[id].doClose()
this.dleChangePage()
this.delSuccessNotify()
this.afterDelMethod()
this.init()
}).catch(() => {
this.delLoading = false
this.$refs[id].doClose()
})
},
afterDelMethod() {},
/**
* 多选删除提示
*/
beforeDelAllMethod() {
this.$confirm('你确定删除选中的数据吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.delAllMethod()
})
},
/**
* 多选删除
*/
delAllMethod() {
this.delAllLoading = true
const data = this.$refs.table.selection
const ids = []
for (let i = 0; i < data.length; i++) {
ids.push(data[i].id)
}
this.crudMethod.delAll(ids).then(() => {
this.delAllLoading = false
this.dleChangePage(ids.length)
this.init()
this.$notify({
title: '删除成功',
type: 'success',
duration: 2500
})
}).catch(() => {
this.delAllLoading = false
})
},
/**
* 显示新增弹窗前可以调用该方法
*/
beforeShowAddForm() {},
/**
* 显示新增弹窗
*/
showAddFormDialog() {
this.isAdd = true
this.resetForm = JSON.parse(JSON.stringify(this.form))
this.beforeShowAddForm()
this.dialog = true
},
/**
* 显示编辑弹窗前可以调用该方法
*/
beforeShowEditForm(data) {},
/**
* 显示编辑弹窗
*/
showEditFormDialog(data = '') {
this.isAdd = false
if (data) {
this.resetForm = JSON.parse(JSON.stringify(this.form))
this.form = JSON.parse(JSON.stringify(data))
}
this.beforeShowEditForm(data)
this.dialog = true
},
/**
* 新增方法
*/
addMethod() {
this.crudMethod.add(this.form).then(() => {
this.addSuccessNotify()
this.loading = false
this.afterAddMethod()
this.cancel()
this.init()
}).catch(() => {
this.loading = false
this.afterAddErrorMethod()
})
},
/**
* 新增后可以调用该方法
*/
afterAddMethod() {},
/**
* 新增失败后调用该方法
*/
afterAddErrorMethod() {},
/**
* 通用的编辑方法
*/
editMethod() {
this.crudMethod.edit(this.form).then(() => {
this.editSuccessNotify()
this.loading = false
this.afterEditMethod()
this.cancel()
this.init()
}).catch(() => {
this.loading = false
})
},
/**
* 编辑后可以调用该方法
*/
afterEditMethod() {},
/**
* 提交前可以调用该方法
*/
beforeSubmitMethod() {
return true
},
/**
* 提交
*/
submitMethod() {
if (!this.beforeSubmitMethod()) {
return
}
if (this.$refs['form']) {
this.$refs['form'].validate((valid) => {
if (valid) {
this.loading = true
if (this.isAdd) {
this.addMethod()
} else this.editMethod()
}
})
}
},
/**
* 隐藏弹窗
*/
cancel() {
this.dialog = false
if (this.$refs['form']) {
this.$refs['form'].clearValidate()
this.form = this.resetForm
}
},
/**
* 获取弹窗的标题
*/
getFormTitle() {
return this.isAdd ? `新增${this.title}` : `编辑${this.title}`
},
/**
* 通用导出
*/
downloadMethod() {
this.beforeInit()
this.downloadLoading = true
download(this.url + '/download', this.params).then(result => {
this.downloadFile(result, this.title + '数据', 'xlsx')
this.downloadLoading = false
}).catch(() => {
this.downloadLoading = false
})
}
}
}

+ 7
- 0
src/public-path.js Прегледај датотеку

@@ -0,0 +1,7 @@

/* eslint-disable camelcase */

if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}

+ 114
- 0
src/router/index.js Прегледај датотеку

@@ -0,0 +1,114 @@
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: '/home',
children: [{
path: 'home',
name: 'home',
component: () => import('@/views/home/index'),
meta: { title: 'Home', icon: 'dashboard' }
}]
},
{
path: '/homePage',
component: Layout,
alwaysShow: true,
redirect: '/homePage/carousel',
name: 'homePage',
meta: {
title: '首页',
icon: 'nested'
},
children: [
{
path: 'carousel',
component: () => import('@/views/homePage/carousel'),
meta: { title: '轮播' }
},
{
path: 'carHot',
component: () => import('@/views/homePage/carHot'),
meta: { title: '汽车资讯热门' }
},
{
path: 'banner1',
component: () => import('@/views/homePage/banner1'),
meta: { title: '广告位1' }
},
{
path: 'yyzHot',
component: () => import('@/views/homePage/yyzHot'),
meta: { title: '元宇宙日爆热门' }
},
{
path: 'banner2',
component: () => import('@/views/homePage/banner2'),
meta: { title: '广告位2' }
}
]
}

// 404 page must be placed at the end !!!
// { path: '*', redirect: '/404', hidden: true }
]
const createRouter = () => new Router({
base: window.__POWERED_BY_QIANKUN__ ? '/vdcop_auth/admin' : '/admin',
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 Прегледај датотеку

@@ -0,0 +1,16 @@
module.exports = {

title: '后台管理系统',

/**
* @type {boolean} true | false
* @description Whether fix the header
*/
fixedHeader: false,

/**
* @type {boolean} true | false
* @description Whether show the logo in sidebar
*/
sidebarLogo: false
}

+ 15
- 0
src/store/getters.js Прегледај датотеку

@@ -0,0 +1,15 @@
/*
* @Date: 2021-07-16 17:52:04
* @LastEditTime: 2022-07-27 10:48:11
* @Description:
*/
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,
isChild: state => state.user.isChild,
roles: state => state.user.roles
}
export default getters

+ 19
- 0
src/store/index.js Прегледај датотеку

@@ -0,0 +1,19 @@
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'

Vue.use(Vuex)

const store = new Vuex.Store({
modules: {
app,
settings,
user
},
getters
})

export default store

+ 48
- 0
src/store/modules/app.js Прегледај датотеку

@@ -0,0 +1,48 @@
import Cookies from 'js-cookie'

const state = {
sidebar: {
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
withoutAnimation: false
},
device: 'desktop'
}

const mutations = {
TOGGLE_SIDEBAR: state => {
state.sidebar.opened = !state.sidebar.opened
state.sidebar.withoutAnimation = false
if (state.sidebar.opened) {
Cookies.set('sidebarStatus', 1)
} else {
Cookies.set('sidebarStatus', 0)
}
},
CLOSE_SIDEBAR: (state, withoutAnimation) => {
Cookies.set('sidebarStatus', 0)
state.sidebar.opened = false
state.sidebar.withoutAnimation = withoutAnimation
},
TOGGLE_DEVICE: (state, device) => {
state.device = device
}
}

const actions = {
toggleSideBar({ commit }) {
commit('TOGGLE_SIDEBAR')
},
closeSideBar({ commit }, { withoutAnimation }) {
commit('CLOSE_SIDEBAR', withoutAnimation)
},
toggleDevice({ commit }, device) {
commit('TOGGLE_DEVICE', device)
}
}

export default {
namespaced: true,
state,
mutations,
actions
}

+ 32
- 0
src/store/modules/settings.js Прегледај датотеку

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


+ 114
- 0
src/store/modules/user.js Прегледај датотеку

@@ -0,0 +1,114 @@
import { login, logout, getInfo } from '@/api/user'
import { getToken, removeToken } from '@/utils/auth'
import { resetRouter } from '@/router'

const getDefaultState = () => {
return {
token: getToken(),
name: '',
avatar: '',
isChild: true,
roles: []
}
}

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_ISCHILD: (state, isChild) => {
state.isChild = isChild
},
SET_ROLES: (state, roles) => {
state.roles = roles
}
}

const actions = {
setChild({ commit }, isChild) {
commit('SET_ISCHILD', isChild)
},
setRoles({ commit }, roles) {
commit('SET_ROLES', roles)
},
setToken({ commit }, token) {
commit('SET_TOKEN', token)
},
// user login
/* login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
commit('SET_TOKEN', data.token)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
}, */

// get user info
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response

if (!data) {
return reject('Verification failed, please Login again.')
}

const { name, avatar } = data

commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
resolve(data)
}).catch(error => {
reject(error)
})
})
},

// user logout
logout({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
removeToken() // must remove token first
resetRouter()
commit('RESET_STATE')
resolve()
}).catch(error => {
reject(error)
})
})
},

// remove token
resetToken({ commit }) {
return new Promise(resolve => {
removeToken() // must remove token first
commit('RESET_STATE')
resolve()
})
}
}

export default {
namespaced: true,
state,
mutations,
actions
}


+ 15
- 0
src/utils/auth.js Прегледај датотеку

@@ -0,0 +1,15 @@
import Cookies from 'js-cookie'

const TokenKey = 'token'

export function getToken() {
return Cookies.get(TokenKey)
}

export function setToken(token) {
return Cookies.set(TokenKey, token)
}

export function removeToken() {
return Cookies.remove(TokenKey)
}

+ 36
- 0
src/utils/clipboard.js Прегледај датотеку

@@ -0,0 +1,36 @@
import Vue from 'vue'
import Clipboard from 'clipboard'

function clipboardSuccess() {
Vue.prototype.$message({
message: 'Copy successfully',
type: 'success',
duration: 1500
})
}

function clipboardError() {
Vue.prototype.$message({
message: 'Copy failed',
type: 'error'
})
}

export default function handleClipboard(text, event) {
const clipboard = new Clipboard(event.target, {
text: () => text
})
clipboard.on('success', () => {
clipboardSuccess()
clipboard.off('error')
clipboard.off('success')
clipboard.destroy()
})
clipboard.on('error', () => {
clipboardError()
clipboard.off('error')
clipboard.off('success')
clipboard.destroy()
})
clipboard.onClick(event)
}

+ 17
- 0
src/utils/get-page-title.js Прегледај датотеку

@@ -0,0 +1,17 @@
/*
* @Autor: Chuting
* @Date: 2021-09-07 17:13:12
* @LastEditors: Chuting
* @LastEditTime: 2022-07-27 10:48:18
* @Description:
*/
import defaultSettings from '@/settings'

const title = defaultSettings.title || '后台管理系统'

export default function getPageTitle(pageTitle) {
if (pageTitle) {
return `${pageTitle} - ${title}`
}
return `${title}`
}

+ 171
- 0
src/utils/index.js Прегледај датотеку

@@ -0,0 +1,171 @@
/**
* 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
}

/**
* @param {Function} func
* @param {number} wait
* @param {boolean} immediate
* @return {*}
*/
export function debounce(func, wait, immediate) {
let timeout, args, context, timestamp, result

const later = function() {
// 据上一次触发时间间隔
const last = +new Date() - timestamp

// 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last)
} else {
timeout = null
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
if (!immediate) {
result = func.apply(context, args)
if (!timeout) context = args = null
}
}
}

return function(...args) {
context = this
timestamp = +new Date()
const callNow = immediate && !timeout
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait)
if (callNow) {
result = func.apply(context, args)
context = args = null
}

return result
}
}

// 下载文件
export function downloadFile(obj, name, suffix) {
const url = window.URL.createObjectURL(new Blob([obj]))
const link = document.createElement('a')
link.style.display = 'none'
link.href = url
const fileName = parseTime(new Date()) + '-' + name + '.' + suffix
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}

+ 23
- 0
src/utils/permission.js Прегледај датотеку

@@ -0,0 +1,23 @@
import store from '@/store'

/**
* @param {Array} value
* @returns {Boolean}
* @example see @/views/permission/directive.vue
*/
export default {
install(Vue) {
Vue.prototype.checkPer = (value) => {
if (value && value instanceof Array && value.length > 0) {
const roles = store.getters && store.getters.roles
const permissionRoles = value
return roles.some(role => {
return permissionRoles.includes(role)
})
} else {
console.error(`need roles! Like v-permission="['admin','editor']"`)
return false
}
}
}
}

+ 71
- 0
src/utils/request.js Прегледај датотеку

@@ -0,0 +1,71 @@
/*
* @Date: 2021-07-01 15:30:29
* @LastEditTime: 2022-07-27 10:48:39
* @Description: 请求封装
*/
import axios from 'axios'
import { Notification } 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, // api 的 base_url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 1000 * 30 // request timeout
})

// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
// const AccessToken = localStorage.getItem("AccessToken")
// if (AccessToken) {
// config.headers.token = AccessToken
// }
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
// 兼容blob下载
if (res instanceof Blob && res.type.toLowerCase().indexOf('json') === -1) {
return res
}
if (res.code === 200) {
return res.data
}
Notification.error({
title: res.message,
duration: 5000
})
return Promise.reject(res.message || 'Error')
},
error => {
Notification.error({
title: '操作失败!',
duration: 5000
})
return Promise.reject(error)
}
)

export default service

+ 36
- 0
src/utils/validate.js Прегледај датотеку

@@ -0,0 +1,36 @@
/**
* 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
}

/**
* 邮箱
* @param {*} s
*/
export function isEmail(s) {
return /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((.[a-zA-Z0-9_-]{2,3}){1,2})$/.test(s)
}

/**
* 手机号码
* @param {*} s
*/
export function isMobile(s) {
return /^1[0-9]{10}$/.test(s)
}

+ 228
- 0
src/views/404.vue Прегледај датотеку

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

+ 115
- 0
src/views/articleManage/index.vue Прегледај датотеку

@@ -0,0 +1,115 @@
<!--
* @Date: 2021-07-08 11:02:40
* @LastEditTime: 2022-07-27 10:48:43
* @Description: 文章列表
-->
<template>
<div class="app-container">
<!--工具栏-->
<div class="head-container">
<div>
<!-- 搜索 -->
<el-input v-model="query.name" prefix-icon="el-icon-search" clearable placeholder="名称" class="filter-item" @keyup.enter.native="crud.toQuery" />

<el-input v-model="query.releaseName" clearable placeholder="活动名称" class="filter-item"/>

<el-select v-model="query.list" clearable placeholder="列表" class="filter-item">
<el-option v-for="item in releaseSourceList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>

<el-date-picker v-model="query.times" value-format="yyyy-MM-dd HH:mm:ss" type="datetimerange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" class="date-item" style="width:300px !important; margin: 0 3px 10px 0;"></el-date-picker>

<rrOperation />
</div>
<div>
<el-button class="filter-item" size="mini" type="primary" icon="el-icon-plus" @click="openSave()">新增</el-button>
<el-button class="filter-item" size="mini" type="warning" icon="el-icon-download" @click="exportFile">导出</el-button>
</div>
</div>
<!--表格渲染-->
<el-table ref="table" v-loading="crud.loading" :data="crud.data" style="width: 100%;">
<el-table-column :show-overflow-tooltip="true" prop="id" label="活动编号" />
<el-table-column :show-overflow-tooltip="true" prop="releaseName" label="活动名称" />
<el-table-column :show-overflow-tooltip="true" prop="contentTitle" label="文章名称" />
<el-table-column :show-overflow-tooltip="true" prop="releaseTime" label="发布时间" />
<el-table-column :show-overflow-tooltip="true" prop="publisher" label="发布人" />
<el-table-column label="操作" width="230" align="center" fixed="right">
<template slot-scope="scope" class="btn">
<el-button type="text" size="mini" @click="openSave(scope.row.id)">编辑</el-button>

<el-popover placement="top" width="200" :ref="`del-${scope.$index}`">
<p>是否确认删除该数据?</p>
<div style="text-align: right; margin: 0">
<el-button size="mini" @click="scope._self.$refs[`del-${scope.$index}`].doClose()">取消</el-button>
<el-button type="primary" size="mini" @click="del(scope)">确定</el-button>
</div>
<el-button slot="reference" type="text" size="mini" style="margin-left:10px">删除</el-button>
</el-popover>
</template>
</el-table-column>
</el-table>
<!--分页组件-->
<pagination />

<!-- 新增修改弹窗 -->
<el-dialog v-if="dialogVisible" title="标题" :visible.sync="dialogVisible" append-to-body width="80%">
<save-page :id='id' @close="close"></save-page>
</el-dialog>
</div>
</template>
<script>
import demoService from '@/api/nested/demo'
import CRUD, { presenter, header, crud } from '@crud/crud'
import rrOperation from '@crud/RR.operation'
import pagination from '@crud/Pagination'
import { downloadFile } from '@/utils/index'
import savePage from './save'

export default {
name: 'Demo',
components: { rrOperation, pagination, savePage },
mixins: [presenter(), header(), crud()],
data() {
return {
releaseSourceList: [],
dialogVisible: false,
id: ''
}
},
cruds() {
return CRUD({ title: '列表', url: 'xxx', query: {}})
},
methods: {
/* 刷新前操作 */
[CRUD.HOOK.beforeRefresh]() {
this.query.id = 'xxx'
},
openSave(id) {
this.id = id || ''
this.dialogVisible = true
},
close(tag) {
if (tag) this.crud.toQuery()
this.dialogVisible = false
},
/* 删除 */
del(scope) {
demoService.del(scope.row.id).then(res => {
this.$message({
message: '删除成功!',
type: 'success'
})
this.crud.toQuery()
scope._self.$refs[`del-${scope.$index}`].doClose()
})
},
/* 导出 */
exportFile() {
demoService.exportFile(this.crud.query).then(res => {
downloadFile(res, '文章列表', 'xlsx')
})
}
}
}
</script>

+ 112
- 0
src/views/articleManage/save.vue Прегледај датотеку

@@ -0,0 +1,112 @@
<!--
* @Date: 2021-10-11 14:12:26
* @LastEditTime: 2022-07-27 10:48:47
* @Description: 新增/编辑页面
-->
<template>
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="resetform">
<el-form-item label="文章标题" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
<el-form-item label="活动区域" prop="region">
<el-select v-model="ruleForm.region" placeholder="请选择活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
<el-form-item label="活动性质" prop="type">
<el-checkbox-group v-model="ruleForm.type">
<el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox>
<el-checkbox label="地推活动" name="type"></el-checkbox>
<el-checkbox label="线下主题活动" name="type"></el-checkbox>
<el-checkbox label="单纯品牌曝光" name="type"></el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="特殊资源" prop="resource">
<el-radio-group v-model="ruleForm.resource">
<el-radio label="线上品牌商赞助"></el-radio>
<el-radio label="线下场地免费"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="活动形式" prop="desc">
<el-input type="textarea" v-model="ruleForm.desc"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loadingBtn" @click="submitForm('ruleForm')">立即创建</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</template>

<script>
import demoService from '@/api/nested/demo'

export default {
props: ['id'],
data() {
return {
ruleForm: {
name: '',
region: '',
type: [],
resource: '',
desc: ''
},
rules: {
name: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
],
region: [
{ required: true, message: '请选择活动区域', trigger: 'change' }
],
type: [
{ type: 'array', required: true, message: '请至少选择一个活动性质', trigger: 'change' }
],
resource: [
{ required: true, message: '请选择活动资源', trigger: 'change' }
],
desc: [
{ required: true, message: '请填写活动形式', trigger: 'blur' }
]
},
loadingBtn: false
}
},
created() {
if (this.id) {
this.getData()
}
},
methods: {
getData() {
demoService.getData().then(res => {
this.ruleForm = res
})
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.loadingBtn = true
demoService.save(this.ruleForm).then(res => {
this.$message({
message: '操作成功!',
type: 'success'
})
this.loadingBtn = false
this.$emit('close', 'save')
}).catch(() => {
this.loadingBtn = true
})
} else {
console.log('error submit!!')
return false
}
})
},
resetForm(formName) {
this.$refs[formName].resetFields()
}
}
}
</script>

+ 30
- 0
src/views/home/index.vue Прегледај датотеку

@@ -0,0 +1,30 @@
<template>
<div class="home">
<img :src="imgUrl" alt="">
</div>
</template>

<script>
import imgUrl from '@/assets/img/home.png'

export default {
name: 'Home',
data() {
return {
imgUrl
}
}
}
</script>

<style lang="scss" scoped>
.home {
height: calc(100vh - 50px);
display: flex;
align-items: center;
justify-content: center;
img {
width: 50%;
}
}
</style>

+ 115
- 0
src/views/homePage/banner1.vue Прегледај датотеку

@@ -0,0 +1,115 @@
<!--
* @Date: 2021-07-08 11:02:40
* @LastEditTime: 2022-07-27 10:48:43
* @Description: 广告位
-->
<template>
<div class="app-container">
<!--表格渲染-->
<el-table :data="data" style="width: 100%;">
<el-table-column :show-overflow-tooltip="true" align="center" prop="link" label="链接" />
<el-table-column :show-overflow-tooltip="true" align="center" prop="image" label="图片">
<template slot-scope="scope">
<img :src="scope.row.image" width="50px" />
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="true" align="center" prop="newOpen" label="是否新开页面打开">
<template slot-scope="scope">
{{ scope.row.newOpen === 1 ? '是' : '否' }}
</template>
</el-table-column>
<el-table-column label="操作" width="230" align="center" fixed="right">
<template slot-scope="scope" class="btn">
<el-button type="text" size="mini" @click="openSave(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>

<!-- 新增修改弹窗 -->
<el-dialog v-if="dialogVisible" title="轮播" :visible.sync="dialogVisible" append-to-body width="600px">
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="130px" class="resetform">
<el-form-item label="图片" prop="image">
<uploadFile :url.sync="ruleForm.image" type="image" />
</el-form-item>
<el-form-item label="链接" prop="link">
<el-input v-model="ruleForm.link"></el-input>
</el-form-item>
<el-form-item label="是否新开页面打开">
<el-radio-group v-model="ruleForm.newOpen">
<el-radio :label="1">是</el-radio>
<el-radio :label="0">否</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">立即创建</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
import { guanggao1, saveGuangGao } from '@/api/homePage'
import uploadFile from '@/components/uploadFile'

export default {
components: {
uploadFile
},
data() {
return {
dialogVisible: false,
id: '',
data: [],
ruleForm: {
id: '',
image: '',
link: '',
newOpen: '1'
},
rules: {
image: [
{ required: true, message: '请上传图片', trigger: 'blur' }
],
link: [
{ required: true, message: '请输入链接', trigger: 'blur' }
]
}
}
},
created() {
this.getData()
},
methods: {
getData() {
guanggao1().then(res => {
this.data = [res]
})
},
openSave(item) {
this.ruleForm.id = item.id
this.ruleForm.image = item.image
this.ruleForm.link = item.link
this.ruleForm.newOpen = item.newOpen
this.dialogVisible = true
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
saveGuangGao(this.ruleForm).then(res => {
this.$message({
message: '操作成功!',
type: 'success'
})
this.getData()
this.dialogVisible = false
})
} else {
console.log('error submit!!')
return false
}
})
}
}
}
</script>

+ 115
- 0
src/views/homePage/banner2.vue Прегледај датотеку

@@ -0,0 +1,115 @@
<!--
* @Date: 2021-07-08 11:02:40
* @LastEditTime: 2022-07-27 10:48:43
* @Description: 广告位
-->
<template>
<div class="app-container">
<!--表格渲染-->
<el-table :data="data" style="width: 100%;">
<el-table-column :show-overflow-tooltip="true" align="center" prop="link" label="链接" />
<el-table-column :show-overflow-tooltip="true" align="center" prop="image" label="图片">
<template slot-scope="scope">
<img :src="scope.row.image" width="50px" />
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="true" align="center" prop="newOpen" label="是否新开页面打开">
<template slot-scope="scope">
{{ scope.row.newOpen === 1 ? '是' : '否' }}
</template>
</el-table-column>
<el-table-column label="操作" width="230" align="center" fixed="right">
<template slot-scope="scope" class="btn">
<el-button type="text" size="mini" @click="openSave(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>

<!-- 新增修改弹窗 -->
<el-dialog v-if="dialogVisible" title="轮播" :visible.sync="dialogVisible" append-to-body width="600px">
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="130px" class="resetform">
<el-form-item label="图片" prop="image">
<uploadFile :url.sync="ruleForm.image" type="image" />
</el-form-item>
<el-form-item label="链接" prop="link">
<el-input v-model="ruleForm.link"></el-input>
</el-form-item>
<el-form-item label="是否新开页面打开">
<el-radio-group v-model="ruleForm.newOpen">
<el-radio :label="1">是</el-radio>
<el-radio :label="0">否</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">立即创建</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
import { guanggao2, saveGuangGao } from '@/api/homePage'
import uploadFile from '@/components/uploadFile'

export default {
components: {
uploadFile
},
data() {
return {
dialogVisible: false,
id: '',
data: [],
ruleForm: {
id: '',
image: '',
link: '',
newOpen: '1'
},
rules: {
image: [
{ required: true, message: '请上传图片', trigger: 'blur' }
],
link: [
{ required: true, message: '请输入链接', trigger: 'blur' }
]
}
}
},
created() {
this.getData()
},
methods: {
getData() {
guanggao2().then(res => {
this.data = [res]
})
},
openSave(item) {
this.ruleForm.id = item.id
this.ruleForm.image = item.image
this.ruleForm.link = item.link
this.ruleForm.newOpen = item.newOpen
this.dialogVisible = true
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
saveGuangGao(this.ruleForm).then(res => {
this.$message({
message: '操作成功!',
type: 'success'
})
this.getData()
this.dialogVisible = false
})
} else {
console.log('error submit!!')
return false
}
})
}
}
}
</script>

+ 54
- 0
src/views/homePage/carHot.vue Прегледај датотеку

@@ -0,0 +1,54 @@
<!--
* @Date: 2021-07-08 11:02:40
* @LastEditTime: 2022-07-27 10:48:43
* @Description: 列表查询demo
-->
<template>
<div class="app-container">
<!--工具栏-->
<div class="head-container">
<div>
<el-button class="filter-item" size="mini" type="primary" icon="el-icon-plus"
@click="openSave()">新增</el-button>
</div>
</div>

<!--表格渲染-->
<el-table ref="table" v-loading="crud.loading" :data="crud.data" style="width: 100%;" @selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column :show-overflow-tooltip="true" prop="newsId" label="序号" />
<el-table-column :show-overflow-tooltip="true" prop="标题" label="newsTitle" />
<el-table-column :show-overflow-tooltip="true" prop="createDate" label="创建时间" />
</el-table>
<!--分页组件-->
<pagination />
</div>
</template>
<script>
import carHotService from '@/api/index/carHot'
import CRUD, { presenter, header, crud } from '@crud/crud'
import pagination from '@crud/Pagination'

export default {
name: 'Demo',
components: { pagination },
mixins: [presenter(), header(), crud()],
data() {
return {
releaseSourceList: [],
dialogVisible: false,
id: '',
multipleSelection: []
}
},
cruds() {
return CRUD({ title: '列表', url: '/index/carNewsHotList', query: {}})
},
methods: {
handleSelectionChange(val) {
this.multipleSelection = val
}
}
}
</script>

+ 171
- 0
src/views/homePage/carousel.vue Прегледај датотеку

@@ -0,0 +1,171 @@
<!--
* @Date: 2021-07-08 11:02:40
* @LastEditTime: 2022-07-27 10:48:43
* @Description: 列表查询demo
-->
<template>
<div class="app-container">
<!--工具栏-->
<div class="head-container">
<!-- <div>
搜索
<el-input v-model="query.name" prefix-icon="el-icon-search" clearable placeholder="名称" class="filter-item"
@keyup.enter.native="crud.toQuery" />

<rrOperation />
</div> -->
<div>
<el-button class="filter-item" size="mini" type="primary" icon="el-icon-plus"
@click="openSave()">新增</el-button>
</div>
</div>

<!--表格渲染-->
<el-table ref="table" v-loading="crud.loading" :data="crud.data" style="width: 100%;">
<el-table-column :show-overflow-tooltip="true" prop="serialNumber" label="序号" />
<el-table-column :show-overflow-tooltip="true" prop="image" label="图片">
<template slot-scope="scope">
<img :src="scope.row.image" width="50px" />
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="true" prop="link" label="链接" />
<el-table-column :show-overflow-tooltip="true" prop="createDate" label="创建时间" />
<el-table-column label="操作" width="230" align="center" fixed="right">
<template slot-scope="scope" class="btn">
<el-button type="text" size="mini" @click="openSave(scope.row)">编辑</el-button>

<el-popover placement="top" width="200" :ref="`del-${scope.$index}`">
<p>是否确认删除该数据?</p>
<div style="text-align: right; margin: 0">
<el-button size="mini"
@click="scope._self.$refs[`del-${scope.$index}`].doClose()">取消</el-button>
<el-button type="primary" size="mini" @click="del(scope)">确定</el-button>
</div>
<el-button slot="reference" type="text" size="mini" style="margin-left:10px">删除</el-button>
</el-popover>
</template>
</el-table-column>
</el-table>
<!--分页组件-->
<pagination />

<!-- 新增修改弹窗 -->
<el-dialog v-if="dialogVisible" title="轮播" :visible.sync="dialogVisible" append-to-body width="600px">
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="130px" class="resetform">
<el-form-item label="活动名称" prop="image">
<uploadFile :url.sync="ruleForm.image" type="image" />
</el-form-item>
<el-form-item label="序号" prop="serialNumber">
<el-input v-model="ruleForm.serialNumber"></el-input>
</el-form-item>
<el-form-item label="链接" prop="link">
<el-input v-model="ruleForm.link"></el-input>
</el-form-item>
<el-form-item label="是否新开页面打开">
<el-radio-group v-model="ruleForm.newOpen">
<el-radio label="1">是</el-radio>
<el-radio label="0">否</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loadingBtn" @click="submitForm('ruleForm')">立即创建</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
import { delLunbo, saveLunbo } from '@/api/homePage'
import CRUD, { presenter, header, crud } from '@crud/crud'
import pagination from '@crud/Pagination'
import uploadFile from '@/components/uploadFile'

export default {
components: { pagination, uploadFile },
mixins: [presenter(), header(), crud()],
data() {
return {
releaseSourceList: [],
dialogVisible: false,
id: '',
ruleForm: {
image: '',
serialNumber: '',
link: [],
newOpen: '1'
},
rules: {
name: [
{ required: true, message: '请上传图片', trigger: 'blur' }
],
serialNumber: [
{ required: true, message: '请输入序号', trigger: 'blur' }
],
link: [
{ required: true, message: '请输入链接', trigger: 'blur' }
],
desc: [
{ required: true, message: '请填写活动形式', trigger: 'blur' }
]
},
loadingBtn: false
}
},
cruds() {
return CRUD({ title: '列表', url: '/index/lunboList', query: {}})
},
methods: {
openSave(item) {
if (item) {
this.ruleForm.id = item.id
this.ruleForm.image = item.image
this.ruleForm.serialNumber = item.serialNumber
this.ruleForm.link = item.link
this.ruleForm.newOpen = item.newOpen || '1'
}
this.dialogVisible = true
},
close(tag) {
if (tag) this.crud.toQuery()
this.dialogVisible = false
},
/* 删除 */
del(scope) {
delLunbo(scope.row.id).then(res => {
this.$message({
message: '删除成功!',
type: 'success'
})
this.crud.toQuery()
scope._self.$refs[`del-${scope.$index}`].doClose()
})
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.loadingBtn = true
saveLunbo(this.ruleForm).then(res => {
this.$message({
message: '操作成功!',
type: 'success'
})
this.loadingBtn = false
this.dialogVisible = false
this.$refs[formName].resetFields()
this.crud.toQuery()
}).catch(() => {
this.loadingBtn = false
})
} else {
console.log('error submit!!')
return false
}
})
},
resetForm(formName) {
this.$refs[formName].resetFields()
}
}
}
</script>

+ 0
- 0
src/views/homePage/link.vue Прегледај датотеку


+ 118
- 0
src/views/homePage/yyzHot.vue Прегледај датотеку

@@ -0,0 +1,118 @@
<!--
* @Date: 2021-10-11 14:12:26
* @LastEditTime: 2022-07-27 10:48:47
* @Description: 新增/编辑页面
-->
<template>
<div class="app-container">
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="resetform">
<el-form-item label="是否显示">
<el-radio-group v-model="ruleForm.isShow">
<el-radio label="1">是</el-radio>
<el-radio label="0">否</el-radio>
</el-radio-group>
</el-form-item>
<el-card style="margin-top:20px" class="suIbtemCard" v-for="(item, index) in imgList" :key="index">
<el-form-item label="上传视频">
<uploadFile :url.sync="item.imgSrc" type="image"/>
<el-input v-model="item.imgSrc"></el-input>
</el-form-item>
<el-form-item label="标题">
<el-input v-model="item.title"></el-input>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="item.desc"></el-input>
</el-form-item>
<el-form-item label="跳转链接">
<el-input v-model="item.link"></el-input>
</el-form-item>
<el-form-item>
<el-button type="success" icon="el-icon-plus" circle v-if="imgList.length - 1 === index" @click="addItem"></el-button>
<el-button type="danger" icon="el-icon-delete" circle @click="imgList.length > 1 && delItem(item, index)"></el-button>
</el-form-item>
</el-card>
<el-form-item>
<el-button type="primary" :loading="loadingBtn" @click="submitForm('ruleForm')">保存</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import demoService from '@/api/nested/demo'
import uploadFile from '@/components/uploadFile'
export default {
components: {
uploadFile
},
data() {
return {
imgList: [
{ imgSrc: '', link: '', title: '', desc: '' }
],
ruleForm: {
name: '',
region: '',
type: [],
resource: '',
desc: ''
},
rules: {
name: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
],
region: [
{ required: true, message: '请选择活动区域', trigger: 'change' }
],
type: [
{ type: 'array', required: true, message: '请至少选择一个活动性质', trigger: 'change' }
],
resource: [
{ required: true, message: '请选择活动资源', trigger: 'change' }
],
desc: [
{ required: true, message: '请填写活动形式', trigger: 'blur' }
]
},
loadingBtn: false
}
},
created() {
if (this.id) {
this.getData()
}
},
methods: {
addItem() {
this.imgList.push({ imgSrc: '', link: '', title: '', desc: '' })
},
delItem(item, index) {
this.imgList.splice(index, 1)
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.loadingBtn = true
demoService.save(this.ruleForm).then(res => {
this.$message({
message: '操作成功!',
type: 'success'
})
this.loadingBtn = false
this.$emit('close', 'save')
}).catch(() => {
this.loadingBtn = true
})
} else {
console.log('error submit!!')
return false
}
})
},
resetForm(formName) {
this.$refs[formName].resetFields()
}
}
}
</script>

+ 289
- 0
src/views/login/index.vue Прегледај датотеку

@@ -0,0 +1,289 @@
<template>
<div class='login-container'>
<div class='Version'>Version 1.0.8 test</div>
<div class='videoBox'>
<video src='https://video.metavatar.cc/sv/4aaaaa5e-18832dbb5e6/4aaaaa5e-18832dbb5e6.mp4' muted autoplay
loop></video>
</div>
<el-form ref='loginFormRef' :model='loginData' class='login-form'>

<el-form-item prop='username'>
<div class='text-white'>
<i class="el-icon-user"></i>
</div>
<el-input class='flex-1' ref='email' size='large' v-model='loginData.username' placeholder='用户名'
name='email' />
</el-form-item>

<el-form-item prop='password'>
<span class='text-white'>
<i class="el-icon-lock"></i>
</span>
<el-input class='flex-1' v-model='loginData.password' placeholder='密码'
:type="passwordVisible === false ? 'password' : 'input'" size='large' name='password'
@keyup.enter='handleLogin' />
<span class='mr-2' @click='passwordVisible = !passwordVisible'>
</span>
</el-form-item>

<!-- 验证码 -->
<el-form-item prop='captcha'>
<span class='text-white'>
<i class="el-icon-key"></i>
</span>
<el-input v-model='loginData.captcha' auto-complete='off' placeholder='验证码' class='flex-1'
@keyup.enter='handleLogin' />

<span class='captcha'>
<img v-if='showCodeImg' :src='verifyCodeUrl' @click='getCode' />
</span>
</el-form-item>

<el-button size='default' :loading='loading' type='primary' class='loginBtn' @click.prevent='handleLogin'>
登录
</el-button>
</el-form>
</div>
</template>
<script>
import {
loginApi
} from '@/api/login'

export default {
data() {
return {
isLogin: 1,
passwordVisible: false,
loading: false,
loginData: {
username: '',
password: '',
captcha: ''
},
showCodeImg: true,
isLogining: true,
verifyCodeUrl: process.env.VUE_APP_BASE_API + '/captcha.jpg'
}
},

methods: {
handleLogin() {
loginApi(this.loginData).then(res => {
this.$router.push({
path: '/homePage/carousel'
})
})
},
getCode() {
this.verifyCodeUrl = process.env.VUE_APP_BASE_API + '/captcha.jpg?time=' + Math.random()
}
}
}
</script>

<style lang='scss' scoped>
.login-container {
position: relative;
width: 100%;
// width: 1920px;
// height: 1080px;
min-height: 100%;
overflow: hidden;
// background-color: #2d3a4b;

.Version {
position: absolute;
bottom: 20px;
right: 20px;
color: #ffffff;
}

.login-form {
position: absolute;
top: 46%;
left: 10%;
width: 520px;
max-width: 100%;
padding: 20px;
overflow: hidden;
backdrop-filter: blur(3px);
background: linear-gradient(123.64deg,
rgba(188, 193, 196, 0.1) -22.71%,
rgba(255, 255, 255, 0.039) 70.04%);
border-radius: 20px;
transform: translateY(-50%);
border: 1px solid #cacaca;
transition: all 0.15s;

.captcha {
position: absolute;
top: 0;
right: 0;

img {
width: 120px;
height: 48px;
cursor: pointer;
}

.sendSMSBtn {
width: 120px;
height: 48px;
line-height: 48px;
color: #fff;
text-align: center;
background: linear-gradient(270deg, #4b61dc, #15d1b8);
}
}

&:hover {
backdrop-filter: blur(7px);
}

.items-center {
padding: 20px;
font-size: 30px;
}

.el-form-item {
.text-white {
padding: 8px;
i{
color:#fff;
font-size: 20px;
}
}
::v-deep .el-form-item__content{
display: flex;
}
::v-deep .el-input .el-input__inner {
color: #fff;
background: transparent;
border: 0;
border-radius: 0;
caret-color: #fff;
}
}
}
}

.loginBtn {
width: 100%;
height: 40px;
background: linear-gradient(270deg, #4b61dc, #15d1b8);
transition: all 0.3s;

&:hover {
transform: scale(1.02);
}

cursor: pointer;
}

.bottomBtnCN {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
color: #fff;
text-align: center;
margin-top: 20px;

.join {
cursor: pointer;
border-bottom: 2px solid #ffffff00;
padding: 0 3px;
padding-bottom: 2px;
transition: all 0.3s;

&:hover {
border-bottom: 2px solid #ffffff;
}
}
}

.bottomBtnEN {
padding: 0 10px;
color: #fff;
text-align: center;
margin-top: 20px;

.join {
cursor: pointer;
border-bottom: 2px solid #ffffff00;
padding: 0 3px;
padding-bottom: 2px;
transition: all 0.3s;

&:hover {
border-bottom: 2px solid #ffffff;
}
}
}

.videoBox {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;

video {
width: 100%;
height: 100%;
object-fit: fill;
}
}

.el-form-item {
background: rgb(0 0 0 / 10%);
border: 1px solid #ffffff2d;
border-radius: 5px;
}

.el-input {
background: transparent;

// 子组件 scoped 无效,使用 :deep
:deep(.el-input__wrapper) {
padding: 0;
background: transparent;
box-shadow: none;

.el-input__inner {
color: #fff;
background: transparent;
border: 0;
border-radius: 0;
caret-color: #fff;

&:-webkit-autofill {
box-shadow: 0 0 0 1000px transparent inset !important;
-webkit-text-fill-color: #fff !important;
}

// 设置输入框自动填充的延迟属性
&:-webkit-autofill,
&:-webkit-autofill:hover,
&:-webkit-autofill:focus,
&:-webkit-autofill:active {
transition: color 99999s ease-out, background-color 99999s ease-out;
transition-delay: 99999s;
}
}
}
}

@keyframes rotate {
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
}
</style>

+ 115
- 0
src/views/nested/demo/index.vue Прегледај датотеку

@@ -0,0 +1,115 @@
<!--
* @Date: 2021-07-08 11:02:40
* @LastEditTime: 2022-07-27 10:48:43
* @Description: 列表查询demo
-->
<template>
<div class="app-container">
<!--工具栏-->
<div class="head-container">
<div>
<!-- 搜索 -->
<el-input v-model="query.name" prefix-icon="el-icon-search" clearable placeholder="名称" class="filter-item" @keyup.enter.native="crud.toQuery" />

<el-input v-model="query.releaseName" clearable placeholder="活动名称" class="filter-item"/>

<el-select v-model="query.list" clearable placeholder="列表" class="filter-item">
<el-option v-for="item in releaseSourceList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>

<el-date-picker v-model="query.times" value-format="yyyy-MM-dd HH:mm:ss" type="datetimerange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" class="date-item" style="width:300px !important; margin: 0 3px 10px 0;"></el-date-picker>

<rrOperation />
</div>
<div>
<el-button class="filter-item" size="mini" type="primary" icon="el-icon-plus" @click="openSave()">新增</el-button>
<el-button class="filter-item" size="mini" type="warning" icon="el-icon-download" @click="exportFile">导出</el-button>
</div>
</div>
<!--表格渲染-->
<el-table ref="table" v-loading="crud.loading" :data="crud.data" style="width: 100%;">
<el-table-column :show-overflow-tooltip="true" prop="id" label="活动编号" />
<el-table-column :show-overflow-tooltip="true" prop="releaseName" label="活动名称" />
<el-table-column :show-overflow-tooltip="true" prop="contentTitle" label="文章名称" />
<el-table-column :show-overflow-tooltip="true" prop="releaseTime" label="发布时间" />
<el-table-column :show-overflow-tooltip="true" prop="publisher" label="发布人" />
<el-table-column label="操作" width="230" align="center" fixed="right">
<template slot-scope="scope" class="btn">
<el-button type="text" size="mini" @click="openSave(scope.row.id)">编辑</el-button>

<el-popover placement="top" width="200" :ref="`del-${scope.$index}`">
<p>是否确认删除该数据?</p>
<div style="text-align: right; margin: 0">
<el-button size="mini" @click="scope._self.$refs[`del-${scope.$index}`].doClose()">取消</el-button>
<el-button type="primary" size="mini" @click="del(scope)">确定</el-button>
</div>
<el-button slot="reference" type="text" size="mini" style="margin-left:10px">删除</el-button>
</el-popover>
</template>
</el-table-column>
</el-table>
<!--分页组件-->
<pagination />

<!-- 新增修改弹窗 -->
<el-dialog v-if="dialogVisible" title="标题" :visible.sync="dialogVisible" append-to-body width="500px">
<save-page :id='id' @close="close"></save-page>
</el-dialog>
</div>
</template>
<script>
import demoService from '@/api/nested/demo'
import CRUD, { presenter, header, crud } from '@crud/crud'
import rrOperation from '@crud/RR.operation'
import pagination from '@crud/Pagination'
import { downloadFile } from '@/utils/index'
import savePage from './save'

export default {
name: 'Demo',
components: { rrOperation, pagination, savePage },
mixins: [presenter(), header(), crud()],
data() {
return {
releaseSourceList: [],
dialogVisible: false,
id: ''
}
},
cruds() {
return CRUD({ title: '列表', url: 'xxx', query: {}})
},
methods: {
/* 刷新前操作 */
[CRUD.HOOK.beforeRefresh]() {
this.query.id = 'xxx'
},
openSave(id) {
this.id = id || ''
this.dialogVisible = true
},
close(tag) {
if (tag) this.crud.toQuery()
this.dialogVisible = false
},
/* 删除 */
del(scope) {
demoService.del(scope.row.id).then(res => {
this.$message({
message: '删除成功!',
type: 'success'
})
this.crud.toQuery()
scope._self.$refs[`del-${scope.$index}`].doClose()
})
},
/* 导出 */
exportFile() {
demoService.exportFile(this.crud.query).then(res => {
downloadFile(res, '文章列表', 'xlsx')
})
}
}
}
</script>

+ 112
- 0
src/views/nested/demo/save.vue Прегледај датотеку

@@ -0,0 +1,112 @@
<!--
* @Date: 2021-10-11 14:12:26
* @LastEditTime: 2022-07-27 10:48:47
* @Description: 新增/编辑页面
-->
<template>
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="resetform">
<el-form-item label="活动名称" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
<el-form-item label="活动区域" prop="region">
<el-select v-model="ruleForm.region" placeholder="请选择活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
<el-form-item label="活动性质" prop="type">
<el-checkbox-group v-model="ruleForm.type">
<el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox>
<el-checkbox label="地推活动" name="type"></el-checkbox>
<el-checkbox label="线下主题活动" name="type"></el-checkbox>
<el-checkbox label="单纯品牌曝光" name="type"></el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="特殊资源" prop="resource">
<el-radio-group v-model="ruleForm.resource">
<el-radio label="线上品牌商赞助"></el-radio>
<el-radio label="线下场地免费"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="活动形式" prop="desc">
<el-input type="textarea" v-model="ruleForm.desc"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loadingBtn" @click="submitForm('ruleForm')">立即创建</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</template>

<script>
import demoService from '@/api/nested/demo'

export default {
props: ['id'],
data() {
return {
ruleForm: {
name: '',
region: '',
type: [],
resource: '',
desc: ''
},
rules: {
name: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
],
region: [
{ required: true, message: '请选择活动区域', trigger: 'change' }
],
type: [
{ type: 'array', required: true, message: '请至少选择一个活动性质', trigger: 'change' }
],
resource: [
{ required: true, message: '请选择活动资源', trigger: 'change' }
],
desc: [
{ required: true, message: '请填写活动形式', trigger: 'blur' }
]
},
loadingBtn: false
}
},
created() {
if (this.id) {
this.getData()
}
},
methods: {
getData() {
demoService.getData().then(res => {
this.ruleForm = res
})
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.loadingBtn = true
demoService.save(this.ruleForm).then(res => {
this.$message({
message: '操作成功!',
type: 'success'
})
this.loadingBtn = false
this.$emit('close', 'save')
}).catch(() => {
this.loadingBtn = true
})
} else {
console.log('error submit!!')
return false
}
})
},
resetForm(formName) {
this.$refs[formName].resetFields()
}
}
}
</script>

+ 7
- 0
src/views/nested/menu1/index.vue Прегледај датотеку

@@ -0,0 +1,7 @@
<template>
<div style="padding:30px;">
<el-alert :closable="false" title="menu 1">
<router-view />
</el-alert>
</div>
</template>

+ 7
- 0
src/views/nested/menu1/menu1-1/index.vue Прегледај датотеку

@@ -0,0 +1,7 @@
<template>
<div style="padding:30px;">
<el-alert :closable="false" title="menu 1-1" type="success">
<router-view />
</el-alert>
</div>
</template>

+ 7
- 0
src/views/nested/menu1/menu1-2/index.vue Прегледај датотеку

@@ -0,0 +1,7 @@
<template>
<div style="padding:30px;">
<el-alert :closable="false" title="menu 1-2" type="success">
<router-view />
</el-alert>
</div>
</template>

+ 5
- 0
src/views/nested/menu1/menu1-2/menu1-2-1/index.vue Прегледај датотеку

@@ -0,0 +1,5 @@
<template functional>
<div style="padding:30px;">
<el-alert :closable="false" title="menu 1-2-1" type="warning" />
</div>
</template>

+ 5
- 0
src/views/nested/menu1/menu1-2/menu1-2-2/index.vue Прегледај датотеку

@@ -0,0 +1,5 @@
<template functional>
<div style="padding:30px;">
<el-alert :closable="false" title="menu 1-2-2" type="warning" />
</div>
</template>

+ 5
- 0
src/views/nested/menu1/menu1-3/index.vue Прегледај датотеку

@@ -0,0 +1,5 @@
<template functional>
<div style="padding:30px;">
<el-alert :closable="false" title="menu 1-3" type="success" />
</div>
</template>

+ 126
- 0
vue.config.js Прегледај датотеку

@@ -0,0 +1,126 @@
'use strict'
const path = require('path')
const defaultSettings = require('./src/settings.js')

function resolve(dir) {
return path.join(__dirname, dir)
}

const title = defaultSettings.title || '后台管理系统' // page title

// If your port is set to 80,
// use administrator privileges to execute the command line.
// For example, Mac: sudo npm run
// You can change the port by the following methods:
// port = 9528 npm run dev OR npm run dev --port = 9528
const port = 8011 // dev port
const { name } = require('./package')

// All configuration item explanations can be find in https://cli.vuejs.org/config/
module.exports = {
/**
* You will need to set publicPath if you plan to deploy your site under a sub path,
* for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
* then publicPath should be set to "/bar/".
* In most cases please use '/' !!!
* Detail: https://cli.vuejs.org/config/#publicpath
*/
publicPath: process.env.NODE_ENV === 'development' ? '/' : '/admin',
outputDir: process.env.OUTPUT_DIR,
assetsDir: 'static',
lintOnSave: process.env.NODE_ENV === 'development',
productionSourceMap: false,
devServer: {
port: port,
headers: {
'Access-Control-Allow-Origin': '*'
},
open: true,
overlay: {
warnings: false,
errors: true
},
proxy: {
'/api': {
target: process.env.VUE_APP_BASE_API,
changeOrigin: true,
pathRewrite: {
'^/api': 'api'
}
}
}
},
configureWebpack: {
devtool: 'source map',
// provide the app's title in webpack's name field, so that
// it can be accessed in index.html to inject the correct title.
name: title,
resolve: {
alias: {
'@': resolve('src'),
'@crud': resolve('src/components/Crud')
}
},
output: {
// 把子应用打包成 umd 库格式
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`
}
},
chainWebpack(config) {
// it can improve the speed of the first screen, it is recommended to turn on preload
config.plugin('preload').tap(() => [
{
rel: 'preload',
// to ignore runtime.js
// https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
include: 'initial'
}
])

// when there are many pages, it will cause too many meaningless requests
config.plugins.delete('prefetch')

config
.when(process.env.NODE_ENV !== 'development',
config => {
config
.plugin('ScriptExtHtmlWebpackPlugin')
.after('html')
.use('script-ext-html-webpack-plugin', [{
// `runtime` must same as runtimeChunk name. default is `runtime`
inline: /runtime\..*\.js$/
}])
.end()
config
.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial' // only package third parties that are initially dependent
},
elementUI: {
name: 'chunk-elementUI', // split elementUI into a single package
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'), // can customize your rules
minChunks: 3, // minimum common number
priority: 5,
reuseExistingChunk: true
}
}
})
// https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
config.optimization.runtimeChunk('single')
}
)
}
}

Loading…
Откажи
Сачувај