master
xy 2 years ago
commit 2152d89d27

@ -0,0 +1,14 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

@ -0,0 +1,7 @@
# just a flag
ENV = 'development'
# base api
VUE_APP_BASE_API='https://cz-hjjc.115.langye.net'
VUE_APP_UPLOAD_API='https://cz-hjjc.115.langye.net/api/admin/upload-file'
VUE_APP_PREVIEW=//view.langye.net/preview/onlinePreview

@ -0,0 +1,8 @@
# just a flag
ENV = 'production'
# base api
VUE_APP_BASE_API='http://cz-hjjc.115.langye.net'
VUE_APP_UPLOAD_API='http://cz-hjjc.115.langye.net/api/admin/upload-file'
VUE_APP_PREVIEW=//view.langye.net/preview/onlinePreview

@ -0,0 +1,9 @@
NODE_ENV = production
# just a flag
ENV = 'staging'
# base api
VUE_APP_BASE_API='http://cz-hjjc.115.langye.net'
VUE_APP_UPLOAD_API='http://cz-hjjc.115.langye.net/api/admin/upload-file'
VUE_APP_PREVIEW=//view.langye.net/preview/onlinePreview

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

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

16
.gitignore vendored

@ -0,0 +1,16 @@
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
tests/**/coverage/
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln

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

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017-present PanJiaChen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,111 @@
# vue-admin-template
> 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint这些搭建后台必要的东西。
[线上地址](http://panjiachen.github.io/vue-admin-template)
[国内访问](https://panjiachen.gitee.io/vue-admin-template)
目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`
<p align="center">
<b>SPONSORED BY</b>
</p>
<p align="center">
<a href="https://finclip.com?from=vue_element" title="FinClip" target="_blank">
<img height="200px" src="https://gitee.com/panjiachen/gitee-cdn/raw/master/vue%E8%B5%9E%E5%8A%A9.png" title="FinClip">
</a>
</p>
## Extra
如果你想要根据用户角色来动态生成侧边栏和 router你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
## 相关项目
- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目:
- [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2)
- [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac)
- [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35)
- [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56)
- [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836)
## Build Setup
```bash
# 克隆项目
git clone https://github.com/PanJiaChen/vue-admin-template.git
# 进入项目目录
cd vue-admin-template
# 安装依赖
npm install
# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npm.taobao.org
# 启动服务
npm run dev
```
浏览器访问 [http://localhost:9528](http://localhost:9528)
## 发布
```bash
# 构建测试环境
npm run build:stage
# 构建生产环境
npm run build:prod
```
## 其它
```bash
# 预览发布环境效果
npm run preview
# 预览发布环境效果 + 静态资源分析
npm run preview -- --report
# 代码格式检查
npm run lint
# 代码格式检查并自动修复
npm run lint -- --fix
```
更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/)
## 购买贴纸
你也可以通过 购买[官方授权的贴纸](https://smallsticker.com/product/vue-element-admin) 的方式来支持 vue-element-admin - 每售出一张贴纸,我们将获得 2 元的捐赠。
## Demo
![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
## Browsers support
Modern browsers and Internet Explorer 10+.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| --------- | --------- | --------- | --------- |
| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
## License
[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
Copyright (c) 2017-present PanJiaChen

@ -0,0 +1,99 @@
# vue-admin-template
English | [简体中文](./README-zh.md)
> A minimal vue admin template with Element UI & axios & iconfont & permission control & lint
**Live demo:** http://panjiachen.github.io/vue-admin-template
**The current version is `v4.0+` build on `vue-cli`. If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0), it does not rely on `vue-cli`**
<p align="center">
<b>SPONSORED BY</b>
</p>
<p align="center">
<a href="https://finclip.com?from=vue_element" title="FinClip" target="_blank">
<img height="200px" src="https://gitee.com/panjiachen/gitee-cdn/raw/master/vue%E8%B5%9E%E5%8A%A9.png" title="FinClip">
</a>
</p>
## Build Setup
```bash
# clone the project
git clone https://github.com/PanJiaChen/vue-admin-template.git
# enter the project directory
cd vue-admin-template
# install dependency
npm install
# develop
npm run dev
```
This will automatically open http://localhost:9528
## Build
```bash
# build for test environment
npm run build:stage
# build for production environment
npm run build:prod
```
## Advanced
```bash
# preview the release environment effect
npm run preview
# preview the release environment effect + static resource analysis
npm run preview -- --report
# code format check
npm run lint
# code format check and auto fix
npm run lint -- --fix
```
Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information
## Demo
![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
## Extra
If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour))
## Related Project
- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
## Browsers support
Modern browsers and Internet Explorer 10+.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| --------- | --------- | --------- | --------- |
| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
## License
[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
Copyright (c) 2017-present PanJiaChen

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

@ -0,0 +1,35 @@
const { run } = require('runjs')
const chalk = require('chalk')
const config = require('../vue.config.js')
const rawArgv = process.argv.slice(2)
const args = rawArgv.join(' ')
if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
const report = rawArgv.includes('--report')
run(`vue-cli-service build ${args}`)
const port = 9526
const publicPath = config.publicPath
var connect = require('connect')
var serveStatic = require('serve-static')
const app = connect()
app.use(
publicPath,
serveStatic('./dist', {
index: ['index.html', '/']
})
)
app.listen(port, function () {
console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
if (report) {
console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
}
})
} else {
run(`vue-cli-service build ${args}`)
}

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

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

@ -0,0 +1,57 @@
const Mock = require('mockjs')
const { param2Obj } = require('./utils')
const user = require('./user')
const table = require('./table')
const mocks = [
...user,
...table
]
// for front mock
// please use it cautiously, it will redefine XMLHttpRequest,
// which will cause many of your third-party libraries to be invalidated(like progress event).
function mockXHR() {
// mock patch
// https://github.com/nuysoft/Mock/issues/300
Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
Mock.XHR.prototype.send = function() {
if (this.custom.xhr) {
this.custom.xhr.withCredentials = this.withCredentials || false
if (this.responseType) {
this.custom.xhr.responseType = this.responseType
}
}
this.proxy_send(...arguments)
}
function XHR2ExpressReqWrap(respond) {
return function(options) {
let result = null
if (respond instanceof Function) {
const { body, type, url } = options
// https://expressjs.com/en/4x/api.html#req
result = respond({
method: type,
body: JSON.parse(body),
query: param2Obj(url)
})
} else {
result = respond
}
return Mock.mock(result)
}
}
for (const i of mocks) {
Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
}
}
module.exports = {
mocks,
mockXHR
}

@ -0,0 +1,81 @@
const chokidar = require('chokidar')
const bodyParser = require('body-parser')
const chalk = require('chalk')
const path = require('path')
const Mock = require('mockjs')
const mockDir = path.join(process.cwd(), 'mock')
function registerRoutes(app) {
let mockLastIndex
const { mocks } = require('./index.js')
const mocksForServer = mocks.map(route => {
return responseFake(route.url, route.type, route.response)
})
for (const mock of mocksForServer) {
app[mock.type](mock.url, mock.response)
mockLastIndex = app._router.stack.length
}
const mockRoutesLength = Object.keys(mocksForServer).length
return {
mockRoutesLength: mockRoutesLength,
mockStartIndex: mockLastIndex - mockRoutesLength
}
}
function unregisterRoutes() {
Object.keys(require.cache).forEach(i => {
if (i.includes(mockDir)) {
delete require.cache[require.resolve(i)]
}
})
}
// for mock server
const responseFake = (url, type, respond) => {
return {
url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`),
type: type || 'get',
response(req, res) {
console.log('request invoke:' + req.path)
res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
}
}
}
module.exports = app => {
// parse app.body
// https://expressjs.com/en/4x/api.html#req.body
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({
extended: true
}))
const mockRoutes = registerRoutes(app)
var mockRoutesLength = mockRoutes.mockRoutesLength
var mockStartIndex = mockRoutes.mockStartIndex
// watch files, hot reload mock server
chokidar.watch(mockDir, {
ignored: /mock-server/,
ignoreInitial: true
}).on('all', (event, path) => {
if (event === 'change' || event === 'add') {
try {
// remove mock routes stack
app._router.stack.splice(mockStartIndex, mockRoutesLength)
// clear routes cache
unregisterRoutes()
const mockRoutes = registerRoutes(app)
mockRoutesLength = mockRoutes.mockRoutesLength
mockStartIndex = mockRoutes.mockStartIndex
console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
} catch (error) {
console.log(chalk.redBright(error))
}
}
})
}

@ -0,0 +1,29 @@
const Mock = require('mockjs')
const data = Mock.mock({
'items|30': [{
id: '@id',
title: '@sentence(10, 20)',
'status|1': ['published', 'draft', 'deleted'],
author: 'name',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
}]
})
module.exports = [
{
url: '/vue-admin-template/table/list',
type: 'get',
response: config => {
const items = data.items
return {
code: 20000,
data: {
total: items.length,
items: items
}
}
}
}
]

@ -0,0 +1,84 @@
const tokens = {
admin: {
token: 'admin-token'
},
editor: {
token: 'editor-token'
}
}
const users = {
'admin-token': {
roles: ['admin'],
introduction: 'I am a super administrator',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Super Admin'
},
'editor-token': {
roles: ['editor'],
introduction: 'I am an editor',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Normal Editor'
}
}
module.exports = [
// user login
{
url: '/vue-admin-template/user/login',
type: 'post',
response: config => {
const { username } = config.body
const token = tokens[username]
// mock error
if (!token) {
return {
code: 60204,
message: 'Account and password are incorrect.'
}
}
return {
code: 20000,
data: token
}
}
},
// get user info
{
url: '/vue-admin-template/user/info\.*',
type: 'get',
response: config => {
const { token } = config.query
const info = users[token]
// mock error
if (!info) {
return {
code: 50008,
message: 'Login failed, unable to get user details.'
}
}
return {
code: 20000,
data: info
}
}
},
// user logout
{
url: '/vue-admin-template/user/logout',
type: 'post',
response: _ => {
return {
code: 20000,
data: 'success'
}
}
}
]

@ -0,0 +1,25 @@
/**
* @param {string} url
* @returns {Object}
*/
function param2Obj(url) {
const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
if (!search) {
return {}
}
const obj = {}
const searchArr = search.split('&')
searchArr.forEach(v => {
const index = v.indexOf('=')
if (index !== -1) {
const name = v.substring(0, index)
const val = v.substring(index + 1, v.length)
obj[name] = val
}
})
return obj
}
module.exports = {
param2Obj
}

@ -0,0 +1,68 @@
{
"name": "vue-admin-template",
"version": "4.4.0",
"description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
"author": "Pan <panfree23@gmail.com>",
"scripts": {
"dev": "vue-cli-service serve",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
"lint": "eslint --ext .js,.vue src",
"test:unit": "jest --clearCache && vue-cli-service test:unit",
"test:ci": "npm run lint && npm run test:unit"
},
"dependencies": {
"axios": "0.18.1",
"bpmn-js": "^7.3.1",
"bpmn-js-properties-panel": "^0.37.2",
"camunda-bpmn-moddle": "^4.4.0",
"core-js": "3.6.5",
"element-ui": "2.13.2",
"gojs": "^2.3.11",
"js-cookie": "2.2.0",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"vue": "2.6.10",
"vue-router": "3.0.6",
"vuex": "3.1.0",
"vxe-table": "^3.8.22",
"wujie-vue2": "^1.0.22"
},
"devDependencies": {
"@vue/cli-plugin-babel": "4.4.4",
"@vue/cli-plugin-eslint": "4.4.4",
"@vue/cli-plugin-unit-jest": "4.4.4",
"@vue/cli-service": "4.4.4",
"@vue/test-utils": "1.0.0-beta.29",
"autoprefixer": "9.5.1",
"babel-eslint": "10.1.0",
"babel-jest": "23.6.0",
"babel-plugin-dynamic-import-node": "2.3.3",
"chalk": "2.4.2",
"connect": "3.6.6",
"eslint": "6.7.2",
"eslint-plugin-vue": "6.2.2",
"html-webpack-plugin": "3.2.0",
"mockjs": "1.0.1-beta3",
"runjs": "4.3.2",
"sass": "1.26.8",
"sass-loader": "8.0.2",
"script-ext-html-webpack-plugin": "2.1.3",
"serve-static": "1.13.2",
"svg-sprite-loader": "4.1.3",
"svgo": "1.2.2",
"vue-template-compiler": "2.6.10"
},
"browserslist": [
"> 1%",
"last 2 versions"
],
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
},
"license": "MIT"
}

@ -0,0 +1,8 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
'plugins': {
// to edit target browsers: use "browserslist" field in package.json
'autoprefixer': {}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

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

@ -0,0 +1,11 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>

@ -0,0 +1,20 @@
import request from '@/utils/request'
export function departmentListNoAuth(params) {
return request({
url: '/api/admin/other/admin-department-list',
method: 'get',
params:params,
isLoading: false
})
}
export function adminListNoAuth(params) {
return request({
url: '/api/admin/other/admin-user-list',
method: 'get',
params:params,
isLoading: false
})
}

@ -0,0 +1,25 @@
import request from '@/utils/request';
export function index (params) {
return request({
url: "/api/backend/menu",
method: "get",
params
})
}
export function save (data) {
return request({
url: "/api/backend/menu/save",
method: "post",
data
})
}
export function destroy (data) {
return request({
url: "/api/backend/menu/delete",
method: "post",
data
})
}

@ -0,0 +1,33 @@
import request from '@/utils/request';
export function index (params) {
return request({
url: "/api/backend/module",
method: "get",
params
})
}
export function save (data) {
return request({
url: "/api/backend/module/save",
method: "post",
data
})
}
export function destroy (data) {
return request({
url: "/api/backend/module/delete",
method: "post",
data
})
}
export function getRoles (params) {
return request({
url: "/api/backend/module/grant/get-roles",
method: "get",
params
})
}

@ -0,0 +1,25 @@
import request from '@/utils/request';
export function index (params) {
return request({
url: "/api/backend/role",
method: "get",
params
})
}
export function save (data) {
return request({
url: "/api/backend/role/save",
method: "post",
data
})
}
export function destroy (data) {
return request({
url: "/api/backend/role/delete",
method: "post",
data
})
}

@ -0,0 +1,34 @@
import request from '@/utils/request'
export function login(data) {
return request({
url: '/api/auth/login',
method: 'post',
data,
isLoading: false
})
}
export function getInfo(token) {
return request({
url: '/api/auth/me',
method: 'post',
params: { token },
isLoading: false
})
}
export function logout() {
return request({
url: '/api/auth/logout',
method: 'post'
})
}
export function permissions () {
return request({
url: "/api/auth/permissions",
method: "post",
isLoading: false,
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

@ -0,0 +1,78 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script>
import pathToRegexp from 'path-to-regexp'
export default {
data() {
return {
levelList: null
}
},
watch: {
$route() {
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
// only show routes with meta.title
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
const first = matched[0]
if (!this.isDashboard(first)) {
matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
},
pathCompile(path) {
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
const { params } = this.$route
var toPath = pathToRegexp.compile(path)
return toPath(params)
},
handleLink(item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(this.pathCompile(path))
}
}
}
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>

@ -0,0 +1,44 @@
<template>
<div style="padding: 0 15px;" @click="toggleClick">
<svg
:class="{'is-active':isActive}"
class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
</div>
</template>
<script>
export default {
name: 'Hamburger',
props: {
isActive: {
type: Boolean,
default: false
}
},
methods: {
toggleClick() {
this.$emit('toggleClick')
}
}
}
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>

@ -0,0 +1,62 @@
<template>
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
import { isExternal } from '@/utils/validate'
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
}
}
}
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
}
</style>

@ -0,0 +1,9 @@
import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon'// svg component
// register globally
Vue.component('svg-icon', SvgIcon)
const req = require.context('./svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req)

@ -0,0 +1 @@
<svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg>

After

Width:  |  Height:  |  Size: 497 B

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -0,0 +1 @@
<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>

After

Width:  |  Height:  |  Size: 944 B

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.068 23.784c-1.02 0-1.877-.32-2.572-.96a8.588 8.588 0 0 1-1.738-2.237 11.524 11.524 0 0 1-1.042-2.621c-.232-.895-.348-1.641-.348-2.238V0h.278c.834 0 1.622.085 2.363.256.742.17 1.645.575 2.711 1.214 1.066.64 2.363 1.535 3.892 2.686 1.53 1.15 3.453 2.664 5.77 4.54 2.502 2.045 4.494 3.771 5.977 5.178 1.483 1.406 2.618 2.6 3.406 3.58.787.98 1.274 1.812 1.46 2.494.185.682.277 1.278.277 1.79v2.046H84.068zM127.3 84.01c.278.682.464 1.535.556 2.558.093 1.023-.37 2.003-1.39 2.94-.463.427-.88.832-1.25 1.215-.372.384-.696.704-.974.96a6.69 6.69 0 0 1-.973.767l-11.816-10.741a44.331 44.331 0 0 0 1.877-1.535 31.028 31.028 0 0 1 1.737-1.406c1.112-.938 2.317-1.343 3.615-1.215 1.297.128 2.363.405 3.197.83.927.427 1.923 1.173 2.989 2.239 1.065 1.065 1.876 2.195 2.432 3.388zM78.23 95.902c2.038 0 3.752-.511 5.143-1.534l-26.969 25.83H18.037c-1.761 0-3.684-.47-5.77-1.407a24.549 24.549 0 0 1-5.838-3.709 21.373 21.373 0 0 1-4.518-5.306c-1.204-2.003-1.807-4.07-1.807-6.202V16.495c0-1.79.44-3.665 1.32-5.626A18.41 18.41 0 0 1 5.04 5.562a21.798 21.798 0 0 1 5.213-3.964C12.198.533 14.237 0 16.37 0h53.24v15.984c0 1.62.278 3.367.834 5.242a16.704 16.704 0 0 0 2.572 5.179c1.159 1.577 2.665 2.898 4.518 3.964 1.853 1.066 4.078 1.598 6.673 1.598h20.295v42.325L85.458 92.45c1.02-1.364 1.529-2.856 1.529-4.476 0-2.216-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1c-2.409 0-4.448.789-6.116 2.366-1.668 1.577-2.502 3.474-2.502 5.69 0 2.217.834 4.092 2.502 5.626 1.668 1.535 3.707 2.302 6.117 2.302h52.13zM26.1 47.951c-2.41 0-4.449.789-6.117 2.366-1.668 1.577-2.502 3.473-2.502 5.69 0 2.216.834 4.092 2.502 5.626 1.668 1.534 3.707 2.302 6.117 2.302h52.13c2.409 0 4.47-.768 6.185-2.302 1.715-1.534 2.572-3.41 2.572-5.626 0-2.217-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1zm52.407 64.063l1.807-1.663 3.476-3.196a479.75 479.75 0 0 0 4.587-4.284 500.757 500.757 0 0 1 5.004-4.667c3.985-3.666 8.48-7.758 13.485-12.276l11.677 10.741-13.485 12.404-5.004 4.603-4.587 4.22a179.46 179.46 0 0 0-3.267 3.068c-.88.853-1.367 1.322-1.46 1.407-.463.341-.973.703-1.529 1.087-.556.383-1.112.703-1.668.959-.556.256-1.413.575-2.572.959a83.5 83.5 0 0 1-3.545 1.087 72.2 72.2 0 0 1-3.475.895c-1.112.256-1.946.426-2.502.511-1.112.17-1.854.043-2.224-.383-.371-.426-.464-1.151-.278-2.174.092-.511.278-1.279.556-2.302.278-1.023.602-2.067.973-3.132l1.042-3.005c.325-.938.58-1.577.765-1.918a10.157 10.157 0 0 1 2.224-2.941z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg>

After

Width:  |  Height:  |  Size: 285 B

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg>

After

Width:  |  Height:  |  Size: 821 B

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>

After

Width:  |  Height:  |  Size: 623 B

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg>

After

Width:  |  Height:  |  Size: 597 B

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

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

After

Width:  |  Height:  |  Size: 440 B

@ -0,0 +1,22 @@
# replace default config
# multipass: true
# full: true
plugins:
# - name
#
# or:
# - name: false
# - name: true
#
# or:
# - name:
# param1: 1
# param2: 2
- removeAttrs:
attrs:
- 'fill'
- 'fill-rule'

@ -0,0 +1,47 @@
<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 - 56px - 51px);
width: 100%;
position: relative;
overflow: hidden;
padding: 20px;
}
.fixed-header+.app-main {
padding-top: 50px;
}
@media (max-width: 768px) {
.app-main {
min-height: calc(100vh - 56px);
padding: 10px;
}
}
</style>
<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
.fixed-header {
padding-right: 15px;
}
}
</style>

@ -0,0 +1,36 @@
<script>
export default {
props: {
icon: String
},
render(h) {
if (this.icon) {
if (this.icon.includes('el-icon')) {
return <i class={[this.icon, 'sub-el-icon']} />
}
else if(this.icon.includes("iviewicon")){
let icon = this.icon.split("/")[1];
return <Icon type={icon} class='sub-el-icon' size="18" />
}
else if(this.icon.includes("iconfont")){
let icon = this.icon.split("/")[1]+" iconfont";
return <Icon custom={icon} class='sub-el-icon' size="18" />
}
else {
return <svg-icon class-name="sub-el-icon" icon-class={this.icon}/>
}
}
},
data() {
return {}
},
methods: {},
computed: {}
}
</script>
<style scoped lang="scss">
.sub-el-icon {
margin-right: 4px;
}
</style>

@ -0,0 +1,96 @@
<template>
<div class="sidebar-item" 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="onlyOneChild.key || onlyOneChild.path">
<icon :icon="onlyOneChild.meta.icon"></icon>{{ onlyOneChild.meta.title }}
</el-menu-item>
</app-link>
</template>
<template v-else>
<el-submenu ref="subMenu" :index="item.key || item.path" popper-append-to-body>
<template slot="title">
<icon :icon="item.meta.icon"></icon>{{ item.meta.title }}
</template>
<navbar-item v-for="child in item.children" :key="child.path" :item="child"
:base-path="resolvePath(child.path)" class="nest-menu" />
</el-submenu>
</template>
</div>
</template>
<script>
import path from 'path'
import AppLink from '../Sidebar/Link.vue'
import { isExternal } from '@/utils/validate'
import Icon from "./Icon.vue"
export default {
name: 'NavbarItem',
components: {
AppLink,
Icon
},
props: {
item: {
type: Object,
required: true,
},
basePath: {
type: String,
default: "",
},
},
data() {
return {
onlyOneChild: null,
};
},
methods: {
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
},
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
},
},
computed: {
},
};
</script>
<style scoped lang="scss"></style>

@ -0,0 +1,122 @@
<template>
<div>
<header class="navbar navbar-expand-md d-print-none">
<div class="container-xl">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
<router-link to="/">
<svg xmlns="http://www.w3.org/2000/svg" width="110" height="32" viewBox="0 0 232 68" class="navbar-brand-image">
<path d="M64.6 16.2C63 9.9 58.1 5 51.8 3.4 40 1.5 28 1.5 16.2 3.4 9.9 5 5 9.9 3.4 16.2 1.5 28 1.5 40 3.4 51.8 5 58.1 9.9 63 16.2 64.6c11.8 1.9 23.8 1.9 35.6 0C58.1 63 63 58.1 64.6 51.8c1.9-11.8 1.9-23.8 0-35.6zM33.3 36.3c-2.8 4.4-6.6 8.2-11.1 11-1.5.9-3.3.9-4.8.1s-2.4-2.3-2.5-4c0-1.7.9-3.3 2.4-4.1 2.3-1.4 4.4-3.2 6.1-5.3-1.8-2.1-3.8-3.8-6.1-5.3-2.3-1.3-3-4.2-1.7-6.4s4.3-2.9 6.5-1.6c4.5 2.8 8.2 6.5 11.1 10.9 1 1.4 1 3.3.1 4.7zM49.2 46H37.8c-2.1 0-3.8-1-3.8-3s1.7-3 3.8-3h11.4c2.1 0 3.8 1 3.8 3s-1.7 3-3.8 3z" fill="#066fd1" style="fill: var(--tblr-primary, #066fd1)"></path>
</svg>
</router-link>
</div>
<div class="navbar-nav flex-row order-md-last">
<div class="d-none d-md-flex">
<a class="nav-link px-0 hide-theme-dark" data-bs-toggle="tooltip" data-bs-placement="bottom" aria-label="Enable dark mode" data-bs-original-title="Enable dark mode">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z"></path></svg>
</a>
<a class="nav-link px-0 hide-theme-light" data-bs-toggle="tooltip" data-bs-placement="bottom" aria-label="Enable light mode" data-bs-original-title="Enable light mode">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path><path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7"></path></svg>
</a>
<div class="nav-item d-none d-md-flex me-3">
<a class="nav-link px-0" data-bs-toggle="dropdown" tabindex="-1" aria-label="Show notifications">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2 -3v-3a7 7 0 0 1 4 -6"></path><path d="M9 17v1a3 3 0 0 0 6 0v-1"></path></svg>
<span class="badge bg-red"></span>
</a>
</div>
</div>
<div class="nav-item">
<a class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
<span class="avatar avatar-sm" style=""></span>
<div class="d-none d-xl-block ps-2">
<div>{{ name }}</div>
<!-- <div class="mt-1 small text-secondary">UI Designer</div>-->
</div>
</a>
</div>
</div>
</div>
</header>
<header class="navbar-expand-md">
<div class="collapse navbar-collapse" id="navbar-menu">
<el-menu class="container-xl"
:text-color="variables.menuText"
:background-color="variables.menuBg"
:active-text-color="variables.menuActiveText"
:default-active="activeMenu"
mode="horizontal" @select="menuSelect">
<Item v-for="(item, index) in permission_routes" :key="item.key" :item="item" :base-path="item.path"></Item>
</el-menu>
</div>
<!-- <div class="collapse navbar-collapse" id="navbar-menu">-->
<!-- <div class="navbar">-->
<!-- <div class="container-xl">-->
<!-- <div class="row flex-fill align-items-center">-->
<!-- <div class="col">-->
<!-- <ul class="navbar-nav">-->
<!-- <Item v-for="(item, index) in permission_routes" :key="item.key" :item="item"></Item>-->
<!-- </ul>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
</header>
</div>
</template>
<script>
import variables from "@/styles/variables.scss"
import { mapGetters } from 'vuex'
import Item from "./Item.vue"
export default {
components: {
Item
},
data () {
return {
activeMenu: ""
}
},
computed: {
variables () {
return variables
},
...mapGetters([
'sidebar',
'avatar',
'name',
'permission_routes',
]),
// activeMenu() {
// const route = this.$route
// const { meta, path, matched } = route
// // if set path, the sidebar will highlight the path you set
// if (meta.activeMenu) {
// return meta.activeMenu
// }
// console.log(matched[matched.length-1].parent.path)
// return matched[matched.length-1].parent.path
// },
},
methods: {
menuSelect (index,path) {
this.activeMenu = path[0]
},
async logout() {
await this.$store.dispatch('user/logout')
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
}
},
created() {
}
}
</script>
<style lang="scss" scoped>
@import url('~@/styles/navbar.css');
</style>

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

@ -0,0 +1,41 @@
<script>
export default {
name: 'MenuItem',
functional: true,
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
render(h, context) {
const { icon, title } = context.props
const vnodes = []
if (icon) {
if (icon.includes('el-icon')) {
vnodes.push(<i class={[icon, 'sub-el-icon']} />)
} else {
vnodes.push(<svg-icon icon-class={icon}/>)
}
}
if (title) {
vnodes.push(<span slot='title'>{(title)}</span>)
}
return vnodes
}
}
</script>
<style scoped>
.sub-el-icon {
color: currentColor;
width: 1em;
height: 1em;
}
</style>

@ -0,0 +1,43 @@
<template>
<component :is="type" v-bind="linkProps(to)">
<slot />
</component>
</template>
<script>
import { isExternal } from '@/utils/validate'
export default {
props: {
to: {
type: String,
required: true
}
},
computed: {
isExternal() {
return isExternal(this.to)
},
type() {
if (this.isExternal) {
return 'a'
}
return 'router-link'
}
},
methods: {
linkProps(to) {
if (this.isExternal) {
return {
href: to,
target: '_blank',
rel: 'noopener'
}
}
return {
to: to
}
}
}
}
</script>

@ -0,0 +1,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: 'Vue Admin Template',
logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png'
}
}
}
</script>
<style lang="scss" scoped>
.sidebarLogoFade-enter-active {
transition: opacity 1.5s;
}
.sidebarLogoFade-enter,
.sidebarLogoFade-leave-to {
opacity: 0;
}
.sidebar-logo-container {
position: relative;
width: 100%;
height: 50px;
line-height: 50px;
background: #2b2f3a;
text-align: center;
overflow: hidden;
& .sidebar-logo-link {
height: 100%;
width: 100%;
& .sidebar-logo {
width: 32px;
height: 32px;
vertical-align: middle;
margin-right: 12px;
}
& .sidebar-title {
display: inline-block;
margin: 0;
color: #fff;
font-weight: 600;
line-height: 50px;
font-size: 14px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
vertical-align: middle;
}
}
&.collapse {
.sidebar-logo {
margin-right: 0px;
}
}
}
</style>

@ -0,0 +1,95 @@
<template>
<div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
</el-menu-item>
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
</template>
<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
mixins: [FixiOSBug],
props: {
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
data() {
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
// TODO: refactor with render function
this.onlyOneChild = null
return {}
},
methods: {
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
}
}
}
</script>

@ -0,0 +1,56 @@
<template>
<div :class="{'has-logo':showLogo}">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'sidebar'
]),
routes() {
return this.$router.options.routes
},
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>

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

@ -0,0 +1,94 @@
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<!-- <sidebar class="sidebar-container" />-->
<navbar></navbar>
<div class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<!-- <navbar />-->
</div>
<app-main />
</div>
</div>
</template>
<script>
import Navbar from "./components/Navbar"
import { AppMain } from './components'
import ResizeMixin from './mixin/ResizeHandler'
export default {
name: 'Layout',
components: {
Navbar,
AppMain
},
mixins: [ResizeMixin],
computed: {
sidebar() {
return this.$store.state.app.sidebar
},
device() {
return this.$store.state.app.device
},
fixedHeader() {
return this.$store.state.settings.fixedHeader
},
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
}
}
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/styles/mixin.scss";
@import "~@/styles/variables.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar{
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$sideBarWidth});
transition: width 0.28s;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px)
}
.mobile .fixed-header {
width: 100%;
}
</style>

@ -0,0 +1,45 @@
import store from '@/store'
const { body } = document
const WIDTH = 992 // refer to Bootstrap's responsive design
export default {
watch: {
$route(route) {
if (this.device === 'mobile' && this.sidebar.opened) {
store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
},
beforeMount() {
window.addEventListener('resize', this.$_resizeHandler)
},
beforeDestroy() {
window.removeEventListener('resize', this.$_resizeHandler)
},
mounted() {
const isMobile = this.$_isMobile()
if (isMobile) {
store.dispatch('app/toggleDevice', 'mobile')
store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
},
methods: {
// use $_ for mixins properties
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
$_isMobile() {
const rect = body.getBoundingClientRect()
return rect.width - 1 < WIDTH
},
$_resizeHandler() {
if (!document.hidden) {
const isMobile = this.$_isMobile()
store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
if (isMobile) {
store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
}
}
}
}

@ -0,0 +1,58 @@
import Vue from "vue";
import "normalize.css/normalize.css"; // A modern alternative to CSS resets
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
import locale from "element-ui/lib/locale/lang/en"; // lang i18n
import "@/styles/index.scss"; // global css
import App from "./App";
import store from "./store";
import router from "./router";
import "@/icons"; // icon
import "@/permission"; // permission control
/**
* If you don't want to use mock-server
* you want to use MockJs for mock api
* you can execute: mockXHR()
*
* Currently MockJs will be used in the production environment,
* please remove it before going online ! ! !
*/
if (process.env.NODE_ENV === "production") {
const { mockXHR } = require("../mock");
mockXHR();
}
// set ElementUI lang to EN
Vue.use(ElementUI, { locale });
// 如果想要中文版 element-ui按如下方式声明
// Vue.use(ElementUI)
import { VXETable, VxeIcon, VxeTable, VxeColumn, VxeColgroup, VxeTableEditModule } from "vxe-table";
import "vxe-table/styles/index.scss"
Vue.use(VxeTableEditModule);
Vue.use(VxeIcon);
Vue.use(VxeTable);
Vue.use(VxeColumn);
Vue.use(VxeColgroup);
//wujie
import Wujie from "wujie-vue2";
Vue.use(Wujie);
const { setupApp, preloadApp } = Wujie;
setupApp({
name: "contract",
url: "http://localhost:9530/admin/#/",
exec: true,
});
Vue.config.productionTip = false;
new Vue({
el: "#app",
router,
store,
render: (h) => h(App),
});

@ -0,0 +1,74 @@
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login'] // no redirect whitelist
let isFirstEnter = true; //第一次进入获取用户信息
router.beforeEach(async(to, from, next) => {
console.log(from.path,to.path)
// start progress bar
NProgress.start()
// set page title
document.title = getPageTitle(to.meta.title)
// determine whether the user has logged in
const hasToken = getToken()
if (hasToken && hasToken !== 'undefined' && hasToken !== 'null') {
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done()
} else {
if (!isFirstEnter) {
next()
} else {
try {
// get user info
const { roles } = await store.dispatch('user/getInfo')
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// dynamically add accessible routes
router.addRoutes(accessRoutes)
// hack method to ensure that addRoutes is complete
// set the replace: true, so the navigation will not leave a history record
isFirstEnter = false;
next({ ...to, replace: true })
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
isFirstEnter = true;
NProgress.done()
}
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
// other pages that do not have permission to access are redirected to the login page.
next(`/login?redirect=${to.path}`)
isFirstEnter = true;
NProgress.done()
}
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})

@ -0,0 +1,80 @@
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
/**
* Note: sub-menu only appear when route children.length >= 1
* Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
*
* hidden: true if set true, item will not show in the sidebar(default is false)
* alwaysShow: true if set true, will always show the root menu
* if not set alwaysShow, when item has more than one children route,
* it will becomes nested mode, otherwise not show the root menu
* redirect: noRedirect if set noRedirect will no redirect in the breadcrumb
* name:'router-name' the name is used by <keep-alive> (must set!!!)
* meta : {
roles: ['admin','editor'] control the page roles (you can set multiple roles)
title: 'title' the name show in sidebar and breadcrumb (recommend set)
icon: 'svg-name'/'el-icon-x' the icon show in the sidebar
breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
}
*/
/**
* constantRoutes
* a base page that does not have permission requirements
* all roles can be accessed
*/
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
meta: { title: '首页', icon: 'dashboard' },
children: [{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: '首页', icon: 'dashboard' }
}]
},
// 404 page must be placed at the end !!!
//{ path: '*', redirect: '/404', hidden: true }
]
export const asyncRoutes = [
]
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
const router = createRouter()
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router

@ -0,0 +1,17 @@
module.exports = {
title: 'Vue Admin Template',
copyright: '版权所有',
/**
* @type {boolean} true | false
* @description Whether fix the header
*/
fixedHeader: false,
/**
* @type {boolean} true | false
* @description Whether show the logo in sidebar
*/
sidebarLogo: false
}

@ -0,0 +1,11 @@
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,
admins: state => state.data.admins,
departments: state => state.data.departments,
permission_routes: state => state.permission.routes,
}
export default getters

@ -0,0 +1,23 @@
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import app from './modules/app'
import settings from './modules/settings'
import user from './modules/user'
import data from './modules/data'
import permission from './modules/permission'
import { init } from "./init"
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
app,
settings,
user,
data,
permission
},
getters
})
//init(store)
export default store

@ -0,0 +1,4 @@
export function init (store) {
store.dispatch('data/getAdmins')
store.dispatch('data/getDepartments')
}

@ -0,0 +1,48 @@
import Cookies from 'js-cookie'
const state = {
sidebar: {
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
withoutAnimation: false
},
device: 'desktop'
}
const mutations = {
TOGGLE_SIDEBAR: state => {
state.sidebar.opened = !state.sidebar.opened
state.sidebar.withoutAnimation = false
if (state.sidebar.opened) {
Cookies.set('sidebarStatus', 1)
} else {
Cookies.set('sidebarStatus', 0)
}
},
CLOSE_SIDEBAR: (state, withoutAnimation) => {
Cookies.set('sidebarStatus', 0)
state.sidebar.opened = false
state.sidebar.withoutAnimation = withoutAnimation
},
TOGGLE_DEVICE: (state, device) => {
state.device = device
}
}
const actions = {
toggleSideBar({ commit }) {
commit('TOGGLE_SIDEBAR')
},
closeSideBar({ commit }, { withoutAnimation }) {
commit('CLOSE_SIDEBAR', withoutAnimation)
},
toggleDevice({ commit }, device) {
commit('TOGGLE_DEVICE', device)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

@ -0,0 +1,36 @@
import { departmentListNoAuth, adminListNoAuth } from "@/api/common"
const state = {
admins: [],
departments: []
}
const mutations = {
SET_ADMINS: (state, admins) => {
state.admins = admins
},
SET_DEPARTMENTS: (state, departments) => {
state.departments = departments
}
}
const actions = {
getAdmins ({ commit }) {
adminListNoAuth().then(res => {
commit("SET_ADMINS", res.data)
})
},
getDepartments ({ commit }) {
departmentListNoAuth().then(res => {
commit("SET_DEPARTMENTS", res.data)
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

@ -0,0 +1,74 @@
import { asyncRoutes, constantRoutes } from '@/router'
import { permissions } from "@/api/user"
import path from "path"
import Layout from "@/layout"
const loadView = (view) => {
return (resolve) => require([`@/views${view}`], resolve);
}
export function filterAsyncRoutes(routes) {
const res = []
routes.forEach(route => {
if (!route.visible) return
let tmp= {
key: `key-${route.id}`,
path: route.path === '#' ? '' : route.path,
component: route.path === "#" ? Layout : loadView(route.path),
name: route.name,
hidden: !route.visible,
meta: {
title: route.title,
icon: route.icon,
guard: route.guard_name,
folder: route.folder
}
}
if (route.children) {
tmp.children = filterAsyncRoutes(route.children)
}
res.push(tmp)
})
//把404拦截放在最后匹配
res.push({ path: '*', redirect: '/404', hidden: true })
return res
}
const state = {
routes: [],
addRoutes: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
}
const actions = {
generateRoutes({ commit }, roles) {
return new Promise(async (resolve, reject) => {
try {
const routes = await permissions()
let accessedRoutes
accessedRoutes = filterAsyncRoutes(routes)
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
} catch (err) {
reject(err)
}
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

@ -0,0 +1,32 @@
import defaultSettings from '@/settings'
const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
const state = {
showSettings: showSettings,
fixedHeader: fixedHeader,
sidebarLogo: sidebarLogo
}
const mutations = {
CHANGE_SETTING: (state, { key, value }) => {
// eslint-disable-next-line no-prototype-builtins
if (state.hasOwnProperty(key)) {
state[key] = value
}
}
}
const actions = {
changeSetting({ commit }, data) {
commit('CHANGE_SETTING', data)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

@ -0,0 +1,103 @@
import { login, logout, getInfo } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { resetRouter } from '@/router'
const getDefaultState = () => {
return {
token: getToken(),
name: '',
avatar: '',
adminId: '',
role: [],
}
}
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_ROLE: (state, role) => {
state.role = role
},
SET_ADMIN_ID: (state, id) => {
state.adminId = id
}
}
const actions = {
// user login
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
console.log(response)
const { access_token } = response
commit('SET_TOKEN', access_token)
setToken(access_token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// get user info
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { name, avatar, id, role } = response
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
commit('SET_ADMIN_ID', id)
commit('SET_ROLE', role)
resolve(response)
}).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
}

@ -0,0 +1,49 @@
// cover some element-ui styles
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
font-weight: 400 !important;
}
.el-upload {
input[type="file"] {
display: none !important;
}
}
.el-upload__input {
display: none;
}
// to fixed https://github.com/ElemeFE/element/issues/2461
.el-dialog {
transform: none;
left: 0;
position: relative;
margin: 0 auto;
}
// refine element ui upload
.upload-container {
.el-upload {
width: 100%;
.el-upload-dragger {
width: 100%;
height: 200px;
}
}
}
// dropdown
.el-dropdown-menu {
a {
display: block
}
}
// to fix el-date-picker css style
.el-range-separator {
box-sizing: content-box;
}

@ -0,0 +1,65 @@
@import './variables.scss';
@import './mixin.scss';
@import './transition.scss';
@import './element-ui.scss';
@import './sidebar.scss';
body {
height: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
label {
font-weight: 700;
}
html {
height: 100%;
box-sizing: border-box;
}
#app {
height: 100%;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
a:focus,
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
div:focus {
outline: none;
}
.clearfix {
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
}
// main-container global css
.app-container {
padding: 20px;
}

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

File diff suppressed because it is too large Load Diff

@ -0,0 +1,17 @@
#navbar-menu {
.sidebar-item {
display: inline-block;
}
.el-menu-item, .el-submenu__title {
height: 50px;
line-height: 50px;
}
.el-submenu__icon-arrow {
position: initial;
margin-top: 0;
}
}
.el-menu--collapse .el-menu .el-submenu, .el-menu--popup {
min-width: 120px;
text-align: center;
}

@ -0,0 +1,224 @@
#app {
.main-container {
min-height: 100%;
transition: margin-left .28s;
position: relative;
}
.sidebar-container {
transition: width 0.28s;
width: $sideBarWidth !important;
background-color: $menuBg;
height: 100%;
position: fixed;
font-size: 0px;
top: 0;
bottom: 0;
left: 0;
z-index: 1001;
overflow: hidden;
// reset element-ui css
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
}
.scrollbar-wrapper {
overflow-x: hidden !important;
}
.el-scrollbar__bar.is-vertical {
right: 0px;
}
.el-scrollbar {
height: 100%;
}
&.has-logo {
.el-scrollbar {
height: calc(100% - 50px);
}
}
.is-horizontal {
display: none;
}
a {
display: inline-block;
width: 100%;
overflow: hidden;
}
.svg-icon {
margin-right: 16px;
}
.sub-el-icon {
margin-right: 12px;
margin-left: -2px;
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
}
// menu hover
.submenu-title-noDropdown,
.el-submenu__title {
&:hover {
background-color: $menuHover !important;
}
}
.is-active>.el-submenu__title {
color: $subMenuActiveText !important;
}
& .nest-menu .el-submenu>.el-submenu__title,
& .el-submenu .el-menu-item {
min-width: $sideBarWidth !important;
background-color: $subMenuBg !important;
&:hover {
background-color: $subMenuHover !important;
}
}
}
.hideSidebar {
.sidebar-container {
width: 54px !important;
}
.main-container {
}
.submenu-title-noDropdown {
padding: 0 !important;
position: relative;
.el-tooltip {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
.sub-el-icon {
margin-left: 19px;
}
}
}
.el-submenu {
overflow: hidden;
&>.el-submenu__title {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
.sub-el-icon {
margin-left: 19px;
}
.el-submenu__icon-arrow {
display: none;
}
}
}
.el-menu--collapse {
.el-submenu {
&>.el-submenu__title {
&>span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
}
}
.el-menu--collapse .el-menu .el-submenu {
min-width: $sideBarWidth !important;
}
// mobile responsive
.mobile {
.main-container {
margin-left: 0px;
}
.sidebar-container {
transition: transform .28s;
width: $sideBarWidth !important;
}
&.hideSidebar {
.sidebar-container {
pointer-events: none;
transition-duration: 0.3s;
transform: translate3d(-$sideBarWidth, 0, 0);
}
}
}
.withoutAnimation {
.main-container,
.sidebar-container {
transition: none;
}
}
}
// when menu collapsed
.el-menu--vertical {
&>.el-menu {
.svg-icon {
margin-right: 16px;
}
.sub-el-icon {
margin-right: 12px;
margin-left: -2px;
}
}
.nest-menu .el-submenu>.el-submenu__title,
.el-menu-item {
&:hover {
// you can use $subMenuHover
background-color: $menuHover !important;
}
}
// the scroll bar appears when the subMenu is too long
>.el-menu--popup {
max-height: 100vh;
overflow-y: auto;
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
}

@ -0,0 +1,48 @@
// global transition css
/* fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.28s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .5s;
}
.fade-transform-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* breadcrumb transition */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all .5s;
}
.breadcrumb-enter,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-move {
transition: all .5s;
}
.breadcrumb-leave-active {
position: absolute;
}

@ -0,0 +1,25 @@
// sidebar
$menuText:#333;
$menuActiveText:#409EFF;
$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
$menuBg:#fff;
$menuHover:#263445;
$subMenuBg:#1f2d3d;
$subMenuHover:#001528;
$sideBarWidth: 210px;
$navBarHeight: 200px;
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
menuText: $menuText;
menuActiveText: $menuActiveText;
subMenuActiveText: $subMenuActiveText;
menuBg: $menuBg;
menuHover: $menuHover;
subMenuBg: $subMenuBg;
subMenuHover: $subMenuHover;
sideBarWidth: $sideBarWidth;
}

@ -0,0 +1,15 @@
import Cookies from 'js-cookie'
const TokenKey = 'schj_admin_token'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}

@ -0,0 +1,10 @@
import defaultSettings from '@/settings'
const title = defaultSettings.title || 'Vue Admin Template'
export default function getPageTitle(pageTitle) {
if (pageTitle) {
return `${pageTitle} - ${title}`
}
return `${title}`
}

@ -0,0 +1,182 @@
/**
* Created by PanJiaChen on 16/11/18.
*/
/**
* Parse the time to string
* @param {(Object|string|number)} time
* @param {string} cFormat
* @returns {string | null}
*/
export function parseTime(time, cFormat) {
if (arguments.length === 0 || !time) {
return null
}
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
if ((typeof time === 'string')) {
if ((/^[0-9]+$/.test(time))) {
// support "1548221490638"
time = parseInt(time)
} else {
// support safari
// https://stackoverflow.com/questions/4310953/invalid-date-in-safari
time = time.replace(new RegExp(/-/gm), '/')
}
}
if ((typeof time === 'number') && (time.toString().length === 10)) {
time = time * 1000
}
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
const value = formatObj[key]
// Note: getDay() returns 0 on Sunday
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
return value.toString().padStart(2, '0')
})
return time_str
}
/**
* @param {number} time
* @param {string} option
* @returns {string}
*/
export function formatTime(time, option) {
if (('' + time).length === 10) {
time = parseInt(time) * 1000
} else {
time = +time
}
const d = new Date(time)
const now = Date.now()
const diff = (now - d) / 1000
if (diff < 30) {
return '刚刚'
} else if (diff < 3600) {
// less 1 hour
return Math.ceil(diff / 60) + '分钟前'
} else if (diff < 3600 * 24) {
return Math.ceil(diff / 3600) + '小时前'
} else if (diff < 3600 * 24 * 2) {
return '1天前'
}
if (option) {
return parseTime(time, option)
} else {
return (
d.getMonth() +
1 +
'月' +
d.getDate() +
'日' +
d.getHours() +
'时' +
d.getMinutes() +
'分'
)
}
}
/**
* @param {string} url
* @returns {Object}
*/
export function param2Obj(url) {
const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
if (!search) {
return {}
}
const obj = {}
const searchArr = search.split('&')
searchArr.forEach(v => {
const index = v.indexOf('=')
if (index !== -1) {
const name = v.substring(0, index)
const val = v.substring(index + 1, v.length)
obj[name] = val
}
})
return obj
}
export function deepCopy(data) {
//string,number,bool,null,undefined,symbol
//object,array,date
if (data && typeof data === "object") {
//针对函数的拷贝
if (typeof data === "function") {
let tempFunc = data.bind(null);
tempFunc.prototype = deepCopy(data.prototype);
return tempFunc;
}
switch (Object.prototype.toString.call(data)) {
case "[object String]":
return data.toString();
case "[object Number]":
return Number(data.toString());
case "[object Boolean]":
return Boolean(data.toString());
case "[object Date]":
return new Date(data.getTime());
case "[object Array]":
let arr = [];
for (let i = 0; i < data.length; i++) {
arr[i] = deepCopy(data[i]);
}
return arr;
//js自带对象或用户自定义类实例
case "[object Object]":
let obj = {};
for (let key in data) {
//会遍历原型链上的属性方法可以用hasOwnProperty来控制 obj.hasOwnProperty(prop)
obj[key] = deepCopy(data[key]);
}
return obj;
}
} else {
//string,number,bool,null,undefined,symbol
return data;
}
}
export function throttle(fn, interval, immediate=true) {
// 1.记录上一次的开始时间
let lastTime = 0
// 2.事件触发时, 真正执行的函数
return function(...args) {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
//当lastTime为0时且第一次不执行此时 remainTime = interval - (nowTime - lastTime) = interval > 0
if (!lastTime && !immediate) lastTime = nowTime//决定第一次是否执行函数
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 2.3.真正触发函数
fn.apply(this, args)
// 2.4.保留上次触发的时间
lastTime = nowTime
}
}
}

@ -0,0 +1,107 @@
import axios from 'axios'
import { MessageBox, Message, Loading } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
import { throttle } from "@/utils"
import Vue from "vue"
let loadingInstance;
const loading = throttle(() => {
loadingInstance = Loading.service({
lock:true,
spinner: "el-icon-loading",
background:"rgba(0,0,0,0.4)",
text:"正在加载中..."
})
}, 200)
// create an axios instance
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
if (config.isLoading) {
loading()
}
const token = getToken();
if (token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers['Authorization'] = `Bearer ${token}`;
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
Vue.prototype.$nextTick().then(r => loadingInstance?.close())
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 => {
loadingInstance?.close()
if (response.status !== 200) {
return Promise.reject("系统错误")
}
const res = response.data
// if the custom code is not 20000, it is judged as an error.
if (res.code !== 0) {
Message({
message: res.msg || '系统错误',
type: 'error',
duration: 5 * 1000
})
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code === 20001) {
// to re-login
MessageBox.alert('您的登录状态已过期,需重新登录',
'登录已过期', {
confirmButtonText: '确定',
type: 'warning',
closeOnClickModal: false,
showClose: false
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(res.msg || 'Error'))
} else {
return res.data
}
},
error => {
loadingInstance?.close()
console.error('err' + error) // for debug
Message({
message: /Network Error/g.test(error) ? "网络错误" : "系统错误",
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service

@ -0,0 +1,20 @@
/**
* Created by PanJiaChen on 16/11/18.
*/
/**
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUsername(str) {
const valid_map = ['admin', 'editor']
return valid_map.indexOf(str.trim()) >= 0
}

@ -0,0 +1,225 @@
<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">抱歉!</div>
<div class="bullshit__headline">{{ message }}</div>
<div class="bullshit__info">请检查您输入的网址是否正确或点击下面的按钮返回主页</div>
<a href="" class="bullshit__return-home">回到首页</a>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Page404',
computed: {
message() {
return '您无法进入这个页面...'
}
}
}
</script>
<style lang="scss" scoped>
.wscn-http404-container{
transform: translate(-50%,-50%);
position: absolute;
top: 40%;
left: 50%;
}
.wscn-http404 {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic-404 {
position: relative;
float: left;
width: 600px;
overflow: hidden;
&__parent {
width: 100%;
}
&__child {
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
&.mid {
width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1.2s;
}
&.right {
width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline {
font-size: 20px;
line-height: 24px;
color: #222;
font-weight: bold;
opacity: 0;
margin-bottom: 10px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
}
}
</style>

@ -0,0 +1,471 @@
<template>
<div class="containers">
<button @click="exportXml">exportXML</button>
<button @click="exportSvg">exportSVG</button>
<!-- canvas bpmn容器 -->
<div class="canvas" ref="canvas" id="canvas"/>
<!-- propertiesPanel 右侧属性栏容器 -->
<div id="propertiesPanel" class="panel"></div>
</div>
</template>
<script>
const bpmXmlStr = `<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL">
<bpmn2:process id="process1567044459787" name="流程1567044459787">
<bpmn2:documentation>描述</bpmn2:documentation>
<bpmn2:startEvent id="StartEvent_01ydzqe" name="开始">
<bpmn2:outgoing>SequenceFlow_1qw929z</bpmn2:outgoing>
</bpmn2:startEvent>
<bpmn2:sequenceFlow id="SequenceFlow_1qw929z" sourceRef="StartEvent_01ydzqe" targetRef="Task_1piqdk6" />
<bpmn2:userTask id="Task_1piqdk6" name="请假申请">
<bpmn2:incoming>SequenceFlow_1qw929z</bpmn2:incoming>
<bpmn2:outgoing>SequenceFlow_11h4o22</bpmn2:outgoing>
</bpmn2:userTask>
<bpmn2:exclusiveGateway id="ExclusiveGateway_0k39v3u">
<bpmn2:incoming>SequenceFlow_11h4o22</bpmn2:incoming>
<bpmn2:outgoing>SequenceFlow_1iu7pfe</bpmn2:outgoing>
<bpmn2:outgoing>SequenceFlow_04uqww2</bpmn2:outgoing>
</bpmn2:exclusiveGateway>
<bpmn2:sequenceFlow id="SequenceFlow_11h4o22" sourceRef="Task_1piqdk6" targetRef="ExclusiveGateway_0k39v3u" />
<bpmn2:sequenceFlow id="SequenceFlow_1iu7pfe" sourceRef="ExclusiveGateway_0k39v3u" targetRef="Task_10fqcwp" />
<bpmn2:userTask id="Task_10fqcwp" name="经理审批">
<bpmn2:incoming>SequenceFlow_1iu7pfe</bpmn2:incoming>
<bpmn2:outgoing>SequenceFlow_1xod8nh</bpmn2:outgoing>
</bpmn2:userTask>
<bpmn2:sequenceFlow id="SequenceFlow_04uqww2" sourceRef="ExclusiveGateway_0k39v3u" targetRef="Task_15n23yh" />
<bpmn2:userTask id="Task_15n23yh" name="总部审批">
<bpmn2:incoming>SequenceFlow_04uqww2</bpmn2:incoming>
<bpmn2:outgoing>SequenceFlow_0c8wrs4</bpmn2:outgoing>
</bpmn2:userTask>
<bpmn2:exclusiveGateway id="ExclusiveGateway_1sq33g6">
<bpmn2:incoming>SequenceFlow_0c8wrs4</bpmn2:incoming>
<bpmn2:incoming>SequenceFlow_1xod8nh</bpmn2:incoming>
<bpmn2:outgoing>SequenceFlow_0h8za82</bpmn2:outgoing>
</bpmn2:exclusiveGateway>
<bpmn2:sequenceFlow id="SequenceFlow_0c8wrs4" sourceRef="Task_15n23yh" targetRef="ExclusiveGateway_1sq33g6" />
<bpmn2:sequenceFlow id="SequenceFlow_1xod8nh" sourceRef="Task_10fqcwp" targetRef="ExclusiveGateway_1sq33g6" />
<bpmn2:endEvent id="EndEvent_0pnmjd3">
<bpmn2:incoming>SequenceFlow_0h8za82</bpmn2:incoming>
</bpmn2:endEvent>
<bpmn2:sequenceFlow id="SequenceFlow_0h8za82" sourceRef="ExclusiveGateway_1sq33g6" targetRef="EndEvent_0pnmjd3" />
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="process1567044459787">
<bpmndi:BPMNShape id="StartEvent_01ydzqe_di" bpmnElement="StartEvent_01ydzqe">
<dc:Bounds x="532" y="72" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="539" y="53" width="22" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1qw929z_di" bpmnElement="SequenceFlow_1qw929z">
<di:waypoint x="550" y="108" />
<di:waypoint x="550" y="150" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="UserTask_1qxjy46_di" bpmnElement="Task_1piqdk6">
<dc:Bounds x="500" y="150" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ExclusiveGateway_0k39v3u_di" bpmnElement="ExclusiveGateway_0k39v3u" isMarkerVisible="true">
<dc:Bounds x="525" y="275" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_11h4o22_di" bpmnElement="SequenceFlow_11h4o22">
<di:waypoint x="550" y="230" />
<di:waypoint x="550" y="275" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1iu7pfe_di" bpmnElement="SequenceFlow_1iu7pfe">
<di:waypoint x="575" y="300" />
<di:waypoint x="680" y="300" />
<di:waypoint x="680" y="380" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="UserTask_18pwui1_di" bpmnElement="Task_10fqcwp">
<dc:Bounds x="630" y="380" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_04uqww2_di" bpmnElement="SequenceFlow_04uqww2">
<di:waypoint x="525" y="300" />
<di:waypoint x="430" y="300" />
<di:waypoint x="430" y="380" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="UserTask_1j0us24_di" bpmnElement="Task_15n23yh">
<dc:Bounds x="380" y="380" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ExclusiveGateway_1sq33g6_di" bpmnElement="ExclusiveGateway_1sq33g6" isMarkerVisible="true">
<dc:Bounds x="525" y="515" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0c8wrs4_di" bpmnElement="SequenceFlow_0c8wrs4">
<di:waypoint x="430" y="460" />
<di:waypoint x="430" y="540" />
<di:waypoint x="525" y="540" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1xod8nh_di" bpmnElement="SequenceFlow_1xod8nh">
<di:waypoint x="680" y="460" />
<di:waypoint x="680" y="540" />
<di:waypoint x="575" y="540" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_0pnmjd3_di" bpmnElement="EndEvent_0pnmjd3">
<dc:Bounds x="532" y="602" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0h8za82_di" bpmnElement="SequenceFlow_0h8za82">
<di:waypoint x="550" y="565" />
<di:waypoint x="550" y="602" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>`
const translations = {
// Labels
'Activate the global connect tool' : '激活全局连接工具',
'Append {type}': '追加 {type}',
'Append EndEvent': '追加 结束事件 ',
'Append Task':'追加 任务',
'TextAnnotation':'追加 注释文本',
'Append Gateway':'追加 网关',
'Append Intermediate/Boundary Event':'追加 中间/边界 事件',
'Add Lane above': '在上面添加道',
'Divide into two Lanes': '分割成两个道',
'Divide into three Lanes': '分割成三个道',
'Add Lane below': '在下面添加道',
'Append compensation activity': '追加补偿活动',
'Change type': '修改类型',
'Connect using Association': '使用关联连接',
'Connect using Sequence/MessageFlow or Association': '使用顺序/消息流或者关联连接',
'Connect using DataInputAssociation': '使用数据输入关联连接',
'Remove': '移除',
'Activate the hand tool': '激活抓手工具',
'Activate the lasso tool': '激活套索工具',
'Activate the create/remove space tool': '激活创建/删除空间工具',
'Create expanded SubProcess': '创建扩展子过程',
'Create IntermediateThrowEvent/BoundaryEvent' : '创建中间抛出事件/边界事件',
'Create Pool/Participant': '创建池/参与者',
'Parallel Multi Instance': '并行多重事件',
'Sequential Multi Instance': '时序多重事件',
'DataObjectReference':'数据对象参考',
'DataStoreReference':'数据存储参考',
'Loop': '循环',
'Ad-hoc': '即席',
'Create {type}': '创建 {type}',
'Create Task':'创建任务',
'Create StartEvent':'创建开始事件',
'Create EndEvent':'创建结束事件',
'Create Group':'创建组',
'Task': '任务',
'Send Task': '发送任务',
'Receive Task': '接收任务',
'User Task': '用户任务',
'Manual Task': '手工任务',
'Business Rule Task': '业务规则任务',
'Service Task': '服务任务',
'Script Task': '脚本任务',
'Call Activity': '调用活动',
'Sub Process (collapsed)': '子流程(折叠的)',
'Sub Process (expanded)': '子流程(展开的)',
'Start Event': '开始事件',
'StartEvent': '开始事件',
'Intermediate Throw Event': '中间事件',
'End Event': '结束事件',
'EndEvent': '结束事件',
'Create Gateway': '创建网关',
'GateWay':'网关',
'Create Intermediate/Boundary Event': '创建中间/边界事件',
'Message Start Event': '消息开始事件',
'Timer Start Event': '定时开始事件',
'Conditional Start Event': '条件开始事件',
'Signal Start Event': '信号开始事件',
'Error Start Event': '错误开始事件',
'Escalation Start Event': '升级开始事件',
'Compensation Start Event': '补偿开始事件',
'Message Start Event (non-interrupting)': '消息开始事件(非中断)',
'Timer Start Event (non-interrupting)': '定时开始事件(非中断)',
'Conditional Start Event (non-interrupting)': '条件开始事件(非中断)',
'Signal Start Event (non-interrupting)': '信号开始事件(非中断)',
'Escalation Start Event (non-interrupting)': '升级开始事件(非中断)',
'Message Intermediate Catch Event': '消息中间捕获事件',
'Message Intermediate Throw Event': '消息中间抛出事件',
'Timer Intermediate Catch Event': '定时中间捕获事件',
'Escalation Intermediate Throw Event': '升级中间抛出事件',
'Conditional Intermediate Catch Event': '条件中间捕获事件',
'Link Intermediate Catch Event': '链接中间捕获事件',
'Link Intermediate Throw Event': '链接中间抛出事件',
'Compensation Intermediate Throw Event': '补偿中间抛出事件',
'Signal Intermediate Catch Event': '信号中间捕获事件',
'Signal Intermediate Throw Event': '信号中间抛出事件',
'Message End Event': '消息结束事件',
'Escalation End Event': '定时结束事件',
'Error End Event': '错误结束事件',
'Cancel End Event': '取消结束事件',
'Compensation End Event': '补偿结束事件',
'Signal End Event': '信号结束事件',
'Terminate End Event': '终止结束事件',
'Message Boundary Event': '消息边界事件',
'Message Boundary Event (non-interrupting)': '消息边界事件(非中断)',
'Timer Boundary Event': '定时边界事件',
'Timer Boundary Event (non-interrupting)': '定时边界事件(非中断)',
'Escalation Boundary Event': '升级边界事件',
'Escalation Boundary Event (non-interrupting)': '升级边界事件(非中断)',
'Conditional Boundary Event': '条件边界事件',
'Conditional Boundary Event (non-interrupting)': '条件边界事件(非中断)',
'Error Boundary Event': '错误边界事件',
'Cancel Boundary Event': '取消边界事件',
'Signal Boundary Event': '信号边界事件',
'Signal Boundary Event (non-interrupting)': '信号边界事件(非中断)',
'Compensation Boundary Event': '补偿边界事件',
'Exclusive Gateway': '互斥网关',
'Parallel Gateway': '并行网关',
'Inclusive Gateway': '相容网关',
'Complex Gateway': '复杂网关',
'Event based Gateway': '事件网关',
'Transaction': '转运',
'Sub Process': '子流程',
'Event Sub Process': '事件子流程',
'Collapsed Pool': '折叠池',
'Expanded Pool': '展开池',
// Errors
'no parent for {element} in {parent}': '在{parent}里,{element}没有父类',
'no shape type specified': '没有指定的形状类型',
'flow elements must be children of pools/participants': '流元素必须是池/参与者的子类',
'out of bounds release': 'out of bounds release',
'more than {count} child lanes': '子道大于{count} ',
'element required': '元素不能为空',
'diagram not part of bpmn:Definitions': '流程图不符合bpmn规范',
'no diagram to display': '没有可展示的流程图',
'no process or collaboration to display': '没有可展示的流程/协作',
'element {element} referenced by {referenced}#{property} not yet drawn': '由{referenced}#{property}引用的{element}元素仍未绘制',
'already rendered {element}': '{element} 已被渲染',
'failed to import {element}': '导入{element}失败',
//
'Id':'编号',
'Name':'名称',
'General':'常规',
'Details':'详情',
'Message Name':'消息名称',
'Message':'消息',
'Initiator':'创建者',
'Asynchronous Continuations':'持续异步',
'Asynchronous Before':'异步前',
'Asynchronous After':'异步后',
'Job Configuration':'工作配置',
'Exclusive':'排除',
'Job Priority':'工作优先级',
'Retry Time Cycle':'重试时间周期',
'Documentation':'文档',
'Element Documentation':'元素文档',
'History Configuration':'历史配置',
'History Time To Live':'历史的生存时间',
'Forms':'表单',
'Form Key':'表单key',
'Form Fields':'表单字段',
'Business Key':'业务key',
'Form Field':'表单字段',
'ID':'编号',
'Type':'类型',
'Label':'名称',
'Default Value':'默认值',
'Validation':'校验',
'Add Constraint':'添加约束',
'Config':'配置',
'Properties':'属性',
'Add Property':'添加属性',
'Value':'值',
'Add':'添加',
'Values':'值',
'Add Value':'添加值',
'Listeners':'监听器',
'Execution Listener':'执行监听',
'Event Type':'事件类型',
'Listener Type':'监听器类型',
'Java Class':'Java类',
'Expression':'表达式',
'Must provide a value':'必须提供一个值',
'Delegate Expression':'代理表达式',
'Script':'脚本',
'Script Format':'脚本格式',
'Script Type':'脚本类型',
'Inline Script':'内联脚本',
'External Script':'外部脚本',
'Resource':'资源',
'Field Injection':'字段注入',
'Extensions':'扩展',
'Input/Output':'输入/输出',
'Input Parameters':'输入参数',
'Output Parameters':'输出参数',
'Parameters':'参数',
'Output Parameter':'输出参数',
'Timer Definition Type':'定时器定义类型',
'Timer Definition':'定时器定义',
'Date':'日期',
'Duration':'持续',
'Cycle':'循环',
'Signal':'信号',
'Signal Name':'信号名称',
'Escalation':'升级',
'Error':'错误',
'Link Name':'链接名称',
'Condition':'条件名称',
'Variable Name':'变量名称',
'Variable Event':'变量事件',
'Specify more than one variable change event as a comma separated list.':'多个变量事件以逗号隔开',
'Wait for Completion':'等待完成',
'Activity Ref':'活动参考',
'Version Tag':'版本标签',
'Executable':'可执行文件',
'External Task Configuration':'扩展任务配置',
'Task Priority':'任务优先级',
'External':'外部',
'Connector':'连接器',
'Must configure Connector':'必须配置连接器',
'Connector Id':'连接器编号',
'Implementation':'实现方式',
'Field Injections':'字段注入',
'Fields':'字段',
'Result Variable':'结果变量',
'Topic':'主题',
'Configure Connector':'配置连接器',
'Input Parameter':'输入参数',
'Assignee':'代理人',
'Candidate Users':'候选用户',
'Candidate Groups':'候选组',
'Due Date':'到期时间',
'Follow Up Date':'跟踪日期',
'Priority':'优先级',
'The follow up date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)':'跟踪日期必须符合EL表达式 ${someDate} ,或者一个ISO标准日期2015-06-26T09:54:00',
'The due date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)':'跟踪日期必须符合EL表达式 ${someDate} ,或者一个ISO标准日期2015-06-26T09:54:00',
'Variables':'变量',
'Candidate Starter Configuration':'候选开始配置',
'Task Listener':'任务监听器',
'Candidate Starter Groups':'候选开始组',
'Candidate Starter Users':'候选开始用户',
'Tasklist Configuration':'任务列表配置',
'Startable':'启动',
'Specify more than one group as a comma separated list.':'指定多个组,用逗号分隔',
'Specify more than one user as a comma separated list.':'指定多个用户,用逗号分隔',
'This maps to the process definition key.':'这会映射为流程定义的键',
'CallActivity Type':'调用活动类型',
'Condition Type':'条件类型',
'Create UserTask':'创建用户任务',
'Create CallActivity':'创建调用活动',
'Called Element':'调用元素',
'Create DataObjectReference':'创建数据对象引用',
'Create DataStoreReference':'创建数据存储引用',
'Multi Instance':'多实例',
'Loop Cardinality':'实例数量',
'Collection':'任务参与人列表',
'Element Variable':'元素变量',
'Completion Condition':'完成条件'
}
const customTranslate = (template, replacements) => {
replacements = replacements || {};
// Translate
template = translations[template] || template;
// Replace
return template.replace(/{([^}]+)}/g, function (_, key) {
let str = replacements[key];
if (translations[replacements[key]] != null && translations[replacements[key]] != 'undefined') {
str = translations[replacements[key]];
}
return str || '{' + key + '}';
});
}
import BpmnModeler from "bpmn-js/lib/Modeler"; // bpmn-js
// propertiesPanelModule propertiesProviderModule camundaModdleDescriptor
import propertiesPanelModule from "bpmn-js-properties-panel";
import propertiesProviderModule from "bpmn-js-properties-panel/lib/provider/camunda";
import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda.json';
export default {
data() {
return {
bpmnModeler: null
}
},
computed: {},
mounted() {
this.initModeler();
},
methods: {
initXML() {
//
this.bpmnModeler.importXML(bpmXmlStr, err => {
if (err) {
console.error(err);
console.error("初始化模板异常");
}
});
},
initModeler() {
//
const customTranslateModule = {
translate: ['value', customTranslate]
}
//
this.bpmnModeler = new BpmnModeler({
container: "#canvas",
propertiesPanel: {
parent: "#propertiesPanel"
},
additionalModules: [
//
customTranslateModule,
propertiesPanelModule,
propertiesProviderModule
],
moddleExtensions: {
camunda: camundaModdleDescriptor
},
//
keyboard: {
bindTo: document // window
}
});
this.initXML();
},
exportXml () {
this.bpmnModeler.saveXML({ format: true },(err, xml) => {
console.log(xml)
})
},
exportSvg () {
this.bpmnModeler.saveSVG({},(err, svg) => {
console.log(svg)
const encodedData = encodeURIComponent(svg);
// ,aclassNameahrefdownloaddata
let xmlFile = new File([svg], "test.bpmn");
console.log(xmlFile);
if (svg) {
let a = document.createElement('a');
a.setAttribute('href',"data:application/bpmn20-xml;charset=UTF-8," + encodedData)
a.download = 'test.svg'
a.click()
}
})
}
}
}
</script>
<style scoped lang="scss">
@import "~bpmn-js/dist/assets/diagram-js.css";
@import "~bpmn-js/dist/assets/bpmn-font/css/bpmn.css";
@import "~bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css";
@import "~bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css";
@import "~bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css";
.containers {
position: absolute;
background-color: #ffffff;
width: 100%;
height: 100%;
}
.canvas {
width: 100%;
height: 100%;
}
.panel {
position: absolute;
right: 0;
top: 0;
bottom: 0;
overflow-y: scroll;
width: 300px;
}
</style>

@ -0,0 +1,30 @@
<template>
<div class="dashboard-container">
<div class="dashboard-text">name: {{ name }}</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'Dashboard',
computed: {
...mapGetters([
'name'
])
}
}
</script>
<style lang="scss" scoped>
.dashboard {
&-container {
margin: 30px;
}
&-text {
font-size: 30px;
line-height: 46px;
}
}
</style>

@ -0,0 +1,353 @@
<template>
<div class="body">
<div id="paletteDiv"></div>
<div id="diagramContainer" />
<ul id="contextMenu" class="ctxmenu" v-show="isShowContextMenu" :style="{ 'left': contextMenuPt.x + 'px', 'top': contextMenuPt.y + 'px' }">
<li class="menu-item" @click="changeLine('line')">
line
</li>
<li id="color" class="menu-item" @click="changeLine('dashed')">
dashed
</li>
</ul>
</div>
</template>
<script>
import go from "gojs";
const $ = go.GraphObject.make;
export default {
data() {
return {
diagram: "",
palette: "",
isShowContextMenu: false,
contextMenuPt: {
x: 100,
y: 100
}
};
},
methods: {
init() {
let myDiagram = this.diagram =
$(go.Diagram, "diagramContainer", // must name or refer to the DIV HTML element
{
grid: $(go.Panel, "Grid",
$(go.Shape, "LineH", { stroke: "lightgray", strokeWidth: 0.5 }),
$(go.Shape, "LineH", { stroke: "gray", strokeWidth: 0.5, interval: 10 }),
$(go.Shape, "LineV", { stroke: "lightgray", strokeWidth: 0.5 }),
$(go.Shape, "LineV", { stroke: "gray", strokeWidth: 0.5, interval: 10 })
),
"draggingTool.dragsLink": true,
"draggingTool.isGridSnapEnabled": true,
"linkingTool.isUnconnectedLinkValid": true,
"linkingTool.portGravity": 20,
"relinkingTool.isUnconnectedLinkValid": true,
"relinkingTool.portGravity": 20,
"relinkingTool.fromHandleArchetype":
$(go.Shape, "Diamond", { segmentIndex: 0, cursor: "pointer", desiredSize: new go.Size(8, 8), fill: "tomato", stroke: "darkred" }),
"relinkingTool.toHandleArchetype":
$(go.Shape, "Diamond", { segmentIndex: -1, cursor: "pointer", desiredSize: new go.Size(8, 8), fill: "darkred", stroke: "tomato" }),
"linkReshapingTool.handleArchetype":
$(go.Shape, "Diamond", { desiredSize: new go.Size(7, 7), fill: "lightblue", stroke: "deepskyblue" }),
"rotatingTool.handleAngle": 270,
"rotatingTool.handleDistance": 30,
"rotatingTool.snapAngleMultiple": 15,
"rotatingTool.snapAngleEpsilon": 15,
"undoManager.isEnabled": true
});
// Define a function for creating a "port" that is normally transparent.
// The "name" is used as the GraphObject.portId, the "spot" is used to control how links connect
// and where the port is positioned on the node, and the boolean "output" and "input" arguments
// control whether the user can draw links from or to the port.
function makePort(name, spot, output, input) {
// the port is basically just a small transparent square
return $(go.Shape, "Circle",
{
fill: null, // not seen, by default; set to a translucent gray by showSmallPorts, defined below
stroke: null,
desiredSize: new go.Size(7, 7),
alignment: spot, // align the port on the main Shape
alignmentFocus: spot, // just inside the Shape
portId: name, // declare this object to be a "port"
fromSpot: spot, toSpot: spot, // declare where links may connect at this port
fromLinkable: output, toLinkable: input, // declare whether the user may draw links to/from here
cursor: "pointer" // show a different cursor to indicate potential link point
});
}
let nodeSelectionAdornmentTemplate =
$(go.Adornment, "Auto",
$(go.Shape, { fill: null, stroke: "deepskyblue", strokeWidth: 1.5, strokeDashArray: [4, 2] }),
$(go.Placeholder)
);
let nodeResizeAdornmentTemplate =
$(go.Adornment, "Spot",
{ locationSpot: go.Spot.Right },
$(go.Placeholder),
$(go.Shape, { alignment: go.Spot.TopLeft, cursor: "nw-resize", desiredSize: new go.Size(6, 6), fill: "lightblue", stroke: "deepskyblue" }),
$(go.Shape, { alignment: go.Spot.Top, cursor: "n-resize", desiredSize: new go.Size(6, 6), fill: "lightblue", stroke: "deepskyblue" }),
$(go.Shape, { alignment: go.Spot.TopRight, cursor: "ne-resize", desiredSize: new go.Size(6, 6), fill: "lightblue", stroke: "deepskyblue" }),
$(go.Shape, { alignment: go.Spot.Left, cursor: "w-resize", desiredSize: new go.Size(6, 6), fill: "lightblue", stroke: "deepskyblue" }),
$(go.Shape, { alignment: go.Spot.Right, cursor: "e-resize", desiredSize: new go.Size(6, 6), fill: "lightblue", stroke: "deepskyblue" }),
$(go.Shape, { alignment: go.Spot.BottomLeft, cursor: "se-resize", desiredSize: new go.Size(6, 6), fill: "lightblue", stroke: "deepskyblue" }),
$(go.Shape, { alignment: go.Spot.Bottom, cursor: "s-resize", desiredSize: new go.Size(6, 6), fill: "lightblue", stroke: "deepskyblue" }),
$(go.Shape, { alignment: go.Spot.BottomRight, cursor: "sw-resize", desiredSize: new go.Size(6, 6), fill: "lightblue", stroke: "deepskyblue" })
);
let nodeRotateAdornmentTemplate =
$(go.Adornment,
{ locationSpot: go.Spot.Center, locationObjectName: "CIRCLE" },
$(go.Shape, "Circle", { name: "CIRCLE", cursor: "pointer", desiredSize: new go.Size(7, 7), fill: "lightblue", stroke: "deepskyblue" }),
$(go.Shape, { geometryString: "M3.5 7 L3.5 30", isGeometryPositioned: true, stroke: "deepskyblue", strokeWidth: 1.5, strokeDashArray: [4, 2] })
);
function hideCX() {
if (myDiagram.currentTool instanceof go.ContextMenuTool) {
myDiagram.currentTool.doCancel();
}
}
let myContextMenu = $(go.HTMLInfo, {
show: (obj, diagram, tool) => {
// Show only the relevant buttons given the current state.
this.isShowContextMenu = true;
console.log(this.isShowContextMenu)
// we don't bother overriding positionContextMenu, we just do it here:
let mousePt = diagram.lastInput.viewPoint;
this.contextMenuPt.x = mousePt.x + 5;
this.contextMenuPt.y = mousePt.y;
// Optional: Use a `window` click listener with event capture to
// remove the context menu if the user clicks elsewhere on the page
window.addEventListener("click", hideCX, true);
},
hide: () => {
this.isShowContextMenu = false;
// Optional: Use a `window` click listener with event capture to
// remove the context menu if the user clicks elsewhere on the page
window.removeEventListener("click", hideCX, true);
}
});
myDiagram.nodeTemplate =
$(go.Node, "Spot",
{ contextMenu: myContextMenu },
{ locationSpot: go.Spot.Center },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
{ selectable: true, selectionAdornmentTemplate: nodeSelectionAdornmentTemplate },
{ resizable: true, resizeObjectName: "PANEL", resizeAdornmentTemplate: nodeResizeAdornmentTemplate },
{ rotatable: true, rotateAdornmentTemplate: nodeRotateAdornmentTemplate },
new go.Binding("angle").makeTwoWay(),
// the main object is a Panel that surrounds a TextBlock with a Shape
$(go.Panel, "Auto",
{ name: "PANEL" },
new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
$(go.Shape, "Rectangle", // default figure
{
portId: "", // the default port: if no spot on link data, use closest side
fromLinkable: true,
toLinkable: true,
cursor: "pointer",
fill: "white", // default color
strokeWidth: 2,
},
new go.Binding('strokeDashArray','lineType',(lineType) => {
return lineType === 'dashed' ? [4,2] : null
}),
new go.Binding("figure"),
new go.Binding("fill")),
$(go.TextBlock,
{
font: "bold 11pt Helvetica, Arial, sans-serif",
margin: 8,
maxSize: new go.Size(160, NaN),
wrap: go.TextBlock.WrapFit,
editable: true
},
new go.Binding("text").makeTwoWay())
),
// four small named ports, one on each side:
makePort("T", go.Spot.Top, false, true),
makePort("L", go.Spot.Left, true, true),
makePort("R", go.Spot.Right, true, true),
makePort("B", go.Spot.Bottom, true, false),
{ // handle mouse enter/leave events to show/hide the ports
mouseEnter: function(e, node) { showSmallPorts(node, true); },
mouseLeave: function(e, node) { showSmallPorts(node, false); }
}
);
function changeLinkType(obj, type) {
let link = obj.part.adornedPart;
console.log(link)
if (link !== null) {
let shape = link.findObject("MYLINK");
console.log(shape)
if (shape !== null) {
if (type === 'solid') {
shape.strokeDashArray = null; // 线
} else if (type === 'dashed') {
shape.strokeDashArray = [4, 2]; // 线
}
}
}
}
function showSmallPorts(node, show) {
node.ports.each(function(port) {
if (port.portId !== "") { // don't change the default port, which is the big shape
port.fill = show ? "rgba(0,0,0,.3)" : null;
}
});
}
let linkSelectionAdornmentTemplate =
$(go.Adornment, "Link",
$(go.Shape,
// isPanelMain declares that this Shape shares the Link.geometry
{ isPanelMain: true, fill: null, stroke: "deepskyblue", strokeWidth: 0 }) // use selection object's strokeWidth
);
myDiagram.linkTemplate =
$(go.Link, // the whole link panel
{ selectable: true, selectionAdornmentTemplate: linkSelectionAdornmentTemplate },
{ relinkableFrom: true, relinkableTo: true, reshapable: true },
{
contextMenu: //
$("ContextMenu",
$("ContextMenuButton",
$(go.TextBlock, "直线"),
{ click: function(e, obj) { changeLinkType(obj, 'solid'); } }),
$("ContextMenuButton",
$(go.TextBlock, "虚线"),
{ click: function(e, obj) { changeLinkType(obj, 'dashed'); } })
)
},
{
curve: go.Link.Bezier,
adjusting: go.Link.Stretch,
reshapable: true,
relinkableFrom: true,
relinkableTo: true,
toShortLength: 3,
},
new go.Binding("points").makeTwoWay(),
$(go.Shape, // the link path shape
{ name: 'MYLINK', isPanelMain: true, strokeWidth: 2, strokeDashArray: [4,2] },
),
$(go.Shape, // the arrowhead
{ toArrow: "Standard", stroke: null }),
$(go.Panel, "Auto",
new go.Binding("visible", "isSelected").ofObject(),
$(go.Shape, "RoundedRectangle", // the link shape
{ fill: "#F8F8F8", stroke: null }),
$(go.TextBlock,
{
textAlign: "center",
font: "10pt helvetica, arial, sans-serif",
stroke: "#919191",
margin: 2,
minSize: new go.Size(10, NaN),
editable: true
},
new go.Binding("text").makeTwoWay())
)
);
//load(); // load an initial diagram from some JSON text
// initialize the Palette that is on the left side of the page
let myPalette = this.palette
myPalette =
$(go.Palette, "paletteDiv", // must name or refer to the DIV HTML element
{
maxSelectionCount: 1,
nodeTemplateMap: myDiagram.nodeTemplateMap, // share the templates used by myDiagram
model: new go.GraphLinksModel([ // specify the contents of the Palette
{ text: "Start", figure: "Circle", fill: "#fff" },
{ text: "Step" },
{ text: "DB", figure: "Database", fill: "lightgray" },
{ text: "Select", figure: "Diamond", fill: "lightskyblue" },
{ text: "End", figure: "Circle", fill: "#CE0620" },
{ text: "Comment", figure: "RoundedRectangle", fill: "lightyellow" }
], [
// the Palette also has a disconnected Link, which the user can drag-and-drop
{ points: new go.List(/*go.Point*/).addAll([new go.Point(0, 0), new go.Point(30, 0), new go.Point(30, 40), new go.Point(60, 40)]) }
])
});
},
changeLine (type='line') {
this.diagram.selection.each((node) => {
if (node instanceof go.Node) { // ignore any selected Links and simple Parts
// Examine and modify the data, not the Node directly.
let data = node.data;
// Call setDataProperty to support undo/redo as well as
// automatically evaluating any relevant bindings.
this.diagram.model.setDataProperty(data, 'lineType', type);
}
})
}
},
computed: {},
mounted() {
this.init();
},
};
</script>
<style scoped lang="scss">
.body {
position: absolute;
inset: 0 0 0 0;
}
#paletteDiv {
border-right: 1px solid #eee;
z-index: 3;
background: #fff;
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 14%;
}
#diagramContainer {
width: 100%;
height: 100%;
}
.ctxmenu {
position: absolute;
margin: 0;
padding: 8px 0;
z-index: 999;
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, .2), 0 8px 10px 1px rgba(0, 0, 0, .14), 0 3px 14px 2px rgba(0, 0, 0, .12);
list-style: none;
background-color: #ffffff;
border-radius: 4px;
}
.menu-item {
display: block;
position: relative;
min-width: 60px;
margin: 0;
padding: 6px 16px;
font: bold 12px sans-serif;
color: rgba(0, 0, 0, .87);
cursor: pointer;
}
.menu-item::before {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
content: "";
width: 100%;
height: 100%;
}
.menu-item:hover::before {
opacity: .04;
}
.menu .menu {
top: -8px;
left: 100%;
}
.show-menu, .menu-item:hover .ctxmenu {
display: block;
opacity: 1;
}
</style>

@ -0,0 +1,427 @@
<template>
<div class="login-container">
<img class="build-img" src="~@/assets/login/build.png" alt="">
<!-- <vue-particles color="#ffffff" :particleOpacity="0.7" :particlesNumber="80" shapeType="circle" :particleSize="4"-->
<!-- linesColor="#ffffff" :linesWidth="1" :lineLinked="true" :lineOpacity="0.4" :linesDistance="150" :moveSpeed="3"-->
<!-- :hoverEffect="true" hoverMode="grab" :clickEffect="true" clickMode="push"> </vue-particles>-->
<!-- <img class="title-img" src="~@/assets/login/title.png" alt="">-->
<el-form size="small" ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on"
label-position="left">
<div class="title-container">
<h3 class="title">欢迎登录</h3>
</div>
<el-form-item prop="username">
<div class="form-item">
<span class="svg-container">
<svg-icon icon-class="user" />
</span>
<el-input ref="username" v-model="loginForm.username" placeholder="请输入登录名" name="username" type="text"
tabindex="1" auto-complete="on" />
</div>
</el-form-item>
<el-form-item prop="password">
<div class="form-item">
<span class="svg-container">
<svg-icon icon-class="password" />
</span>
<el-input :key="passwordType" ref="password" v-model="loginForm.password" :type="passwordType"
placeholder="请输入密码" name="password" tabindex="2" auto-complete="on" @keyup.enter.native="handleLogin" />
<span class="show-pwd" @click="showPwd">
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
</span>
</div>
</el-form-item>
<!-- <el-form-item prop="code">-->
<!-- <div class="form-item">-->
<!-- <span class="svg-container">-->
<!-- <svg-icon icon-class="message" />-->
<!-- </span>-->
<!-- <el-input ref="username" v-model="loginForm.code" placeholder="请输入验证码" name="username" type="text"-->
<!-- tabindex="1" auto-complete="on" />-->
<!-- <el-button class="msg-btn" :loading="msgLoading" type="primary" size="small" :disabled="isVer" @click="sendSms">{{ isVer ? (''+verTime+'') : '' }}</el-button>-->
<!-- </div>-->
<!-- </el-form-item>-->
<el-button :loading="loading" type="primary" class="login-btn"
@click.native.prevent="handleLogin">登录</el-button>
</el-form>
<footer class="copyright">
版权所有{{ copyright }}
</footer>
</div>
</template>
<script>
import {
validUsername
} from '@/utils/validate'
import { login, getInfo } from "@/api/user";
const defaultSettings = require('../../../src/settings.js')
export default {
name: 'Login',
data() {
const validateUsername = (rule, value, callback) => {
if (!validUsername(value)) {
callback(new Error('请正确输入登录名'))
} else {
callback()
}
}
const validatePassword = (rule, value, callback) => {
if (value.length < 6) {
callback(new Error('密码输入错误'))
} else {
callback()
}
}
return {
title: "",
loginForm: {
username: '',
password: '',
//code: ''
},
loginRules: {
username: [{
required: true,
trigger: 'blur',
validator: validateUsername
}],
password: [{
required: true,
trigger: 'blur',
validator: validatePassword
}],
// code: [
// {
// required: true,
// trigger: 'blur',
// message: '',
// validator: (rule, value, callback) => {
// if (value.length !== 4) {
// callback(new Error(''))
// } else {
// callback()
// }
// }
// }
// ]
},
msgLoading: false,
loading: false,
passwordType: 'password',
redirect: undefined,
verTime: 60,
isVer: false,
temp: {
mobile: "",
token: ""
}
}
},
watch: {
$route: {
handler: function(route) {
this.redirect = route.query && route.query.redirect
},
immediate: true
}
},
created() {
this.title = defaultSettings.title;
this.copyright = defaultSettings.copyright;
},
methods: {
async sendSms () {
if (this.isVer) return;
if (this.loginForm.username) {
this.msgLoading = true;
try {
await sendSms({
username: this.loginForm.username
})
this.isVer = true;
this.verTime = 60;
this.$message({
message: '验证码已发送',
type:'success'
})
let timer = setInterval(() => {
this.verTime--;
if (this.verTime <= 0) {
this.isVer = false;
clearInterval(timer);
}
}, 1000);
this.msgLoading = false;
} catch (e) {
this.msgLoading = false;
}
} else {
this.$message({
message: '请输入登录名',
type: 'warning'
})
}
},
showPwd() {
if (this.passwordType === 'password') {
this.passwordType = ''
} else {
this.passwordType = 'password'
}
this.$nextTick(() => {
this.$refs.password.focus()
})
},
//
async handleLogin() {
this.$refs.loginForm.validate(async(valid) => {
if (valid) {
this.loading = true
this.$store.dispatch('user/login', this.loginForm).then(() => {
this.$router.push({
path: this.redirect || '/'
})
this.loading = false
}).catch(() => {
this.loading = false
})
} else {
console.log('error submit!!')
return false
}
})
}
}
}
</script>
<style lang="scss">
#particles-js {
width: 100%;
height: 99%;
position: absolute;
}
/* 修复input 背景不协调 和光标变色 */
/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
$bg:#122583;
$light_gray:#122583;
$cursor: #122583;
@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
.login-container .el-input input {
color: $cursor;
}
}
/* reset element-ui css */
.login-container {
.el-input {
flex: 1;
display: inline-block;
input {
background: transparent;
border: 0px;
-webkit-appearance: none;
border-radius: 0px;
padding: 4px 5px 4px 15px;
color: $light_gray;
caret-color: $cursor;
&:-webkit-autofill {
//box-shadow: 0 0 0px 1000px $bg inset !important;
//-webkit-text-fill-color: $cursor !important;
}
}
}
}
</style>
<style lang="scss" scoped>
$bg:#122583;
$dark_gray:#122583;
$light_gray:#122583;
.build-img {
position: absolute;
width: 36.5vw;
object-fit: contain;
top: 50%;
transform: translateY(-50%);
left: 10.2%;
}
.title-img {
display: block;
width: 44.64vw;
object-fit: contain;
margin: 16.11vh auto 0;
}
.form-item {
height: 3vw;
width: 86.4%;
margin: auto;
border-radius: 8px;
background-color: #f4f4f4;
filter: drop-shadow(0 0 1px #cdcdcd);
display: flex;
align-items: center;
}
.login-container {
height: 100%;
width: 100%;
background: url("~@/assets/login/bkg.png") no-repeat;
background-size: cover;
overflow: hidden;
.login-form {
position: relative;
width: 28.7vw;
margin: 50vh 10.2vw 0 auto;
transform: translateY(-50%);
overflow: hidden;
border-radius: 10px;
box-shadow: inset 0 0 4px rgb(255,255,255,0.8);
filter: drop-shadow(0 0 7px rgba(0,0,0,0.2));
background-color: rgba(255,255,255,0.2);
}
.tips {
font-size: 14px;
color: #fff;
margin-bottom: 10px;
span {
&:first-of-type {
margin-right: 16px;
}
}
}
.svg-container {
padding: 6px 5px 6px 15px;
color: $dark_gray;
vertical-align: middle;
width: 30px;
display: inline-block;
}
.title-container {
position: relative;
margin-bottom: 2vw;
.title {
text-align: center;
font-size: 1.7vw;
letter-spacing: 4px;
color: #ffffff;
padding: 1.41vw 0 1.2vw;
position: relative;
&::after {
content: "";
width: 100%;
height: 1px;
background: linear-gradient(to right, #fff0 20%, #fff, #fff0 80%);
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
}
}
.show-pwd {
position: absolute;
right: 8%;
top: 50%;
transform: translate(0, -50%);
font-size: 16px;
color: $dark_gray;
cursor: pointer;
user-select: none;
}
.msg-btn {
border-radius: 0 8px 8px 0;
height: 100%;
background-image: linear-gradient(90deg, #1d57da 0%, #1d57da 0%, #4185c5 100%);
}
.login-btn {
width: 86.4%;
height: 3vw;
border-radius: 8px;
padding: 0;
background-image: linear-gradient(90deg, #1d57da 0%, #1d57da 0%, #05a658 100%);
display: block;
margin: auto auto 3.2vw;
font-size: 1.21vw;
letter-spacing: 3px;
color: #ffffff;
}
}
::v-deep .el-form-item--small.el-form-item {
margin-bottom: 1.5vw;
}
.copyright {
width: 100%;
text-align: center;
color: #fff;
line-height: 16px;
font-size: 16px;
position: absolute;
left: 0;
bottom: 6vh;
}
::v-deep .el-form-item__error {
left: 6.8%;
}
@media (max-width: 960px) {
.title-img {
display: none;
}
.login-container .login-form {
max-width: 420px;
width: 78%;
margin: auto;
position: relative;
top: 50%;
transform: translateY(-50%);
}
.login-container .title-container .title {
font-size: 15px;
}
.form-item {
height: 40px;
margin-bottom: 12px;
}
.login-container .login-btn {
height: 40px;
font-size: 14px;
}
::v-deep .el-form-item__error {
bottom: 0;
top: auto;
}
}
</style>

@ -0,0 +1,33 @@
<template>
<div>
<vxe-table
:align="allAlign"
:data="tableData">
<vxe-column type="seq" width="60"></vxe-column>
<vxe-column field="name" title="Name"></vxe-column>
<vxe-column field="sex" title="Sex"></vxe-column>
<vxe-column field="age" title="Age"></vxe-column>
</vxe-table>
</div>
</template>
<script>
export default {
data() {
return {
allAlign: null,
tableData: [
{ id: 10001, name: 'Test1', role: 'Develop', sex: 'Man', age: 28, address: 'test abc' },
{ id: 10002, name: 'Test2', role: 'Test', sex: 'Women', age: 22, address: 'Guangzhou' },
{ id: 10003, name: 'Test3', role: 'PM', sex: 'Man', age: 32, address: 'Shanghai' },
{ id: 10004, name: 'Test4', role: 'Designer', sex: 'Women', age: 24, address: 'Shanghai' }
]
}
},
methods: {},
computed: {}
}
</script>
<style scoped lang="scss">
</style>

@ -0,0 +1,167 @@
<template>
<div>
<div>
<el-button icon="el-icon-plus" type="primary" size="small" @click="add"></el-button>
</div>
<vxe-table
stripe
style="margin-top: 10px;"
ref="table"
:loading="loading"
keep-source
show-overflow
:edit-rules="validRules"
:edit-config="{ trigger: 'manual', mode: 'row', showStatus: true }"
:align="allAlign"
:data="tableData">
<vxe-column type="seq" width="58" align="center"></vxe-column>
<vxe-column field="name" width="160" title="角色" :edit-render="{ name: 'input', attrs: { type: 'text'} }"></vxe-column>
<vxe-column field="sex" title="权限菜单" show-overflow="tooltip">
<template #default="{ row }">
<div>
<el-tag v-for="item in row.permissions" :key="item.id">{{ item.name }}</el-tag>
</div>
</template>
</vxe-column>
<vxe-column field="sortnumber" width="80" title="排序" align="center" :edit-render="{ name: 'input', attrs: { type: 'number' } }"></vxe-column>
<vxe-column field="operate" title="操作">
<template #default="{ row }">
<template v-if="isActiveStatus(row)">
<el-button size="small" type="primary" @click="saveRowEvent(row)"></el-button>
<el-button size="small" type="primary" plain @click="cancelRowEvent(row)"></el-button>
</template>
<template v-else>
<el-button size="small" type="warning" @click="editRowEvent(row)"></el-button>
</template>
<el-button size="small" type="danger" @click="destroyRowEvent(row)"></el-button>
</template>
</vxe-column>
</vxe-table>
<p class="total" type="primary"> {{ total }} 条数据</p>
</div>
</template>
<script>
import { deepCopy } from "@/utils"
import { index, save, destroy } from "@/api/role"
export default {
data() {
return {
loading: false,
total: 0,
allAlign: null,
tableData: [],
validRules: {
},
form: {
id: "",
name: "",
sortnumber: 0
}
}
},
methods: {
editRowEvent (row) {
if (this.$refs['table']) {
this.$refs['table'].setEditRow(row)
}
},
cancelRowEvent (row) {
if (this.$refs['table']) {
this.$refs['table'].clearEdit().then(() => {
//
this.$refs['table'].revertData(row)
})
}
},
async getList () {
this.loading = true;
try {
const res = await index()
this.tableData = res.rows;
this.total = res.total;
console.log(res)
this.loading = false;
} catch (err) {
console.error(err)
this.loading = false;
}
},
add () {
this.tableData.unshift(deepCopy(this.form))
if (this.$refs['table']) {
this.$nextTick(() => {
this.$refs['table'].setEditRow(this.$refs['table'].tableData[0])
})
}
},
async saveRowEvent (row) {
try {
await this.$confirm("确认保存?","提示",{
confirmButtonText: "确认",
cancelButtonText: "取消"
})
let form = deepCopy(this.form)
for (let key in form) {
form[key] = row[key]
}
this.loading = true;
await save(form)
await this.getList();
this.loading = false;
} catch (err) {
this.loading = false;
}
},
async destroyRowEvent (row) {
try {
await this.$confirm("确认删除?","提示",{
confirmButtonText: "确认",
cancelButtonText: "取消"
})
this.loading = true;
if (row.id) {
await destroy({
id: row.id
})
await this.getList();
} else {
console.log(row)
this.tableData.splice(this.tableData.findIndex(i => i._X_ROW_KEY === row._X_ROW_KEY),1)
}
this.loading = false;
} catch (err) {
this.loading = false;
}
},
},
computed: {
isActiveStatus () {
return function (row) {
if (this.$refs['table']) {
return this.$refs['table'].isEditByRow(row)
}
}
}
},
created() {
this.getList()
}
}
</script>
<style scoped lang="scss">
.total {
color: #666;
text-align: right;
line-height: 3;
}
::v-deep .el-tag + .el-tag {
margin-left: 4px;
}
</style>

@ -0,0 +1,17 @@
<template>
<div>
</div>
</template>
<script>
export default {
data() {
return {}
},
methods: {},
computed: {}
}
</script>
<style scoped lang="scss">
</style>

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

@ -0,0 +1,98 @@
import { mount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
import ElementUI from 'element-ui'
import Breadcrumb from '@/components/Breadcrumb/index.vue'
const localVue = createLocalVue()
localVue.use(VueRouter)
localVue.use(ElementUI)
const routes = [
{
path: '/',
name: 'home',
children: [{
path: 'dashboard',
name: 'dashboard'
}]
},
{
path: '/menu',
name: 'menu',
children: [{
path: 'menu1',
name: 'menu1',
meta: { title: 'menu1' },
children: [{
path: 'menu1-1',
name: 'menu1-1',
meta: { title: 'menu1-1' }
},
{
path: 'menu1-2',
name: 'menu1-2',
redirect: 'noredirect',
meta: { title: 'menu1-2' },
children: [{
path: 'menu1-2-1',
name: 'menu1-2-1',
meta: { title: 'menu1-2-1' }
},
{
path: 'menu1-2-2',
name: 'menu1-2-2'
}]
}]
}]
}]
const router = new VueRouter({
routes
})
describe('Breadcrumb.vue', () => {
const wrapper = mount(Breadcrumb, {
localVue,
router
})
it('dashboard', () => {
router.push('/dashboard')
const len = wrapper.findAll('.el-breadcrumb__inner').length
expect(len).toBe(1)
})
it('normal route', () => {
router.push('/menu/menu1')
const len = wrapper.findAll('.el-breadcrumb__inner').length
expect(len).toBe(2)
})
it('nested route', () => {
router.push('/menu/menu1/menu1-2/menu1-2-1')
const len = wrapper.findAll('.el-breadcrumb__inner').length
expect(len).toBe(4)
})
it('no meta.title', () => {
router.push('/menu/menu1/menu1-2/menu1-2-2')
const len = wrapper.findAll('.el-breadcrumb__inner').length
expect(len).toBe(3)
})
// it('click link', () => {
// router.push('/menu/menu1/menu1-2/menu1-2-2')
// const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
// const second = breadcrumbArray.at(1)
// console.log(breadcrumbArray)
// const href = second.find('a').attributes().href
// expect(href).toBe('#/menu/menu1')
// })
// it('noRedirect', () => {
// router.push('/menu/menu1/menu1-2/menu1-2-1')
// const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
// const redirectBreadcrumb = breadcrumbArray.at(2)
// expect(redirectBreadcrumb.contains('a')).toBe(false)
// })
it('last breadcrumb', () => {
router.push('/menu/menu1/menu1-2/menu1-2-1')
const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
const redirectBreadcrumb = breadcrumbArray.at(3)
expect(redirectBreadcrumb.contains('a')).toBe(false)
})
})

@ -0,0 +1,18 @@
import { shallowMount } from '@vue/test-utils'
import Hamburger from '@/components/Hamburger/index.vue'
describe('Hamburger.vue', () => {
it('toggle click', () => {
const wrapper = shallowMount(Hamburger)
const mockFn = jest.fn()
wrapper.vm.$on('toggleClick', mockFn)
wrapper.find('.hamburger').trigger('click')
expect(mockFn).toBeCalled()
})
it('prop isActive', () => {
const wrapper = shallowMount(Hamburger)
wrapper.setProps({ isActive: true })
expect(wrapper.contains('.is-active')).toBe(true)
wrapper.setProps({ isActive: false })
expect(wrapper.contains('.is-active')).toBe(false)
})
})

@ -0,0 +1,22 @@
import { shallowMount } from '@vue/test-utils'
import SvgIcon from '@/components/SvgIcon/index.vue'
describe('SvgIcon.vue', () => {
it('iconClass', () => {
const wrapper = shallowMount(SvgIcon, {
propsData: {
iconClass: 'test'
}
})
expect(wrapper.find('use').attributes().href).toBe('#icon-test')
})
it('className', () => {
const wrapper = shallowMount(SvgIcon, {
propsData: {
iconClass: 'test'
}
})
expect(wrapper.classes().length).toBe(1)
wrapper.setProps({ className: 'test' })
expect(wrapper.classes().includes('test')).toBe(true)
})
})

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

Loading…
Cancel
Save