Merge branch 'v4.0' of github.com:PanJiaChen/vue-element-admin into v4.0

# Conflicts:
#	src/store/index.js
#	src/store/modules/app.js
This commit is contained in:
Estelle00 2019-03-22 16:21:48 +08:00
commit b63c23d624
187 changed files with 4239 additions and 1904 deletions

View File

@ -1,4 +1,5 @@
VUE_APP_BASE_API = '/api' VUE_APP_BASE_API = '/api'
ENV = 'development'
// With this configuration, vue-cli uses babel-plugin-dynamic-import-node // With this configuration, vue-cli uses babel-plugin-dynamic-import-node
// It only does one thing by converting all import() to require() // It only does one thing by converting all import() to require()

View File

@ -1 +1,2 @@
VUE_APP_BASE_API = '/api' VUE_APP_BASE_API = '/api'
ENV = 'production'

3
.env.staging Normal file
View File

@ -0,0 +1,3 @@
NODE_ENV=production
VUE_APP_BASE_API = '/api'
ENV = 'staging'

View File

@ -21,7 +21,10 @@ module.exports = {
"allowFirstLine": false "allowFirstLine": false
} }
}], }],
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline":"off",
"vue/name-property-casing": ["error", "PascalCase"], "vue/name-property-casing": ["error", "PascalCase"],
"vue/no-v-html": "off",
'accessor-pairs': 2, 'accessor-pairs': 2,
'arrow-spacing': [2, { 'arrow-spacing': [2, {
'before': true, 'before': true,

View File

@ -8,14 +8,14 @@ if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
run(`vue-cli-service build ${args}`) run(`vue-cli-service build ${args}`)
const port = 9526 const port = 9526
const basePath = config.baseUrl const publicPath = config.publicPath
var connect = require('connect') var connect = require('connect')
var serveStatic = require('serve-static') var serveStatic = require('serve-static')
const app = connect() const app = connect()
app.use( app.use(
basePath, publicPath,
serveStatic('./dist', { serveStatic('./dist', {
index: ['index.html', '/'] index: ['index.html', '/']
}) })
@ -23,7 +23,7 @@ if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
app.listen(port, function() { app.listen(port, function() {
console.log( console.log(
chalk.green(`> Listening at http://localhost:${port}${basePath}`) chalk.green(`> Listening at http://localhost:${port}${publicPath}`)
) )
}) })
} else { } else {

View File

@ -1,9 +1,9 @@
module.exports = { module.exports = {
verbose: true, verbose: true,
moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
transformIgnorePatterns: [ // transformIgnorePatterns: [
'node_modules/(?!(babel-jest|jest-vue-preprocessor)/)' // 'node_modules/(?!(babel-jest|jest-vue-preprocessor)/)'
], // ],
transform: { transform: {
'^.+\\.vue$': 'vue-jest', '^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',

View File

@ -13,7 +13,7 @@ for (let i = 0; i < count; i++) {
author: '@first', author: '@first',
reviewer: '@first', reviewer: '@first',
title: '@title(5, 10)', title: '@title(5, 10)',
content_short: '我是测试数据', content_short: 'mock data',
content: baseContent, content: baseContent,
forecast: '@float(0, 100, 2, 2)', forecast: '@float(0, 100, 2, 2)',
importance: '@integer(1, 3)', importance: '@integer(1, 3)',
@ -27,48 +27,90 @@ for (let i = 0; i < count; i++) {
})) }))
} }
export default { export default [
'/article/list': config => { {
const { importance, type, title, page = 1, limit = 20, sort } = config.query url: '/article/list',
type: 'get',
response: config => {
const { importance, type, title, page = 1, limit = 20, sort } = config.query
let mockList = List.filter(item => { let mockList = List.filter(item => {
if (importance && item.importance !== +importance) return false if (importance && item.importance !== +importance) return false
if (type && item.type !== type) return false if (type && item.type !== type) return false
if (title && item.title.indexOf(title) < 0) return false if (title && item.title.indexOf(title) < 0) return false
return true return true
}) })
if (sort === '-id') { if (sort === '-id') {
mockList = mockList.reverse() mockList = mockList.reverse()
} }
const pageList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1)) const pageList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1))
return { return {
total: mockList.length, code: 20000,
items: pageList data: {
} total: mockList.length,
}, items: pageList
'/article/detail': config => { }
const { id } = config.query
for (const article of List) {
if (article.id === +id) {
return article
} }
} }
}, },
'/article/pv': {
pvData: [ {
{ key: 'PC', pv: 1024 }, url: '/article/detail',
{ key: 'mobile', pv: 1024 }, type: 'get',
{ key: 'ios', pv: 1024 }, response: config => {
{ key: 'android', pv: 1024 } const { id } = config.query
] for (const article of List) {
if (article.id === +id) {
return {
code: 20000,
data: article
}
}
}
}
}, },
'/article/create': {
data: 'success' {
url: '/article/pv',
type: 'get',
response: _ => {
return {
code: 20000,
data: {
pvData: [
{ key: 'PC', pv: 1024 },
{ key: 'mobile', pv: 1024 },
{ key: 'ios', pv: 1024 },
{ key: 'android', pv: 1024 }
]
}
}
}
}, },
'/article/update': {
data: 'success' {
url: '/article/create',
type: 'post',
response: _ => {
return {
code: 20000,
data: 'success'
}
}
},
{
url: '/article/update',
type: 'post',
response: _ => {
return {
code: 20000,
data: 'success'
}
}
} }
} ]

View File

@ -33,18 +33,21 @@ export function mockXHR() {
} }
} }
for (const [route, respond] of Object.entries(mocks)) { for (const i of mocks) {
Mock.mock(new RegExp(`${route}`), XHR2ExpressReqWrap(respond)) Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
} }
} }
const responseFake = (route, respond) => ( const responseFake = (url, type, respond) => {
{ return {
route: new RegExp(`${MOCK_API_BASE}${route}`), url: new RegExp(`${MOCK_API_BASE}${url}`),
type: type || 'get',
response(req, res) { response(req, res) {
res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
} }
} }
) }
export default Object.keys(mocks).map(route => responseFake(route, mocks[route])) export default mocks.map(route => {
return responseFake(route.url, route.type, route.response)
})

View File

@ -1,33 +0,0 @@
const userMap = {
admin: {
roles: ['admin'],
token: 'admin',
introduction: '我是超级管理员',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Super Admin'
},
editor: {
roles: ['editor'],
token: 'editor',
introduction: '我是编辑',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Normal Editor'
}
}
export default {
'/login/login': config => {
const { username } = config.body
return userMap[username]
},
'/login/logout': 'success',
'/user/info': config => {
const { token } = config.query
if (userMap[token]) {
return userMap[token]
} else {
return false
}
}
}

View File

@ -1,12 +1,12 @@
import login from './login' import user from './user'
import role from './role'
import article from './article' import article from './article'
import search from './remoteSearch' import search from './remoteSearch'
import transaction from './transaction'
export default { export default [
...login, ...user,
...role,
...article, ...article,
...search, ...search
...transaction ]
}

View File

@ -8,15 +8,44 @@ for (let i = 0; i < count; i++) {
name: '@first' name: '@first'
})) }))
} }
NameList.push({ name: 'mockPan' }) NameList.push({ name: 'mock-Pan' })
export default { export default [
'/search/user': config => { // username search
const { name } = config.query {
const mockNameList = NameList.filter(item => { url: '/search/user',
const lowerCaseName = item.name.toLowerCase() type: 'get',
return !(name && lowerCaseName.indexOf(name.toLowerCase()) < 0) response: config => {
}) const { name } = config.query
return { items: mockNameList } const mockNameList = NameList.filter(item => {
const lowerCaseName = item.name.toLowerCase()
return !(name && lowerCaseName.indexOf(name.toLowerCase()) < 0)
})
return {
code: 20000,
data: { items: mockNameList }
}
}
},
// transaction list
{
url: '/transaction/list',
type: 'get',
response: _ => {
return {
code: 20000,
data: {
total: 20,
'items|20': [{
order_no: '@guid()',
timestamp: +Mock.Random.date('T'),
username: '@name()',
price: '@float(1000, 15000, 0, 2)',
'status|1': ['success', 'pending']
}]
}
}
}
} }
} ]

98
mock/role/index.js Normal file
View File

@ -0,0 +1,98 @@
import Mock from 'mockjs'
import { deepClone } from '../../src/utils/index.js'
import { asyncRoutes, constantRoutes } from './routes.js'
const routes = deepClone([...constantRoutes, ...asyncRoutes])
const roles = [
{
key: 'admin',
name: 'admin',
description: 'Super Administrator. Have access to view all pages.',
routes: routes
},
{
key: 'editor',
name: 'editor',
description: 'Normal Editor. Can see all pages except permission page',
routes: routes.filter(i => i.path !== '/permission')// just a mock
},
{
key: 'visitor',
name: 'visitor',
description: 'Just a visitor. Can only see the home page and the document page',
routes: [{
path: '',
redirect: 'dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
meta: { title: 'dashboard', icon: 'dashboard' }
}
]
}]
}
]
export default [
// mock get all routes form server
{
url: '/routes',
type: 'get',
response: _ => {
return {
code: 20000,
data: routes
}
}
},
// mock get all roles form server
{
url: '/roles',
type: 'get',
response: _ => {
return {
code: 20000,
data: roles
}
}
},
// add role
{
url: '/role',
type: 'post',
response: {
code: 20000,
data: {
key: Mock.mock('@integer(300, 5000)')
}
}
},
// update role
{
url: '/role/[A-Za-z0-9]',
type: 'put',
response: {
code: 20000,
data: {
status: 'success'
}
}
},
// delete role
{
url: '/role/[A-Za-z0-9]',
type: 'delete',
response: {
code: 20000,
data: {
status: 'success'
}
}
}
]

525
mock/role/routes.js Normal file
View File

@ -0,0 +1,525 @@
// Just a mock data
export const constantRoutes = [
{
path: '/redirect',
component: 'layout/Layout',
hidden: true,
children: [
{
path: '/redirect/:path*',
component: 'views/redirect/index'
}
]
},
{
path: '/login',
component: 'views/login/index',
hidden: true
},
{
path: '/auth-redirect',
component: 'views/login/authredirect',
hidden: true
},
{
path: '/404',
component: 'views/errorPage/404',
hidden: true
},
{
path: '/401',
component: 'views/errorPage/401',
hidden: true
},
{
path: '',
component: 'layout/Layout',
redirect: 'dashboard',
children: [
{
path: 'dashboard',
component: 'views/dashboard/index',
name: 'Dashboard',
meta: { title: 'dashboard', icon: 'dashboard', noCache: true, affix: true }
}
]
},
{
path: '/documentation',
component: 'layout/Layout',
children: [
{
path: 'index',
component: 'views/documentation/index',
name: 'Documentation',
meta: { title: 'documentation', icon: 'documentation', affix: true }
}
]
},
{
path: '/guide',
component: 'layout/Layout',
redirect: '/guide/index',
children: [
{
path: 'index',
component: 'views/guide/index',
name: 'Guide',
meta: { title: 'guide', icon: 'guide', noCache: true }
}
]
}
]
export const asyncRoutes = [
{
path: '/permission',
component: 'layout/Layout',
redirect: '/permission/index',
alwaysShow: true,
meta: {
title: 'permission',
icon: 'lock',
roles: ['admin', 'editor']
},
children: [
{
path: 'page',
component: 'views/permission/page',
name: 'PagePermission',
meta: {
title: 'pagePermission',
roles: ['admin']
}
},
{
path: 'directive',
component: 'views/permission/directive',
name: 'DirectivePermission',
meta: {
title: 'directivePermission'
}
},
{
path: 'role',
component: 'views/permission/role',
name: 'RolePermission',
meta: {
title: 'rolePermission',
roles: ['admin']
}
}
]
},
{
path: '/icon',
component: 'layout/Layout',
children: [
{
path: 'index',
component: 'views/svg-icons/index',
name: 'Icons',
meta: { title: 'icons', icon: 'icon', noCache: true }
}
]
},
{
path: '/components',
component: 'layout/Layout',
redirect: 'noredirect',
name: 'ComponentDemo',
meta: {
title: 'components',
icon: 'component'
},
children: [
{
path: 'tinymce',
component: 'views/components-demo/tinymce',
name: 'TinymceDemo',
meta: { title: 'tinymce' }
},
{
path: 'markdown',
component: 'views/components-demo/markdown',
name: 'MarkdownDemo',
meta: { title: 'markdown' }
},
{
path: 'json-editor',
component: 'views/components-demo/jsonEditor',
name: 'JsonEditorDemo',
meta: { title: 'jsonEditor' }
},
{
path: 'splitpane',
component: 'views/components-demo/splitpane',
name: 'SplitpaneDemo',
meta: { title: 'splitPane' }
},
{
path: 'avatar-upload',
component: 'views/components-demo/avatarUpload',
name: 'AvatarUploadDemo',
meta: { title: 'avatarUpload' }
},
{
path: 'dropzone',
component: 'views/components-demo/dropzone',
name: 'DropzoneDemo',
meta: { title: 'dropzone' }
},
{
path: 'sticky',
component: 'views/components-demo/sticky',
name: 'StickyDemo',
meta: { title: 'sticky' }
},
{
path: 'count-to',
component: 'views/components-demo/countTo',
name: 'CountToDemo',
meta: { title: 'countTo' }
},
{
path: 'mixin',
component: 'views/components-demo/mixin',
name: 'ComponentMixinDemo',
meta: { title: 'componentMixin' }
},
{
path: 'back-to-top',
component: 'views/components-demo/backToTop',
name: 'BackToTopDemo',
meta: { title: 'backToTop' }
},
{
path: 'drag-dialog',
component: 'views/components-demo/dragDialog',
name: 'DragDialogDemo',
meta: { title: 'dragDialog' }
},
{
path: 'drag-select',
component: 'views/components-demo/dragSelect',
name: 'DragSelectDemo',
meta: { title: 'dragSelect' }
},
{
path: 'dnd-list',
component: 'views/components-demo/dndList',
name: 'DndListDemo',
meta: { title: 'dndList' }
},
{
path: 'drag-kanban',
component: 'views/components-demo/dragKanban',
name: 'DragKanbanDemo',
meta: { title: 'dragKanban' }
}
]
},
{
path: '/charts',
component: 'layout/Layout',
redirect: 'noredirect',
name: 'Charts',
meta: {
title: 'charts',
icon: 'chart'
},
children: [
{
path: 'keyboard',
component: 'views/charts/keyboard',
name: 'KeyboardChart',
meta: { title: 'keyboardChart', noCache: true }
},
{
path: 'line',
component: 'views/charts/line',
name: 'LineChart',
meta: { title: 'lineChart', noCache: true }
},
{
path: 'mixchart',
component: 'views/charts/mixChart',
name: 'MixChart',
meta: { title: 'mixChart', noCache: true }
}
]
},
{
path: '/nested',
component: 'layout/Layout',
redirect: '/nested/menu1/menu1-1',
name: 'Nested',
meta: {
title: 'nested',
icon: 'nested'
},
children: [
{
path: 'menu1',
component: 'views/nested/menu1/index',
name: 'Menu1',
meta: { title: 'menu1' },
redirect: '/nested/menu1/menu1-1',
children: [
{
path: 'menu1-1',
component: 'views/nested/menu1/menu1-1',
name: 'Menu1-1',
meta: { title: 'menu1-1' }
},
{
path: 'menu1-2',
component: 'views/nested/menu1/menu1-2',
name: 'Menu1-2',
redirect: '/nested/menu1/menu1-2/menu1-2-1',
meta: { title: 'menu1-2' },
children: [
{
path: 'menu1-2-1',
component: 'views/nested/menu1/menu1-2/menu1-2-1',
name: 'Menu1-2-1',
meta: { title: 'menu1-2-1' }
},
{
path: 'menu1-2-2',
component: 'views/nested/menu1/menu1-2/menu1-2-2',
name: 'Menu1-2-2',
meta: { title: 'menu1-2-2' }
}
]
},
{
path: 'menu1-3',
component: 'views/nested/menu1/menu1-3',
name: 'Menu1-3',
meta: { title: 'menu1-3' }
}
]
},
{
path: 'menu2',
name: 'Menu2',
component: 'views/nested/menu2/index',
meta: { title: 'menu2' }
}
]
},
{
path: '/example',
component: 'layout/Layout',
redirect: '/example/list',
name: 'Example',
meta: {
title: 'example',
icon: 'example'
},
children: [
{
path: 'create',
component: 'views/example/create',
name: 'CreateArticle',
meta: { title: 'createArticle', icon: 'edit' }
},
{
path: 'edit/:id(\\d+)',
component: 'views/example/edit',
name: 'EditArticle',
meta: { title: 'editArticle', noCache: true },
hidden: true
},
{
path: 'list',
component: 'views/example/list',
name: 'ArticleList',
meta: { title: 'articleList', icon: 'list' }
}
]
},
{
path: '/tab',
component: 'layout/Layout',
children: [
{
path: 'index',
component: 'views/tab/index',
name: 'Tab',
meta: { title: 'tab', icon: 'tab' }
}
]
},
{
path: '/error',
component: 'layout/Layout',
redirect: 'noredirect',
name: 'ErrorPages',
meta: {
title: 'errorPages',
icon: '404'
},
children: [
{
path: '401',
component: 'views/errorPage/401',
name: 'Page401',
meta: { title: 'page401', noCache: true }
},
{
path: '404',
component: 'views/errorPage/404',
name: 'Page404',
meta: { title: 'page404', noCache: true }
}
]
},
{
path: '/error-log',
component: 'layout/Layout',
redirect: 'noredirect',
children: [
{
path: 'log',
component: 'views/errorLog/index',
name: 'ErrorLog',
meta: { title: 'errorLog', icon: 'bug' }
}
]
},
{
path: '/excel',
component: 'layout/Layout',
redirect: '/excel/export-excel',
name: 'Excel',
meta: {
title: 'excel',
icon: 'excel'
},
children: [
{
path: 'export-excel',
component: 'views/excel/exportExcel',
name: 'ExportExcel',
meta: { title: 'exportExcel' }
},
{
path: 'export-selected-excel',
component: 'views/excel/selectExcel',
name: 'SelectExcel',
meta: { title: 'selectExcel' }
},
{
path: 'export-merge-header',
component: 'views/excel/mergeHeader',
name: 'MergeHeader',
meta: { title: 'mergeHeader' }
},
{
path: 'upload-excel',
component: 'views/excel/uploadExcel',
name: 'UploadExcel',
meta: { title: 'uploadExcel' }
}
]
},
{
path: '/zip',
component: 'layout/Layout',
redirect: '/zip/download',
alwaysShow: true,
meta: { title: 'zip', icon: 'zip' },
children: [
{
path: 'download',
component: 'views/zip/index',
name: 'ExportZip',
meta: { title: 'exportZip' }
}
]
},
{
path: '/pdf',
component: 'layout/Layout',
redirect: '/pdf/index',
children: [
{
path: 'index',
component: 'views/pdf/index',
name: 'PDF',
meta: { title: 'pdf', icon: 'pdf' }
}
]
},
{
path: '/pdf/download',
component: 'views/pdf/download',
hidden: true
},
{
path: '/theme',
component: 'layout/Layout',
redirect: 'noredirect',
children: [
{
path: 'index',
component: 'views/theme/index',
name: 'Theme',
meta: { title: 'theme', icon: 'theme' }
}
]
},
{
path: '/clipboard',
component: 'layout/Layout',
redirect: 'noredirect',
children: [
{
path: 'index',
component: 'views/clipboard/index',
name: 'ClipboardDemo',
meta: { title: 'clipboardDemo', icon: 'clipboard' }
}
]
},
{
path: '/i18n',
component: 'layout/Layout',
children: [
{
path: 'index',
component: 'views/i18n-demo/index',
name: 'I18n',
meta: { title: 'i18n', icon: 'international' }
}
]
},
{
path: 'external-link',
component: 'layout/Layout',
children: [
{
path: 'https://github.com/PanJiaChen/vue-element-admin',
meta: { title: 'externalLink', icon: 'link' }
}
]
},
{ path: '*', redirect: '/404', hidden: true }
]

View File

@ -1,16 +0,0 @@
import Mock from 'mockjs'
const count = 20
export default {
'/transaction/list': {
total: count,
[`items|${count}`]: [{
order_no: '@guid()',
timestamp: +Mock.Random.date('T'),
username: '@name()',
price: '@float(1000, 15000, 0, 2)',
'status|1': ['success', 'pending']
}]
}
}

64
mock/user.js Normal file
View File

@ -0,0 +1,64 @@
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'
}
}
export default [
// user login
{
url: '/user/login',
type: 'post',
response: config => {
const { username } = config.body
return {
code: 20000,
data: tokens[username]
}
}
},
// get user info
{
url: '/user/info\.*',
type: 'get',
response: config => {
const { token } = config.query
return {
code: 20000,
data: users[token]
}
}
},
// user logout
{
url: '/user/logout',
type: 'post',
response: _ => {
return {
code: 20000,
data: 'success'
}
}
}
]

View File

@ -7,15 +7,19 @@
"scripts": { "scripts": {
"dev": "vue-cli-service serve", "dev": "vue-cli-service serve",
"build:prod": "vue-cli-service build", "build:prod": "vue-cli-service build",
"build:sit": "vue-cli-service build --mode text", "build:stage": "vue-cli-service build --mode staging",
"build:preview": "node build/index.js --preview", "build:preview": "node build/index.js --preview",
"build:report": "node build/index.js --report", "build:report": "node build/index.js --report",
"lint": "eslint --ext .js,.vue src", "lint": "eslint --ext .js,.vue src",
"test": "npm run lint", "test": "npm run lint",
"test:unit": "vue-cli-service test:unit", "test:unit": "vue-cli-service test:unit",
"precommit": "lint-staged",
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml" "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
}, },
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": { "lint-staged": {
"src/**/*.{js,vue}": [ "src/**/*.{js,vue}": [
"eslint --fix", "eslint --fix",
@ -39,56 +43,58 @@
"dependencies": { "dependencies": {
"axios": "0.18.0", "axios": "0.18.0",
"clipboard": "1.7.1", "clipboard": "1.7.1",
"codemirror": "5.42.0", "codemirror": "5.44.0",
"driver.js": "0.8.1", "driver.js": "0.9.5",
"dropzone": "5.5.1", "dropzone": "5.5.1",
"echarts": "4.1.0", "echarts": "4.1.0",
"element-ui": "2.4.10", "element-ui": "2.6.1",
"file-saver": "1.3.8", "file-saver": "2.0.1",
"fuse.js": "3.4.2", "fuse.js": "3.4.4",
"js-cookie": "2.2.0", "js-cookie": "2.2.0",
"jsonlint": "1.6.3", "jsonlint": "1.6.3",
"jszip": "3.1.5", "jszip": "3.2.0",
"normalize.css": "7.0.0", "normalize.css": "7.0.0",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"path-to-regexp": "2.4.0", "path-to-regexp": "2.4.0",
"screenfull": "4.0.0", "screenfull": "4.0.1",
"showdown": "1.8.6", "showdown": "1.9.0",
"sortablejs": "1.7.0", "sortablejs": "1.8.3",
"tui-editor": "1.2.7", "tui-editor": "1.3.2",
"vue": "2.5.17", "vue": "2.6.8",
"vue-count-to": "1.0.13", "vue-count-to": "1.0.13",
"vue-i18n": "7.3.2", "vue-i18n": "7.3.2",
"vue-router": "3.0.2", "vue-router": "3.0.2",
"vue-splitpane": "1.0.2", "vue-splitpane": "1.0.2",
"vuedraggable": "2.17.0", "vuedraggable": "2.17.0",
"vuex": "3.0.1", "vuex": "3.1.0",
"xlsx": "^0.11.16" "xlsx": "0.14.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.0.0", "@babel/core": "7.0.0",
"@babel/register": "7.0.0", "@babel/register": "7.0.0",
"@vue/cli-plugin-babel": "3.2.0", "@vue/cli-plugin-babel": "3.5.0",
"@vue/cli-plugin-eslint": "3.2.1", "@vue/cli-plugin-unit-jest": "3.5.0",
"@vue/cli-plugin-unit-jest": "3.2.0", "@vue/cli-service": "3.5.0",
"@vue/cli-service": "3.2.0", "@vue/test-utils": "1.0.0-beta.29",
"@vue/test-utils": "1.0.0-beta.25",
"babel-core": "7.0.0-bridge.0", "babel-core": "7.0.0-bridge.0",
"babel-eslint": "10.0.1",
"babel-jest": "23.6.0", "babel-jest": "23.6.0",
"chalk": "^2.4.1", "chalk": "2.4.2",
"connect": "^3.6.6", "connect": "3.6.6",
"husky": "0.14.3", "eslint": "5.15.1",
"eslint-plugin-vue": "5.2.2",
"husky": "1.3.1",
"lint-staged": "7.2.2", "lint-staged": "7.2.2",
"mockjs": "1.0.1-beta3", "mockjs": "1.0.1-beta3",
"node-sass": "^4.9.0", "node-sass": "^4.9.0",
"runjs": "^4.3.2", "runjs": "^4.3.2",
"sass-loader": "7.0.3", "sass-loader": "^7.1.0",
"script-ext-html-webpack-plugin": "2.1.3", "script-ext-html-webpack-plugin": "2.1.3",
"script-loader": "0.7.2", "script-loader": "0.7.2",
"serve-static": "^1.13.2", "serve-static": "^1.13.2",
"svg-sprite-loader": "4.1.3", "svg-sprite-loader": "4.1.3",
"svgo": "1.1.1", "svgo": "1.2.0",
"vue-template-compiler": "2.5.17" "vue-template-compiler": "2.6.8"
}, },
"engines": { "engines": {
"node": ">=8.9", "node": ">=8.9",

View File

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

View File

@ -7,3 +7,11 @@ export function userSearch(name) {
params: { name } params: { name }
}) })
} }
export function transactionList(query) {
return request({
url: '/transaction/list',
method: 'get',
params: query
})
}

38
src/api/role.js Normal file
View File

@ -0,0 +1,38 @@
import request from '@/utils/request'
export function getRoutes() {
return request({
url: '/routes',
method: 'get'
})
}
export function getRoles() {
return request({
url: '/roles',
method: 'get'
})
}
export function addRole(data) {
return request({
url: '/role',
method: 'post',
data
})
}
export function updateRole(id, data) {
return request({
url: `/role/${id}`,
method: 'put',
data
})
}
export function deleteRole(id) {
return request({
url: `/role/${id}`,
method: 'delete'
})
}

View File

@ -1,9 +0,0 @@
import request from '@/utils/request'
export function fetchList(query) {
return request({
url: '/transaction/list',
method: 'get',
params: query
})
}

View File

@ -1,25 +1,14 @@
import request from '@/utils/request' import request from '@/utils/request'
export function loginByUsername(username, password) { export function login(data) {
const data = {
username,
password
}
return request({ return request({
url: '/login/login', url: '/user/login',
method: 'post', method: 'post',
data data
}) })
} }
export function logout() { export function getInfo(token) {
return request({
url: '/login/logout',
method: 'post'
})
}
export function getUserInfo(token) {
return request({ return request({
url: '/user/info', url: '/user/info',
method: 'get', method: 'get',
@ -27,3 +16,10 @@ export function getUserInfo(token) {
}) })
} }
export function logout() {
return request({
url: '/user/logout',
method: 'post'
})
}

View File

@ -4,7 +4,7 @@
<svg width="16" height="16" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg" class="Icon Icon--backToTopArrow" aria-hidden="true" style="height: 16px; width: 16px;"> <svg width="16" height="16" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg" class="Icon Icon--backToTopArrow" aria-hidden="true" style="height: 16px; width: 16px;">
<title>回到顶部</title> <title>回到顶部</title>
<g> <g>
<path d="M12.036 15.59c0 .55-.453.995-.997.995H5.032c-.55 0-.997-.445-.997-.996V8.584H1.03c-1.1 0-1.36-.633-.578-1.416L7.33.29c.39-.39 1.026-.385 1.412 0l6.878 6.88c.782.78.523 1.415-.58 1.415h-3.004v7.004z" fill-rule="evenodd"/> <path d="M12.036 15.59c0 .55-.453.995-.997.995H5.032c-.55 0-.997-.445-.997-.996V8.584H1.03c-1.1 0-1.36-.633-.578-1.416L7.33.29c.39-.39 1.026-.385 1.412 0l6.878 6.88c.782.78.523 1.415-.58 1.415h-3.004v7.004z" fill-rule="evenodd" />
</g> </g>
</svg> </svg>
</div> </div>

View File

@ -3,7 +3,7 @@
<transition-group name="breadcrumb"> <transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path"> <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">{{ <span v-if="item.redirect==='noredirect'||index==levelList.length-1" class="no-redirect">{{
generateTitle(item.meta.title) }}</span> generateTitle(item.meta.title) }}</span>
<a v-else @click.prevent="handleLink(item)">{{ generateTitle(item.meta.title) }}</a> <a v-else @click.prevent="handleLink(item)">{{ generateTitle(item.meta.title) }}</a>
</el-breadcrumb-item> </el-breadcrumb-item>
</transition-group> </transition-group>

View File

@ -1,5 +1,5 @@
<template> <template>
<div :class="className" :id="id" :style="{height:height,width:width}"/> <div :id="id" :class="className" :style="{height:height,width:width}" />
</template> </template>
<script> <script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div :class="className" :id="id" :style="{height:height,width:width}"/> <div :id="id" :class="className" :style="{height:height,width:width}" />
</template> </template>
<script> <script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div :class="className" :id="id" :style="{height:height,width:width}"/> <div :id="id" :class="className" :style="{height:height,width:width}" />
</template> </template>
<script> <script>

View File

@ -4,10 +4,12 @@
<h3>{{ list1Title }}</h3> <h3>{{ list1Title }}</h3>
<draggable :list="list1" :options="{group:'article'}" class="dragArea"> <draggable :list="list1" :options="{group:'article'}" class="dragArea">
<div v-for="element in list1" :key="element.id" class="list-complete-item"> <div v-for="element in list1" :key="element.id" class="list-complete-item">
<div class="list-complete-item-handle">{{ element.id }}[{{ element.author }}] {{ element.title }}</div> <div class="list-complete-item-handle">
{{ element.id }}[{{ element.author }}] {{ element.title }}
</div>
<div style="position:absolute;right:0px;"> <div style="position:absolute;right:0px;">
<span style="float: right ;margin-top: -20px;margin-right:5px;" @click="deleteEle(element)"> <span style="float: right ;margin-top: -20px;margin-right:5px;" @click="deleteEle(element)">
<i style="color:#ff4949" class="el-icon-delete"/> <i style="color:#ff4949" class="el-icon-delete" />
</span> </span>
</div> </div>
</div> </div>
@ -17,7 +19,9 @@
<h3>{{ list2Title }}</h3> <h3>{{ list2Title }}</h3>
<draggable :list="list2" :options="{group:'article'}" class="dragArea"> <draggable :list="list2" :options="{group:'article'}" class="dragArea">
<div v-for="element in list2" :key="element.id" class="list-complete-item"> <div v-for="element in list2" :key="element.id" class="list-complete-item">
<div class="list-complete-item-handle2" @click="pushEle(element)">{{ element.id }} [{{ element.author }}] {{ element.title }}</div> <div class="list-complete-item-handle2" @click="pushEle(element)">
{{ element.id }} [{{ element.author }}] {{ element.title }}
</div>
</div> </div>
</draggable> </draggable>
</div> </div>

View File

@ -1,6 +1,6 @@
<template> <template>
<el-select ref="dragSelect" v-model="selectVal" v-bind="$attrs" class="drag-select" multiple v-on="$listeners"> <el-select ref="dragSelect" v-model="selectVal" v-bind="$attrs" class="drag-select" multiple v-on="$listeners">
<slot/> <slot />
</el-select> </el-select>
</template> </template>

View File

@ -1,5 +1,5 @@
<template> <template>
<div :ref="id" :action="url" :id="id" class="dropzone"> <div :id="id" :ref="id" :action="url" class="dropzone">
<input type="file" name="file"> <input type="file" name="file">
</div> </div>
</template> </template>

View File

@ -12,17 +12,23 @@
<template slot-scope="scope"> <template slot-scope="scope">
<div> <div>
<span class="message-title">Msg:</span> <span class="message-title">Msg:</span>
<el-tag type="danger">{{ scope.row.err.message }}</el-tag> <el-tag type="danger">
{{ scope.row.err.message }}
</el-tag>
</div> </div>
<br> <br>
<div> <div>
<span class="message-title" style="padding-right: 10px;">Info: </span> <span class="message-title" style="padding-right: 10px;">Info: </span>
<el-tag type="warning">{{ scope.row.vm.$vnode.tag }} error in {{ scope.row.info }}</el-tag> <el-tag type="warning">
{{ scope.row.vm.$vnode.tag }} error in {{ scope.row.info }}
</el-tag>
</div> </div>
<br> <br>
<div> <div>
<span class="message-title" style="padding-right: 16px;">Url: </span> <span class="message-title" style="padding-right: 16px;">Url: </span>
<el-tag type="success">{{ scope.row.url }}</el-tag> <el-tag type="success">
{{ scope.row.url }}
</el-tag>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
@ -33,7 +39,6 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>

View File

@ -5,17 +5,20 @@
height="80" height="80"
viewBox="0 0 250 250" viewBox="0 0 250 250"
style="fill:#40c9c6; color:#fff;" style="fill:#40c9c6; color:#fff;"
aria-hidden="true"> aria-hidden="true"
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"/> >
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" />
<path <path
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="currentColor" fill="currentColor"
style="transform-origin: 130px 106px;" style="transform-origin: 130px 106px;"
class="octo-arm"/> class="octo-arm"
/>
<path <path
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor" fill="currentColor"
class="octo-body"/> class="octo-body"
/>
</svg> </svg>
</a> </a>
</template> </template>

View File

@ -1,6 +1,6 @@
<template> <template>
<div :class="{'show':show}" class="header-search"> <div :class="{'show':show}" class="header-search">
<svg-icon class-name="search-icon" icon-class="search" @click="click" /> <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
<el-select <el-select
ref="headerSearchSelect" ref="headerSearchSelect"
v-model="search" v-model="search"
@ -10,13 +10,16 @@
remote remote
placeholder="Search" placeholder="Search"
class="header-search-select" class="header-search-select"
@change="change"> @change="change"
<el-option v-for="item in options" :key="item.path" :value="item" :label="item.title.join(' > ')"/> >
<el-option v-for="item in options" :key="item.path" :value="item" :label="item.title.join(' > ')" />
</el-select> </el-select>
</div> </div>
</template> </template>
<script> <script>
// fuse is a lightweight fuzzy-search module
// make search results more in line with expectations
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import path from 'path' import path from 'path'
import i18n from '@/lang' import i18n from '@/lang'
@ -33,8 +36,8 @@ export default {
} }
}, },
computed: { computed: {
routers() { routes() {
return this.$store.getters.permission_routers return this.$store.getters.permission_routes
}, },
lang() { lang() {
return this.$store.getters.language return this.$store.getters.language
@ -42,10 +45,10 @@ export default {
}, },
watch: { watch: {
lang() { lang() {
this.searchPool = this.generateRouters(this.routers) this.searchPool = this.generateRoutes(this.routes)
}, },
routers() { routes() {
this.searchPool = this.generateRouters(this.routers) this.searchPool = this.generateRoutes(this.routes)
}, },
searchPool(list) { searchPool(list) {
this.initFuse(list) this.initFuse(list)
@ -59,7 +62,7 @@ export default {
} }
}, },
mounted() { mounted() {
this.searchPool = this.generateRouters(this.routers) this.searchPool = this.generateRoutes(this.routes)
}, },
methods: { methods: {
click() { click() {
@ -100,10 +103,10 @@ export default {
}, },
// Filter out the routes that can be displayed in the sidebar // Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title // And generate the internationalized title
generateRouters(routers, basePath = '/', prefixTitle = []) { generateRoutes(routes, basePath = '/', prefixTitle = []) {
let res = [] let res = []
for (const router of routers) { for (const router of routes) {
// skip hidden router // skip hidden router
if (router.hidden) { continue } if (router.hidden) { continue }
@ -125,11 +128,11 @@ export default {
} }
} }
// recursive child routers // recursive child routes
if (router.children) { if (router.children) {
const tempRouters = this.generateRouters(router.children, data.path, data.title) const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
if (tempRouters.length >= 1) { if (tempRoutes.length >= 1) {
res = [...res, ...tempRouters] res = [...res, ...tempRoutes]
} }
} }
} }

View File

@ -2,22 +2,22 @@
<div v-show="value" class="vue-image-crop-upload"> <div v-show="value" class="vue-image-crop-upload">
<div class="vicp-wrap"> <div class="vicp-wrap">
<div class="vicp-close" @click="off"> <div class="vicp-close" @click="off">
<i class="vicp-icon4"/> <i class="vicp-icon4" />
</div> </div>
<div v-show="step == 1" class="vicp-step1"> <div v-show="step == 1" class="vicp-step1">
<div class="vicp-drop-area" @dragleave="preventDefault" @dragover="preventDefault" @dragenter="preventDefault" @click="handleClick" @drop="handleChange"> <div class="vicp-drop-area" @dragleave="preventDefault" @dragover="preventDefault" @dragenter="preventDefault" @click="handleClick" @drop="handleChange">
<i v-show="loading != 1" class="vicp-icon1"> <i v-show="loading != 1" class="vicp-icon1">
<i class="vicp-icon1-arrow"/> <i class="vicp-icon1-arrow" />
<i class="vicp-icon1-body"/> <i class="vicp-icon1-body" />
<i class="vicp-icon1-bottom"/> <i class="vicp-icon1-bottom" />
</i> </i>
<span v-show="loading !== 1" class="vicp-hint">{{ lang.hint }}</span> <span v-show="loading !== 1" class="vicp-hint">{{ lang.hint }}</span>
<span v-show="!isSupported" class="vicp-no-supported-hint">{{ lang.noSupported }}</span> <span v-show="!isSupported" class="vicp-no-supported-hint">{{ lang.noSupported }}</span>
<input v-show="false" v-if="step == 1" ref="fileinput" type="file" @change="handleChange"> <input v-show="false" v-if="step == 1" ref="fileinput" type="file" @change="handleChange">
</div> </div>
<div v-show="hasError" class="vicp-error"> <div v-show="hasError" class="vicp-error">
<i class="vicp-icon2"/> {{ errorMsg }} <i class="vicp-icon2" /> {{ errorMsg }}
</div> </div>
<div class="vicp-operate"> <div class="vicp-operate">
<a @click="off" @mousedown="ripple">{{ lang.btn.off }}</a> <a @click="off" @mousedown="ripple">{{ lang.btn.off }}</a>
@ -48,15 +48,16 @@
@mousedown="imgStartMove" @mousedown="imgStartMove"
@mousemove="imgMove" @mousemove="imgMove"
@mouseup="createImg" @mouseup="createImg"
@mouseout="createImg"> @mouseout="createImg"
<div :style="sourceImgShadeStyle" class="vicp-img-shade vicp-img-shade-1"/> >
<div :style="sourceImgShadeStyle" class="vicp-img-shade vicp-img-shade-2"/> <div :style="sourceImgShadeStyle" class="vicp-img-shade vicp-img-shade-1" />
<div :style="sourceImgShadeStyle" class="vicp-img-shade vicp-img-shade-2" />
</div> </div>
<div class="vicp-range"> <div class="vicp-range">
<input :value="scale.range" type="range" step="1" min="0" max="100" @input="zoomChange"> <input :value="scale.range" type="range" step="1" min="0" max="100" @input="zoomChange">
<i class="vicp-icon5" @mousedown="startZoomSub" @mouseout="endZoomSub" @mouseup="endZoomSub"/> <i class="vicp-icon5" @mousedown="startZoomSub" @mouseout="endZoomSub" @mouseup="endZoomSub" />
<i class="vicp-icon6" @mousedown="startZoomAdd" @mouseout="endZoomAdd" @mouseup="endZoomAdd"/> <i class="vicp-icon6" @mousedown="startZoomAdd" @mouseout="endZoomAdd" @mouseup="endZoomAdd" />
</div> </div>
<div v-if="!noRotate" class="vicp-rotate"> <div v-if="!noRotate" class="vicp-rotate">
@ -87,13 +88,13 @@
<div class="vicp-upload"> <div class="vicp-upload">
<span v-show="loading === 1" class="vicp-loading">{{ lang.loading }}</span> <span v-show="loading === 1" class="vicp-loading">{{ lang.loading }}</span>
<div class="vicp-progress-wrap"> <div class="vicp-progress-wrap">
<span v-show="loading === 1" :style="progressStyle" class="vicp-progress"/> <span v-show="loading === 1" :style="progressStyle" class="vicp-progress" />
</div> </div>
<div v-show="hasError" class="vicp-error"> <div v-show="hasError" class="vicp-error">
<i class="vicp-icon2"/> {{ errorMsg }} <i class="vicp-icon2" /> {{ errorMsg }}
</div> </div>
<div v-show="loading === 2" class="vicp-success"> <div v-show="loading === 2" class="vicp-success">
<i class="vicp-icon3"/> {{ lang.success }} <i class="vicp-icon3" /> {{ lang.success }}
</div> </div>
</div> </div>
<div class="vicp-operate"> <div class="vicp-operate">
@ -101,7 +102,7 @@
<a @click="off" @mousedown="ripple">{{ lang.btn.close }}</a> <a @click="off" @mousedown="ripple">{{ lang.btn.close }}</a>
</div> </div>
</div> </div>
<canvas v-show="false" ref="canvas" :width="width" :height="height"/> <canvas v-show="false" ref="canvas" :width="width" :height="height" />
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="json-editor"> <div class="json-editor">
<textarea ref="textarea"/> <textarea ref="textarea" />
</div> </div>
</template> </template>

View File

@ -6,7 +6,8 @@
<draggable <draggable
:list="list" :list="list"
:options="options" :options="options"
class="board-column-content"> class="board-column-content"
>
<div v-for="element in list" :key="element.id" class="board-item"> <div v-for="element in list" :key="element.id" class="board-item">
{{ element.name }} {{ element.id }} {{ element.name }} {{ element.id }}
</div> </div>

View File

@ -4,9 +4,15 @@
<svg-icon class-name="international-icon" icon-class="language" /> <svg-icon class-name="international-icon" icon-class="language" />
</div> </div>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item :disabled="language==='zh'" command="zh">中文</el-dropdown-item> <el-dropdown-item :disabled="language==='zh'" command="zh">
<el-dropdown-item :disabled="language==='en'" command="en">English</el-dropdown-item> 中文
<el-dropdown-item :disabled="language==='es'" command="es">Español</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item :disabled="language==='en'" command="en">
English
</el-dropdown-item>
<el-dropdown-item :disabled="language==='es'" command="es">
Español
</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</template> </template>
@ -21,7 +27,7 @@ export default {
methods: { methods: {
handleSetLanguage(lang) { handleSetLanguage(lang) {
this.$i18n.locale = lang this.$i18n.locale = lang
this.$store.dispatch('setLanguage', lang) this.$store.dispatch('app/setLanguage', lang)
this.$message({ this.$message({
message: 'Switch Language Success', message: 'Switch Language Success',
type: 'success' type: 'success'

View File

@ -1,12 +1,12 @@
<template> <template>
<div :class="computedClasses" class="material-input__component"> <div :class="computedClasses" class="material-input__component">
<div :class="{iconClass:icon}"> <div :class="{iconClass:icon}">
<i v-if="icon" :class="['el-icon-' + icon]" class="el-input__icon material-input__icon"/> <i v-if="icon" :class="['el-icon-' + icon]" class="el-input__icon material-input__icon" />
<input <input
v-if="type === 'email'" v-if="type === 'email'"
v-model="currentValue"
:name="name" :name="name"
:placeholder="fillPlaceHolder" :placeholder="fillPlaceHolder"
v-model="currentValue"
:readonly="readonly" :readonly="readonly"
:disabled="disabled" :disabled="disabled"
:autoComplete="autoComplete" :autoComplete="autoComplete"
@ -15,12 +15,13 @@
class="material-input" class="material-input"
@focus="handleMdFocus" @focus="handleMdFocus"
@blur="handleMdBlur" @blur="handleMdBlur"
@input="handleModelInput"> @input="handleModelInput"
>
<input <input
v-if="type === 'url'" v-if="type === 'url'"
v-model="currentValue"
:name="name" :name="name"
:placeholder="fillPlaceHolder" :placeholder="fillPlaceHolder"
v-model="currentValue"
:readonly="readonly" :readonly="readonly"
:disabled="disabled" :disabled="disabled"
:autoComplete="autoComplete" :autoComplete="autoComplete"
@ -29,12 +30,13 @@
class="material-input" class="material-input"
@focus="handleMdFocus" @focus="handleMdFocus"
@blur="handleMdBlur" @blur="handleMdBlur"
@input="handleModelInput"> @input="handleModelInput"
>
<input <input
v-if="type === 'number'" v-if="type === 'number'"
v-model="currentValue"
:name="name" :name="name"
:placeholder="fillPlaceHolder" :placeholder="fillPlaceHolder"
v-model="currentValue"
:step="step" :step="step"
:readonly="readonly" :readonly="readonly"
:disabled="disabled" :disabled="disabled"
@ -48,12 +50,13 @@
class="material-input" class="material-input"
@focus="handleMdFocus" @focus="handleMdFocus"
@blur="handleMdBlur" @blur="handleMdBlur"
@input="handleModelInput"> @input="handleModelInput"
>
<input <input
v-if="type === 'password'" v-if="type === 'password'"
v-model="currentValue"
:name="name" :name="name"
:placeholder="fillPlaceHolder" :placeholder="fillPlaceHolder"
v-model="currentValue"
:readonly="readonly" :readonly="readonly"
:disabled="disabled" :disabled="disabled"
:autoComplete="autoComplete" :autoComplete="autoComplete"
@ -64,12 +67,13 @@
class="material-input" class="material-input"
@focus="handleMdFocus" @focus="handleMdFocus"
@blur="handleMdBlur" @blur="handleMdBlur"
@input="handleModelInput"> @input="handleModelInput"
>
<input <input
v-if="type === 'tel'" v-if="type === 'tel'"
v-model="currentValue"
:name="name" :name="name"
:placeholder="fillPlaceHolder" :placeholder="fillPlaceHolder"
v-model="currentValue"
:readonly="readonly" :readonly="readonly"
:disabled="disabled" :disabled="disabled"
:autoComplete="autoComplete" :autoComplete="autoComplete"
@ -78,12 +82,13 @@
class="material-input" class="material-input"
@focus="handleMdFocus" @focus="handleMdFocus"
@blur="handleMdBlur" @blur="handleMdBlur"
@input="handleModelInput"> @input="handleModelInput"
>
<input <input
v-if="type === 'text'" v-if="type === 'text'"
v-model="currentValue"
:name="name" :name="name"
:placeholder="fillPlaceHolder" :placeholder="fillPlaceHolder"
v-model="currentValue"
:readonly="readonly" :readonly="readonly"
:disabled="disabled" :disabled="disabled"
:autoComplete="autoComplete" :autoComplete="autoComplete"
@ -94,10 +99,11 @@
class="material-input" class="material-input"
@focus="handleMdFocus" @focus="handleMdFocus"
@blur="handleMdBlur" @blur="handleMdBlur"
@input="handleModelInput"> @input="handleModelInput"
<span class="material-input-bar"/> >
<span class="material-input-bar" />
<label class="material-label"> <label class="material-label">
<slot/> <slot />
</label> </label>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
<template> <template>
<div :id="id"/> <div :id="id" />
</template> </template>
<script> <script>

View File

@ -9,7 +9,8 @@
:total="total" :total="total"
v-bind="$attrs" v-bind="$attrs"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange"/> @current-change="handleCurrentChange"
/>
</div> </div>
</template> </template>

View File

@ -2,7 +2,7 @@
<div :style="{zIndex:zIndex,height:height,width:width}" class="pan-item"> <div :style="{zIndex:zIndex,height:height,width:width}" class="pan-item">
<div class="pan-info"> <div class="pan-info">
<div class="pan-info-roles-container"> <div class="pan-info-roles-container">
<slot/> <slot />
</div> </div>
</div> </div>
<img :src="image" class="pan-thumb"> <img :src="image" class="pan-thumb">

View File

@ -0,0 +1,137 @@
<template>
<div ref="rightPanel" :class="{show:show}" class="rightPanel-container">
<div class="rightPanel-background" />
<div class="rightPanel">
<el-button class="handle-button" :style="{'top':buttonTop+'px'}" type="primary" circle :icon="show?'el-icon-close':'el-icon-setting'" @click="show=!show" />
<div class="rightPanel-items">
<slot />
</div>
</div>
</div>
</template>
<script>
import { addClass, removeClass } from '@/utils'
export default {
name: 'RightPanel',
props: {
clickNotClose: {
default: false,
type: Boolean
},
buttonTop: {
default: 240,
type: Number
}
},
data() {
return {
show: false
}
},
watch: {
show(value) {
if (value && !this.clickNotClose) {
this.addEventClick()
}
if (value) {
addClass(document.body, 'showRightPanel')
} else {
removeClass(document.body, 'showRightPanel')
}
}
},
mounted() {
this.insertToBody()
},
beforeDestroy() {
const elx = this.$refs.rightPanel
elx.remove()
},
methods: {
addEventClick() {
window.addEventListener('click', this.closeSidebar)
},
closeSidebar(evt) {
const parent = evt.target.closest('.rightPanel')
if (!parent) {
this.show = false
window.removeEventListener('click', this.closeSidebar)
}
},
insertToBody() {
const elx = this.$refs.rightPanel
const body = document.querySelector('body')
body.insertBefore(elx, body.firstChild)
}
}
}
</script>
<style>
.showRightPanel {
overflow: hidden;
position: relative;
width: calc(100% - 15px);
}
</style>
<style rel="stylesheet/scss" lang="scss" scoped>
.rightPanel-background {
opacity: 0;
transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
background: rgba(0, 0, 0, .2);
width: 0;
height: 0;
top: 0;
left: 0;
position: fixed;
z-index: -1;
}
.rightPanel {
background: #fff;
z-index: 3000;
position: fixed;
height: 100vh;
width: 100%;
max-width: 260px;
top: 0px;
left: 0px;
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, .05);
transition: all .25s cubic-bezier(.7, .3, .1, 1);
transform: translate(100%);
z-index: 40000;
left: auto;
right: 0px;
}
.show {
transition: all .3s cubic-bezier(.7, .3, .1, 1);
.rightPanel-background {
z-index: 20000;
opacity: 1;
width: 100%;
height: 100%;
}
.rightPanel {
transform: translate(0);
}
}
.handle-button {
position: absolute;
left: -48px;
border-radius: 6px 0 0 6px !important;
width: 48px;
height: 48px;
pointer-events: auto;
z-index: 0;
font-size: 24px;
text-align: center;
}
</style>

View File

@ -4,8 +4,10 @@
<svg-icon class-name="size-icon" icon-class="size" /> <svg-icon class-name="size-icon" icon-class="size" />
</div> </div>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value">{{ <el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value">
item.label }}</el-dropdown-item> {{
item.label }}
</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</template> </template>
@ -30,7 +32,7 @@ export default {
methods: { methods: {
handleSetSize(size) { handleSetSize(size) {
this.$ELEMENT.size = size this.$ELEMENT.size = size
this.$store.dispatch('setSize', size) this.$store.dispatch('app/setSize', size)
this.refreshView() this.refreshView()
this.$message({ this.$message({
message: 'Switch Size Success', message: 'Switch Size Success',
@ -39,7 +41,7 @@ export default {
}, },
refreshView() { refreshView() {
// In order to make the cached page re-rendered // In order to make the cached page re-rendered
this.$store.dispatch('delAllCachedViews', this.$route) this.$store.dispatch('tagsView/delAllCachedViews', this.$route)
const { fullPath } = this.$route const { fullPath } = this.$route

View File

@ -1,6 +1,9 @@
<template> <template>
<div :style="{height:height+'px',zIndex:zIndex}"> <div :style="{height:height+'px',zIndex:zIndex}">
<div :class="className" :style="{top:stickyTop+'px',zIndex:zIndex,position:position,width:width,height:height+'px'}"> <div
:class="className"
:style="{top:(isSticky ? stickyTop +'px' : ''),zIndex:zIndex,position:position,width:width,height:height+'px'}"
>
<slot> <slot>
<div>sticky</div> <div>sticky</div>
</slot> </slot>

View File

@ -1,6 +1,6 @@
<template> <template>
<svg :class="svgClass" aria-hidden="true" v-on="$listeners"> <svg :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName"/> <use :xlink:href="iconName" />
</svg> </svg>
</template> </template>

View File

@ -1,8 +1,8 @@
<template> <template>
<a :class="className" class="link--mallki" href="#"> <a :class="className" class="link--mallki" href="#">
{{ text }} {{ text }}
<span :data-letters="text"/> <span :data-letters="text" />
<span :data-letters="text"/> <span :data-letters="text" />
</a> </a>
</template> </template>

View File

@ -1,8 +1,10 @@
<template> <template>
<el-color-picker <el-color-picker
v-model="theme" v-model="theme"
:predefine="['#409EFF', '#11a983', '#13c2c2', '#6959CD', '#f5222d', '#eb2f96', '#DB7093', '#e6a23c', '#8B8989', '#212121']"
class="theme-picker" class="theme-picker"
popper-class="theme-picker-dropdown"/> popper-class="theme-picker-dropdown"
/>
</template> </template>
<script> <script>
@ -18,12 +20,21 @@ export default {
} }
}, },
watch: { watch: {
theme(val) { async theme(val) {
const oldVal = this.theme const oldVal = this.theme
if (typeof val !== 'string') return if (typeof val !== 'string') return
const themeCluster = this.getThemeCluster(val.replace('#', '')) const themeCluster = this.getThemeCluster(val.replace('#', ''))
const originalCluster = this.getThemeCluster(oldVal.replace('#', '')) const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
console.log(themeCluster, originalCluster) console.log(themeCluster, originalCluster)
const $message = this.$message({
message: ' Compiling the theme',
customClass: 'theme-message',
type: 'success',
duration: 0,
iconClass: 'el-icon-loading'
})
const getHandler = (variable, id) => { const getHandler = (variable, id) => {
return () => { return () => {
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', '')) const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
@ -39,15 +50,15 @@ export default {
} }
} }
const chalkHandler = getHandler('chalk', 'chalk-style')
if (!this.chalk) { if (!this.chalk) {
const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css` const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
this.getCSSString(url, chalkHandler, 'chalk') await this.getCSSString(url, 'chalk')
} else {
chalkHandler()
} }
const chalkHandler = getHandler('chalk', 'chalk-style')
chalkHandler()
const styles = [].slice.call(document.querySelectorAll('style')) const styles = [].slice.call(document.querySelectorAll('style'))
.filter(style => { .filter(style => {
const text = style.innerText const text = style.innerText
@ -58,39 +69,32 @@ export default {
if (typeof innerText !== 'string') return if (typeof innerText !== 'string') return
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster) style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
}) })
this.$message({
message: '换肤成功', $message.close()
type: 'success'
})
} }
}, },
methods: { methods: {
updateStyle(style, oldCluster, newCluster) { updateStyle(style, oldCluster, newCluster) {
const colorOverrides = [] // only capture color overides let newStyle = style
oldCluster.forEach((color, index) => { oldCluster.forEach((color, index) => {
const value = newCluster[index] newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
const color_plain = color.replace(/([()])/g, '\\$1')
const repl = new RegExp(`(^|})([^{]+{[^{}]+)${color_plain}\\b([^}]*)(?=})`, 'gi')
const nestRepl = new RegExp(color_plain, 'ig') // for greed matching before the 'color'
let v
while ((v = repl.exec(style))) {
colorOverrides.push(v[2].replace(nestRepl, value) + value + v[3] + '}') // '}' not captured in the regexp repl to reserve it as locator-boundary
}
}) })
return colorOverrides.join('') return newStyle
}, },
getCSSString(url, callback, variable) { getCSSString(url, variable) {
const xhr = new XMLHttpRequest() return new Promise(resolve => {
xhr.onreadystatechange = () => { const xhr = new XMLHttpRequest()
if (xhr.readyState === 4 && xhr.status === 200) { xhr.onreadystatechange = () => {
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '') if (xhr.readyState === 4 && xhr.status === 200) {
callback() this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
resolve()
}
} }
} xhr.open('GET', url)
xhr.open('GET', url) xhr.send()
xhr.send() })
}, },
getThemeCluster(theme) { getThemeCluster(theme) {
@ -142,10 +146,14 @@ export default {
</script> </script>
<style> <style>
.theme-message,
.theme-picker-dropdown {
z-index: 99999 !important;
}
.theme-picker .el-color-picker__trigger { .theme-picker .el-color-picker__trigger {
margin-top: 12px; height: 26px !important;
height: 26px!important; width: 26px !important;
width: 26px!important;
padding: 2px; padding: 2px;
} }

View File

@ -1,6 +1,7 @@
<template> <template>
<div class="upload-container"> <div class="upload-container">
<el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">上传图片 <el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">
上传图片
</el-button> </el-button>
<el-dialog :visible.sync="dialogVisible"> <el-dialog :visible.sync="dialogVisible">
<el-upload <el-upload
@ -12,11 +13,18 @@
:before-upload="beforeUpload" :before-upload="beforeUpload"
class="editor-slide-upload" class="editor-slide-upload"
action="https://httpbin.org/post" action="https://httpbin.org/post"
list-type="picture-card"> list-type="picture-card"
<el-button size="small" type="primary">点击上传</el-button> >
<el-button size="small" type="primary">
点击上传
</el-button>
</el-upload> </el-upload>
<el-button @click="dialogVisible = false"> </el-button> <el-button @click="dialogVisible = false">
<el-button type="primary" @click="handleSubmit"> </el-button>
</el-button>
<el-button type="primary" @click="handleSubmit">
</el-button>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>

View File

@ -1,8 +1,8 @@
<template> <template>
<div :class="{fullscreen:fullscreen}" class="tinymce-container editor-container"> <div :class="{fullscreen:fullscreen}" class="tinymce-container editor-container">
<textarea :id="tinymceId" class="tinymce-textarea"/> <textarea :id="tinymceId" class="tinymce-textarea" />
<div class="editor-custom-btn-container"> <div class="editor-custom-btn-container">
<editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK"/> <editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK" />
</div> </div>
</div> </div>
</template> </template>

View File

@ -0,0 +1,220 @@
- [Enlgish](#Brief)
# 中文
## 写在前面
此组件仅提供一个创建 `TreeTable` 的解决思路。它基于`element-ui`的 table 组件实现,通过`el-table`的`row-style`方法,在里面判断元素是否需要隐藏或者显示,从而实现`TreeTable`的展开与收起。
并且本组件充分利用 `vue` 插槽的特性来方便用户自定义。
`evel.js` 里面,`addAttrs` 方法会给数据添加几个属性,`treeTotable` 会对数组扁平化。这些操作都不会破坏源数据,只是会新增属性。
## Props 说明
| Attribute | Description | Type | Default |
| :--------------: | :--------------------------------- | :-----: | :------: |
| data | 原始展示数据 | Array | [] |
| columns | 列属性 | Array | [] |
| defaultExpandAll | 默认是否全部展开 | Boolean | false |
| defaultChildren | 指定子树为节点对象的某个属性值 | String | children | |
| indent | 相邻级节点间的水平缩进,单位为像素 | Number | 50 |
> 任何 `el-table` 的属性都支持,例如`border`、`fit`、`size`或者`@select`、`@cell-click`等方法。详情属性见`el-table`文档。
---
### 代码示例
```html
<tree-table :data="data" :columns="columns" border>
```
#### data(**必填**)
```js
const data = [
{
name:'1'
children: [
{
name: '1-1'
},
{
name: '1-2'
}
]
},
{
name: `2`
}
]
```
#### columns(**必填**)
- label: 显示在表头的文字
- key: 对应 data 的 key。treeTable 将显示相应的 value
- expand: `true` or `false`。若为 true则在该列显示展开收起图标
- checkbox: `true` or `false`。若为 true则在该列显示`checkbox`
- width: 每列的宽度,为一个数字(可选)。例如`200`
- align: 对齐方式 `left/center/right`
- header-align: 表头对齐方式 `left/center/right`
```javascript
const columns = [
{
label: 'Checkbox',
checkbox: true
},
{
label: '',
key: 'id',
expand: true
},
{
label: 'Event',
key: 'event',
width: 200,
align: 'left'
},
{
label: 'Scope',
key: 'scope'
}
]
```
> 树表组件将会根据 columns 的 key 属性生成具名插槽,如果你需要对列数据进行自定义,通过插槽即可实现
```html
<template slot="your key" slot-scope="{scope}">
<el-tag>level: {{ scope.row._level }}</el-tag>
<el-tag>expand: {{ scope.row._expand }}</el-tag>
<el-tag>select: {{ scope.row._select }}</el-tag>
</template>
```
## Events
目前提供了几个方法,不过只是`beta`版本,之后很可能会修改。
```js
this.$refs.TreeTable.addChild(row, data) //添加子元素
this.$refs.TreeTable.addBrother(row, data) //添加兄弟元素
this.$refs.TreeTable.delete(row) //删除该元素
```
## 其他
如果有其他的需求,请参考[el-table](http://element-cn.eleme.io/#/en-US/component/table)的 api 自行修改 index.vue
# English
## Brief
This component only provides a solution for creating `TreeTable`. It is based on the `element-ui` table component. It uses the `row-style` method of `el-table` to determine whether the element needs to be hidden or displayed.
And this component makes full use of the features of the `vue` slot to make it user-friendly.
In `evel.js`, the `addAttrs` method adds several properties to the data, and `treeTotable` flattens the array. None of these operations will destroy the source data, just add properties.
## Props
| Attribute | Description | Type | Default |
| :--------------: | :----------------------------------------------------------- | :-----: | :------: |
| data | original display data | Array | [] |
| columns | column attribute | Array | [] |
| defaultExpandAll | whether to expand all nodes by default | Boolean | false |
| defaultChildren | specify which node object is used as the node's subtree | String | children | |
| indent | horizontal indentation of nodes in adjacent levels in pixels | Number | 50 |
> Any of the `el-table` properties are supported, such as `border`, `fit`, `size` or `@select`, `@cell-click`. See the ʻel-table` documentation for details.
---
### Example
```html
<tree-table :data="data" :columns="columns" border>
```
#### data(**Required**)
```js
const data = [
{
name:'1'
children: [
{
name: '1-1'
},
{
name: '1-2'
}
]
},
{
name: `2`
}
]
```
#### columns(**Required**)
- label: text displayed in the header
- key: data.key will show in column
- expand: `true` or `false`
- checkbox: `true` or `false`
- width: column width 。such as `200`
- align: alignment `left/center/right`
- header-align: alignment of the table header `left/center/right`
```javascript
const columns = [
{
label: 'Checkbox',
checkbox: true
},
{
label: '',
key: 'id',
expand: true
},
{
label: 'Event',
key: 'event',
width: 200,
align: 'left'
},
{
label: 'Scope',
key: 'scope'
}
]
```
> The tree table component will generate a named slot based on the key property of columns. If you need to customize the column data, you can do it through the slot.
```html
<template slot="your key" slot-scope="{scope}">
<el-tag>level: {{ scope.row._level }}</el-tag>
<el-tag>expand: {{ scope.row._expand }}</el-tag>
<el-tag>select: {{ scope.row._select }}</el-tag>
</template>
```
## Events
Several methods are currently available, but only the `beta` version, which is likely to be modified later.
```js
this.$refs.TreeTable.addChild(row, data) //Add child elements
this.$refs.TreeTable.addBrother(row, data) //Add a sibling element
this.$refs.TreeTable.delete(row) //Delete the element
```
## Other
If you have other requirements, please refer to the [el-table](http://element-cn.eleme.io/#/en-US/component/table) api to modify the index.vue

View File

@ -1,29 +1,48 @@
/**
* @Author: jianglei
* @Date: 2017-10-12 12:06:49
*/
'use strict'
import Vue from 'vue' import Vue from 'vue'
export default function treeToArray(data, expandAll, parent = null, level = null) {
// Flattened array
export default function treeToArray(data, children = 'children') {
let tmp = [] let tmp = []
Array.from(data).forEach(function(record) { data.forEach((item, index) => {
if (record._expanded === undefined) { Vue.set(item, '_index', index)
Vue.set(record, '_expanded', expandAll) tmp.push(item)
} if (item[children] && item[children].length > 0) {
let _level = 1 const res = treeToArray(item[children], children)
if (level !== undefined && level !== null) { tmp = tmp.concat(res)
_level = level + 1
}
Vue.set(record, '_level', _level)
// 如果有父元素
if (parent) {
Vue.set(record, 'parent', parent)
}
tmp.push(record)
if (record.children && record.children.length > 0) {
const children = treeToArray(record.children, expandAll, record, _level)
tmp = tmp.concat(children)
} }
}) })
return tmp return tmp
} }
export function addAttrs(data, { parent = null, preIndex = false, level = 0, expand = false, children = 'children', show = true, select = false } = {}) {
data.forEach((item, index) => {
const _id = (preIndex ? `${preIndex}-${index}` : index) + ''
Vue.set(item, '_id', _id)
Vue.set(item, '_level', level)
Vue.set(item, '_expand', expand)
Vue.set(item, '_parent', parent)
Vue.set(item, '_show', show)
Vue.set(item, '_select', select)
if (item[children] && item[children].length > 0) {
addAttrs(item[children], {
parent: item,
level: level + 1,
expand,
preIndex: _id,
children,
status,
select
})
}
})
}
export function cleanParentAttr(data, children = 'children') {
data.forEach(item => {
item._parent = null
if (item[children] && item[children].length > 0) {
addAttrs(item[children], children)
}
})
return data
}

View File

@ -1,127 +1,193 @@
<template> <template>
<el-table :data="formatData" :row-style="showRow" v-bind="$attrs"> <el-table :data="tableData" :row-style="showRow" v-bind="$attrs" v-on="$listeners">
<el-table-column v-if="columns.length===0" width="150"> <slot name="selection" />
<slot name="pre-column" />
<el-table-column
v-for="item in columns"
:key="item.key"
:label="item.label"
:width="item.width"
:align="item.align||'center'"
:header-align="item.headerAlign"
>
<template slot-scope="scope"> <template slot-scope="scope">
<span v-for="space in scope.row._level" :key="space" class="ms-tree-space"/> <slot :scope="scope" :name="item.key">
<span v-if="iconShow(0,scope.row)" class="tree-ctrl" @click="toggleExpanded(scope.$index)"> <template v-if="item.expand">
<i v-if="!scope.row._expanded" class="el-icon-plus"/> <span :style="{'padding-left':+scope.row._level*indent + 'px'} " />
<i v-else class="el-icon-minus"/> <span v-show="showSperadIcon(scope.row)" class="tree-ctrl" @click="toggleExpanded(scope.$index)">
</span> <i v-if="!scope.row._expand" class="el-icon-plus" />
{{ scope.$index }} <i v-else class="el-icon-minus" />
</span>
</template>
<template v-if="item.checkbox">
<el-checkbox
v-if="scope.row[defaultChildren]&&scope.row[defaultChildren].length>0"
v-model="scope.row._select"
:style="{'padding-left':+scope.row._level*indent + 'px'} "
:indeterminate="scope.row._select"
@change="handleCheckAllChange(scope.row)"
/>
<el-checkbox
v-else
v-model="scope.row._select"
:style="{'padding-left':+scope.row._level*indent + 'px'} "
@change="handleCheckAllChange(scope.row)"
/>
</template>
{{ scope.row[item.key] }}
</slot>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column v-for="(column, index) in columns" v-else :key="column.value" :label="column.text" :width="column.width">
<template slot-scope="scope">
<!-- Todo -->
<!-- eslint-disable-next-line vue/no-confusing-v-for-v-if -->
<span v-for="space in scope.row._level" v-if="index === 0" :key="space" class="ms-tree-space"/>
<span v-if="iconShow(index,scope.row)" class="tree-ctrl" @click="toggleExpanded(scope.$index)">
<i v-if="!scope.row._expanded" class="el-icon-plus"/>
<i v-else class="el-icon-minus"/>
</span>
{{ scope.row[column.value] }}
</template>
</el-table-column>
<slot/>
</el-table> </el-table>
</template> </template>
<script> <script>
/** import treeToArray, { addAttrs } from './eval.js'
Auth: Lei.j1ang
Created: 2018/1/19-13:59
*/
import treeToArray from './eval'
export default { export default {
name: 'TreeTable', name: 'TreeTable',
props: { props: {
/* eslint-disable */
data: { data: {
type: [Array, Object], type: Array,
required: true required: true,
default: () => []
}, },
columns: { columns: {
type: Array, type: Array,
default: () => [] default: () => []
}, },
evalFunc: Function, defaultExpandAll: {
evalArgs: Array,
expandAll: {
type: Boolean, type: Boolean,
default: false default: false
},
defaultChildren: {
type: String,
default: 'children'
},
indent: {
type: Number,
default: 50
}
},
data() {
return {
guard: 1
} }
}, },
computed: { computed: {
// children() {
formatData: function() { return this.defaultChildren
let tmp },
if (!Array.isArray(this.data)) { tableData() {
tmp = [this.data] const data = this.data
} else { if (this.data.length === 0) {
tmp = this.data return []
} }
const func = this.evalFunc || treeToArray addAttrs(data, {
const args = this.evalArgs ? [].concat([tmp, this.expandAll], this.evalArgs) : [tmp, this.expandAll] expand: this.defaultExpandAll,
return func.apply(null, args) children: this.defaultChildren
})
const retval = treeToArray(data, this.defaultChildren)
return retval
} }
}, },
methods: { methods: {
showRow: function(row) { addBrother(row, data) {
const show = (row.row.parent ? (row.row.parent._expanded && row.row.parent._show) : true) if (row._parent) {
row.row._show = show row._parent.children.push(data)
return show ? 'animation:treeTableShow 1s;-webkit-animation:treeTableShow 1s;' : 'display:none;' } else {
this.data.push(data)
}
}, },
// addChild(row, data) {
toggleExpanded: function(trIndex) { if (!row.children) {
const record = this.formatData[trIndex] this.$set(row, 'children', [])
record._expanded = !record._expanded }
row.children.push(data)
}, },
// delete(row) {
iconShow(index, record) { const { _index, _parent } = row
return (index === 0 && record.children && record.children.length > 0) if (_parent) {
_parent.children.splice(_index, 1)
} else {
this.data.splice(_index, 1)
}
},
getData() {
return this.tableData
},
showRow: function({ row }) {
const parent = row._parent
const show = parent ? parent._expand && parent._show : true
row._show = show
return show
? 'animation:treeTableShow 1s;-webkit-animation:treeTableShow 1s;'
: 'display:none;'
},
showSperadIcon(record) {
return record[this.children] && record[this.children].length > 0
},
toggleExpanded(trIndex) {
const record = this.tableData[trIndex]
const expand = !record._expand
record._expand = expand
},
handleCheckAllChange(row) {
this.selcetRecursion(row, row._select, this.defaultChildren)
this.isIndeterminate = row._select
},
selcetRecursion(row, select, children = 'children') {
if (select) {
this.$set(row, '_expand', true)
this.$set(row, '_show', true)
}
const sub_item = row[children]
if (sub_item && sub_item.length > 0) {
sub_item.map(child => {
child._select = select
this.selcetRecursion(child, select, children)
})
}
},
updateTreeNode(item) {
return new Promise(resolve => {
const { _id, _parent } = item
const index = _id.split('-').slice(-1)[0] // get last index
if (_parent) {
_parent.children.splice(index, 1, item)
resolve(this.data)
} else {
this.data.splice(index, 1, item)
resolve(this.data)
}
})
} }
} }
} }
</script> </script>
<style rel="stylesheet/css">
@keyframes treeTableShow {
from {opacity: 0;}
to {opacity: 1;}
}
@-webkit-keyframes treeTableShow {
from {opacity: 0;}
to {opacity: 1;}
}
</style>
<style lang="scss" rel="stylesheet/scss" scoped> <style>
$color-blue: #2196F3; @keyframes treeTableShow {
$space-width: 18px; from {
.ms-tree-space { opacity: 0;
position: relative;
top: 1px;
display: inline-block;
font-style: normal;
font-weight: 400;
line-height: 1;
width: $space-width;
height: 14px;
&::before {
content: ""
}
} }
.processContainer{ to {
width: 100%; opacity: 1;
height: 100%;
} }
table td { }
line-height: 26px; @-webkit-keyframes treeTableShow {
from {
opacity: 0;
} }
to {
opacity: 1;
}
}
.tree-ctrl{ .tree-ctrl {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
color: $color-blue; color: #2196f3;
margin-left: -$space-width; }
}
</style> </style>

View File

@ -1,89 +0,0 @@
## 写在前面
此组件仅提供一个创建TreeTable的解决思路
## prop说明
#### *data*
**必填**
原始数据,要求是一个数组或者对象
```javascript
[{
key1: value1,
key2: value2,
children: [{
key1: value1
},
{
key1: value1
}]
},
{
key1: value1
}]
```
或者
```javascript
{
key1: value1,
key2: value2,
children: [{
key1: value1
},
{
key1: value1
}]
}
```
#### columns
列属性,要求是一个数组
1. text: 显示在表头的文字
2. value: 对应data的key。treeTable将显示相应的value
3. width: 每列的宽度,为一个数字(可选)
如果你想要每个字段都有自定义的样式或者嵌套其他组件columns可不提供直接像在el-table一样写即可如果没有自定义内容提供columns将更加的便捷方便
如果你有几个字段是需要自定义的几个不需要那么可以将不需要自定义的字段放入columns将需要自定义的内容放入到slot中详情见后文
```javascript
[{
value:string,
text:string,
width:number
},{
value:string,
text:string,
width:number
}]
```
#### expandAll
是否默认全部展开boolean值默认为false
#### evalFunc
解析函数function非必须
如果不提供,将使用默认的[evalFunc](./eval.js)
如果提供了evalFunc,那么会用提供的evalFunc去解析data并返回treeTable渲染所需要的值。如何编写一个evalFunc请参考[*eval.js*](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/components/TreeTable/eval.js)或[*customEval.js*](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customEval.js)
#### evalArgs
解析函数的参数,是一个数组
**请注意自定义的解析函数参数第一个为this.data第二个参数为 this.expandAll,你不需要在evalArgs填写。一定记住这两个参数是强制性的并且位置不可颠倒** *this.data为需要解析的数据this.expandAll为是否默认展开*
如你的解析函数需要的参数为`(this.data, this.expandAll,1,2,3,4)`,那么你只需要将`[1,2,3,4]`赋值给`evalArgs`就可以了
如果你的解析函数参数只有`(this.data, this.expandAll)`,那么就可以不用填写evalArgs了
具体可参考[*customEval.js*](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customEval.js)的函数参数和[customTreeTable](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customTreeTable.vue)的`evalArgs`属性值
## slot
这是一个自定义列的插槽。
默认情况下treeTable只有一行行展示数据的功能。但是一般情况下我们会要给行加上一个操作按钮或者根据当行数据展示不同的样式这时我们就需要自定义列了。请参考[customTreeTable](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customTreeTable.vue)[实例效果](https://panjiachen.github.io/vue-element-admin/#/table/tree-table)
`slot`和`columns属性`可同时存在,columns里面的数据列会在slot自定义列的左边展示
## 其他
如果有其他的需求,请参考[el-table](http://element-cn.eleme.io/#/en-US/component/table)的api自行修改index.vue

View File

@ -7,15 +7,18 @@
:on-success="handleImageSuccess" :on-success="handleImageSuccess"
class="image-uploader" class="image-uploader"
drag drag
action="https://httpbin.org/post"> action="https://httpbin.org/post"
<i class="el-icon-upload"/> >
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div> <i class="el-icon-upload" />
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
</el-upload> </el-upload>
<div class="image-preview"> <div class="image-preview">
<div v-show="imageUrl.length>1" class="image-preview-wrapper"> <div v-show="imageUrl.length>1" class="image-preview-wrapper">
<img :src="imageUrl+'?imageView2/1/w/200/h/200'"> <img :src="imageUrl+'?imageView2/1/w/200/h/200'">
<div class="image-preview-action"> <div class="image-preview-action">
<i class="el-icon-delete" @click="rmImage"/> <i class="el-icon-delete" @click="rmImage" />
</div> </div>
</div> </div>
</div> </div>

View File

@ -7,15 +7,18 @@
:on-success="handleImageSuccess" :on-success="handleImageSuccess"
class="image-uploader" class="image-uploader"
drag drag
action="https://httpbin.org/post"> action="https://httpbin.org/post"
<i class="el-icon-upload"/> >
<div class="el-upload__text">Drag或<em>点击上传</em></div> <i class="el-icon-upload" />
<div class="el-upload__text">
Drag或<em>点击上传</em>
</div>
</el-upload> </el-upload>
<div v-show="imageUrl.length>0" class="image-preview"> <div v-show="imageUrl.length>0" class="image-preview">
<div v-show="imageUrl.length>1" class="image-preview-wrapper"> <div v-show="imageUrl.length>1" class="image-preview-wrapper">
<img :src="imageUrl"> <img :src="imageUrl">
<div class="image-preview-action"> <div class="image-preview-action">
<i class="el-icon-delete" @click="rmImage"/> <i class="el-icon-delete" @click="rmImage" />
</div> </div>
</div> </div>
</div> </div>

View File

@ -7,15 +7,18 @@
:on-success="handleImageSuccess" :on-success="handleImageSuccess"
class="image-uploader" class="image-uploader"
drag drag
action="https://httpbin.org/post"> action="https://httpbin.org/post"
<i class="el-icon-upload"/> >
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div> <i class="el-icon-upload" />
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
</el-upload> </el-upload>
<div class="image-preview image-app-preview"> <div class="image-preview image-app-preview">
<div v-show="imageUrl.length>1" class="image-preview-wrapper"> <div v-show="imageUrl.length>1" class="image-preview-wrapper">
<img :src="imageUrl"> <img :src="imageUrl">
<div class="image-preview-action"> <div class="image-preview-action">
<i class="el-icon-delete" @click="rmImage"/> <i class="el-icon-delete" @click="rmImage" />
</div> </div>
</div> </div>
</div> </div>
@ -23,7 +26,7 @@
<div v-show="imageUrl.length>1" class="image-preview-wrapper"> <div v-show="imageUrl.length>1" class="image-preview-wrapper">
<img :src="imageUrl"> <img :src="imageUrl">
<div class="image-preview-action"> <div class="image-preview-action">
<i class="el-icon-delete" @click="rmImage"/> <i class="el-icon-delete" @click="rmImage" />
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,7 +3,9 @@
<input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick"> <input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick">
<div class="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover"> <div class="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover">
Drop excel file here or Drop excel file here or
<el-button :loading="loading" style="margin-left:16px;" size="mini" type="primary" @click="handleUpload">Browse</el-button> <el-button :loading="loading" style="margin-left:16px;" size="mini" type="primary" @click="handleUpload">
Browse
</el-button>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,4 +1,4 @@
export default{ export default {
bind(el, binding, vnode) { bind(el, binding, vnode) {
const dialogHeaderEl = el.querySelector('.el-dialog__header') const dialogHeaderEl = el.querySelector('.el-dialog__header')
const dragDom = el.querySelector('.el-dialog') const dragDom = el.querySelector('.el-dialog')

View File

@ -0,0 +1,42 @@
import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event'
/**
* How to use
* <el-table height="100px" v-el-height-adaptive-table="{bottomOffset: 30}">...</el-table>
* el-table height is must be set
* bottomOffset: 30(default) // The height of the table from the bottom of the page.
*/
const doResize = (el, binding, vnode) => {
const { componentInstance: $table } = vnode
const { value } = binding
if (!$table.height) {
throw new Error(`el-$table must set the height. Such as height='100px'`)
}
const bottomOffset = (value && value.bottomOffset) || 30
if (!$table) return
const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset
$table.layout.setHeight(height)
$table.doLayout()
}
export default {
bind(el, binding, vnode) {
el.resizeListener = () => {
doResize(el, binding, vnode)
}
addResizeListener(el, el.resizeListener)
},
inserted(el, binding, vnode) {
doResize(el, binding, vnode)
},
unbind(el) {
removeResizeListener(el, el.resizeListener)
}
}

View File

@ -0,0 +1,14 @@
import adaptive from './adaptive'
const install = function(Vue) {
Vue.directive('el-height-adaptive-table', adaptive)
}
if (window.Vue) {
window['el-height-adaptive-table'] = adaptive
Vue.use(install); // eslint-disable-line
}
adaptive.install = install
export default adaptive

View File

@ -1,7 +1,7 @@
import store from '@/store' import store from '@/store'
export default{ export default {
inserted(el, binding, vnode) { inserted(el, binding, vnode) {
const { value } = binding const { value } = binding
const roles = store.getters && store.getters.roles const roles = store.getters && store.getters.roles

View File

@ -1,42 +1,72 @@
import './waves.css' import './waves.css'
export default{ const context = '@@wavesContext'
bind(el, binding) {
el.addEventListener('click', e => { function handleClick(el, binding) {
const customOpts = Object.assign({}, binding.value) function handle(e) {
const opts = Object.assign({ const customOpts = Object.assign({}, binding.value)
ele: el, // 波纹作用元素 const opts = Object.assign({
type: 'hit', // hit 点击位置扩散 center中心点扩展 ele: el, // 波纹作用元素
color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色 type: 'hit', // hit 点击位置扩散 center中心点扩展
}, customOpts) color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
const target = opts.ele },
if (target) { customOpts
target.style.position = 'relative' )
target.style.overflow = 'hidden' const target = opts.ele
const rect = target.getBoundingClientRect() if (target) {
let ripple = target.querySelector('.waves-ripple') target.style.position = 'relative'
if (!ripple) { target.style.overflow = 'hidden'
ripple = document.createElement('span') const rect = target.getBoundingClientRect()
ripple.className = 'waves-ripple' let ripple = target.querySelector('.waves-ripple')
ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px' if (!ripple) {
target.appendChild(ripple) ripple = document.createElement('span')
} else { ripple.className = 'waves-ripple'
ripple.className = 'waves-ripple' ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
} target.appendChild(ripple)
switch (opts.type) { } else {
case 'center': ripple.className = 'waves-ripple'
ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px'
ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px'
break
default:
ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop || document.body.scrollTop) + 'px'
ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft || document.body.scrollLeft) + 'px'
}
ripple.style.backgroundColor = opts.color
ripple.className = 'waves-ripple z-active'
return false
} }
}, false) switch (opts.type) {
case 'center':
ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'
ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'
break
default:
ripple.style.top =
(e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||
document.body.scrollTop) + 'px'
ripple.style.left =
(e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
document.body.scrollLeft) + 'px'
}
ripple.style.backgroundColor = opts.color
ripple.className = 'waves-ripple z-active'
return false
}
} }
if (!el[context]) {
el[context] = {
removeHandle: handle
}
} else {
el[context].removeHandle = handle
}
return handle
} }
export default {
bind(el, binding) {
el.addEventListener('click', handleClick(el, binding), false)
},
update(el, binding) {
el.removeEventListener('click', el[context].removeHandle, false)
el.addEventListener('click', handleClick(el, binding), false)
},
unbind(el) {
el.removeEventListener('click', el[context].removeHandle, false)
el[context] = null
delete el[context]
}
}

View File

@ -1,19 +0,0 @@
import Vue from 'vue'
import store from './store'
// you can set only in production env show the error-log
if (process.env.NODE_ENV === 'production') {
Vue.config.errorHandler = function(err, vm, info, a) {
// Don't ask me why I use Vue.nextTick, it just a hack.
// detail see https://forum.vuejs.org/t/dispatch-in-vue-config-errorhandler-has-some-problem/23500
Vue.nextTick(() => {
store.dispatch('addErrorLog', {
err,
vm,
info,
url: window.location.href
})
console.error(err, info)
})
}
}

View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M44.8 0h79.543C126.78 0 128 1.422 128 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H44.8c-2.438 0-3.657-1.422-3.657-4.267V4.267C41.143 1.422 42.362 0 44.8 0zm22.857 48h56.686c2.438 0 3.657 1.422 3.657 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H67.657C65.22 80 64 78.578 64 75.733V52.267C64 49.422 65.219 48 67.657 48zm0 48h56.686c2.438 0 3.657 1.422 3.657 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H67.657C65.22 128 64 126.578 64 123.733v-23.466C64 97.422 65.219 96 67.657 96zM50.286 68.267c2.02 0 3.657-1.91 3.657-4.267 0-2.356-1.638-4.267-3.657-4.267H17.37V32h6.4c2.02 0 3.658-1.91 3.658-4.267V4.267C27.429 1.91 25.79 0 23.77 0H3.657C1.637 0 0 1.91 0 4.267v23.466C0 30.09 1.637 32 3.657 32h6.4v80c0 2.356 1.638 4.267 3.657 4.267h36.572c2.02 0 3.657-1.91 3.657-4.267 0-2.356-1.638-4.267-3.657-4.267H17.37V68.267h32.915z"/></svg>

After

Width:  |  Height:  |  Size: 906 B

View File

@ -6,6 +6,7 @@ export default {
guide: 'Guide', guide: 'Guide',
permission: 'Permission', permission: 'Permission',
pagePermission: 'Page Permission', pagePermission: 'Page Permission',
rolePermission: 'Role Permission',
directivePermission: 'Directive Permission', directivePermission: 'Directive Permission',
icons: 'Icons', icons: 'Icons',
components: 'Components', components: 'Components',
@ -56,6 +57,7 @@ export default {
excel: 'Excel', excel: 'Excel',
exportExcel: 'Export Excel', exportExcel: 'Export Excel',
selectExcel: 'Export Selected', selectExcel: 'Export Selected',
mergeHeader: 'Merge Header',
uploadExcel: 'Upload Excel', uploadExcel: 'Upload Excel',
zip: 'Zip', zip: 'Zip',
pdf: 'PDF', pdf: 'PDF',
@ -86,9 +88,14 @@ export default {
github: 'Github Repository' github: 'Github Repository'
}, },
permission: { permission: {
addRole: 'New Role',
editPermission: 'Edit Permission',
roles: 'Your roles', roles: 'Your roles',
switchRoles: 'Switch roles', switchRoles: 'Switch roles',
tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.' tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.',
delete: 'Delete',
confirm: 'Confirm',
cancel: 'Cancel'
}, },
guide: { guide: {
description: 'The guide page is useful for some people who entered the project for the first time. You can briefly introduce the features of the project. Demo is based on ', description: 'The guide page is useful for some people who entered the project for the first time. You can briefly introduce the features of the project. Demo is based on ',

View File

@ -5,6 +5,7 @@ export default {
documentation: 'Documentación', documentation: 'Documentación',
guide: 'Guía', guide: 'Guía',
permission: 'Permisos', permission: 'Permisos',
rolePermission: 'Permisos de rol',
pagePermission: 'Permisos de la página', pagePermission: 'Permisos de la página',
directivePermission: 'Permisos de la directiva', directivePermission: 'Permisos de la directiva',
icons: 'Iconos', icons: 'Iconos',
@ -56,6 +57,7 @@ export default {
excel: 'Excel', excel: 'Excel',
exportExcel: 'Exportar a Excel', exportExcel: 'Exportar a Excel',
selectExcel: 'Export seleccionado', selectExcel: 'Export seleccionado',
mergeHeader: 'Merge Header',
uploadExcel: 'Subir Excel', uploadExcel: 'Subir Excel',
zip: 'Zip', zip: 'Zip',
pdf: 'PDF', pdf: 'PDF',
@ -86,9 +88,14 @@ export default {
github: 'Repositorio Github' github: 'Repositorio Github'
}, },
permission: { permission: {
addRole: 'Nuevo rol',
editPermission: 'Permiso de edición',
roles: 'Tus permisos', roles: 'Tus permisos',
switchRoles: 'Cambiar permisos', switchRoles: 'Cambiar permisos',
tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.' tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.',
delete: 'Borrar',
confirm: 'Confirmar',
cancel: 'Cancelar'
}, },
guide: { guide: {
description: 'The guide page is useful for some people who entered the project for the first time. You can briefly introduce the features of the project. Demo is based on ', description: 'The guide page is useful for some people who entered the project for the first time. You can briefly introduce the features of the project. Demo is based on ',

View File

@ -5,6 +5,7 @@ export default {
documentation: '文档', documentation: '文档',
guide: '引导页', guide: '引导页',
permission: '权限测试页', permission: '权限测试页',
rolePermission: '角色权限',
pagePermission: '页面权限', pagePermission: '页面权限',
directivePermission: '指令权限', directivePermission: '指令权限',
icons: '图标', icons: '图标',
@ -54,9 +55,10 @@ export default {
page404: '404', page404: '404',
errorLog: '错误日志', errorLog: '错误日志',
excel: 'Excel', excel: 'Excel',
exportExcel: 'Export Excel', exportExcel: '导出 Excel',
selectExcel: 'Export Selected', selectExcel: '导出 已选择项',
uploadExcel: 'Upload Excel', mergeHeader: '导出 多级表头',
uploadExcel: '上传 Excel',
zip: 'Zip', zip: 'Zip',
pdf: 'PDF', pdf: 'PDF',
exportZip: 'Export Zip', exportZip: 'Export Zip',
@ -86,9 +88,14 @@ export default {
github: 'Github 地址' github: 'Github 地址'
}, },
permission: { permission: {
addRole: '新增角色',
editPermission: '编辑权限',
roles: '你的权限', roles: '你的权限',
switchRoles: '切换权限', switchRoles: '切换权限',
tips: '在某些情况下,不适合使用 v-permission。例如Element-UI 的 Tab 组件或 el-table-column 以及其它动态渲染 dom 的场景。你只能通过手动设置 v-if 来实现。' tips: '在某些情况下,不适合使用 v-permission。例如Element-UI 的 Tab 组件或 el-table-column 以及其它动态渲染 dom 的场景。你只能通过手动设置 v-if 来实现。',
delete: '删除',
confirm: '确定',
cancel: '取消'
}, },
guide: { guide: {
description: '引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。本 Demo 是基于', description: '引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。本 Demo 是基于',

97
src/layout/Layout.vue Normal file
View File

@ -0,0 +1,97 @@
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<sidebar class="sidebar-container" />
<div :class="{hasTagsView:needTagsView}" class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<navbar />
<tags-view v-if="needTagsView" />
</div>
<app-main />
<right-panel v-if="showSettings">
<settings />
</right-panel>
</div>
</div>
</template>
<script>
import RightPanel from '@/components/RightPanel'
import { Navbar, Sidebar, AppMain, TagsView, Settings } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
export default {
name: 'Layout',
components: {
RightPanel,
Navbar,
Sidebar,
AppMain,
TagsView,
Settings
},
mixins: [ResizeMixin],
computed: {
...mapState({
sidebar: state => state.app.sidebar,
device: state => state.app.device,
showSettings: state => state.settings.showSettings,
needTagsView: state => state.settings.tagsView,
fixedHeader: state => 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 rel="stylesheet/scss" 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>

View File

@ -2,7 +2,7 @@
<section class="app-main"> <section class="app-main">
<transition name="fade-transform" mode="out-in"> <transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews"> <keep-alive :include="cachedViews">
<router-view :key="key"/> <router-view :key="key" />
</keep-alive> </keep-alive>
</transition> </transition>
</section> </section>
@ -22,13 +22,28 @@ export default {
} }
</script> </script>
<style scoped> <style rel="stylesheet/scss" lang="scss" scoped>
.app-main { .app-main {
/*84 = navbar + tags-view = 50 +34 */ /*50= navbar 50 */
min-height: calc(100vh - 84px); min-height: calc(100vh - 50px);
width: 100%; width: 100%;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.fixed-header+.app-main {
margin-top: 50px;
}
.hasTagsView {
.app-main {
/*84 = navbar + tags-view = 50 + 34 */
min-height: calc(100vh - 84px);
}
.fixed-header+.app-main {
margin-top: 80px;
}
}
</style> </style>

View File

@ -1,32 +1,29 @@
<template> <template>
<div class="navbar"> <div class="navbar">
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar"/> <hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<breadcrumb class="breadcrumb-container"/> <breadcrumb class="breadcrumb-container" />
<div class="right-menu"> <div class="right-menu">
<template v-if="device!=='mobile'"> <template v-if="device!=='mobile'">
<search class="right-menu-item" /> <search class="right-menu-item" />
<error-log class="errLog-container right-menu-item hover-effect"/> <error-log class="errLog-container right-menu-item hover-effect" />
<screenfull class="right-menu-item hover-effect"/> <screenfull class="right-menu-item hover-effect" />
<el-tooltip :content="$t('navbar.size')" effect="dark" placement="bottom"> <el-tooltip :content="$t('navbar.size')" effect="dark" placement="bottom">
<size-select class="right-menu-item hover-effect"/> <size-select class="right-menu-item hover-effect" />
</el-tooltip> </el-tooltip>
<lang-select class="right-menu-item hover-effect"/> <lang-select class="right-menu-item hover-effect" />
<el-tooltip :content="$t('navbar.theme')" effect="dark" placement="bottom">
<theme-picker class="right-menu-item hover-effect"/>
</el-tooltip>
</template> </template>
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click"> <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
<div class="avatar-wrapper"> <div class="avatar-wrapper">
<img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar"> <img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
<i class="el-icon-caret-bottom"/> <i class="el-icon-caret-bottom" />
</div> </div>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<router-link to="/"> <router-link to="/">
@ -56,7 +53,6 @@ import ErrorLog from '@/components/ErrorLog'
import Screenfull from '@/components/Screenfull' import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect' import SizeSelect from '@/components/SizeSelect'
import LangSelect from '@/components/LangSelect' import LangSelect from '@/components/LangSelect'
import ThemePicker from '@/components/ThemePicker'
import Search from '@/components/HeaderSearch' import Search from '@/components/HeaderSearch'
export default { export default {
@ -67,7 +63,6 @@ export default {
Screenfull, Screenfull,
SizeSelect, SizeSelect,
LangSelect, LangSelect,
ThemePicker,
Search Search
}, },
computed: { computed: {
@ -80,21 +75,29 @@ export default {
}, },
methods: { methods: {
toggleSideBar() { toggleSideBar() {
this.$store.dispatch('toggleSideBar') this.$store.dispatch('app/toggleSideBar')
}, },
logout() { async logout() {
this.$store.dispatch('LogOut').then(() => { await this.$store.dispatch('user/logout')
location.reload()// In order to re-instantiate the vue-router object to avoid bugs this.$router.push(`/login?redirect=${this.$route.fullPath}`)
})
} }
} }
} }
</script> </script>
<style rel="stylesheet/scss" lang="scss" scoped> <style rel="stylesheet/scss" lang="scss" scoped>
.hasTagsView {
.navbar {
border-bottom: none;
}
}
.navbar { .navbar {
height: 50px; height: 50px;
overflow: hidden; overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
.hamburger-container { .hamburger-container {
line-height: 46px; line-height: 46px;
@ -150,6 +153,7 @@ export default {
.avatar-wrapper { .avatar-wrapper {
margin-top: 5px; margin-top: 5px;
position: relative; position: relative;
.user-avatar { .user-avatar {
cursor: pointer; cursor: pointer;
width: 40px; width: 40px;

View File

@ -0,0 +1,88 @@
<template>
<div class="drawer-container">
<div>
<h3 class="drawer-title">
系统布局配置
</h3>
<div class="drawer-item">
<span>主题色</span>
<theme-picker style="float: right;height: 26px;margin: -3px 5px 0 0;" />
</div>
<div class="drawer-item">
<span>开启 Tags-View</span>
<el-switch v-model="tagsView" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>固定 Header</span>
<el-switch v-model="fixedHeader" class="drawer-switch" />
</div>
</div>
</div>
</template>
<script>
import ThemePicker from '@/components/ThemePicker'
export default {
components: { ThemePicker },
data() {
return {
sidebarLogo: true
}
},
computed: {
fixedHeader: {
get() {
return this.$store.state.settings.fixedHeader
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'fixedHeader',
value: val
})
}
},
tagsView: {
get() {
return this.$store.state.settings.tagsView
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'tagsView',
value: val
})
}
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.drawer-container {
padding: 24px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
.drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, .85);
font-size: 14px;
line-height: 22px;
}
.drawer-item {
color: rgba(0, 0, 0, .65);
font-size: 14px;
padding: 12px 0;
}
.drawer-switch {
float: right
}
}
</style>

View File

@ -2,7 +2,7 @@
<template> <template>
<!-- eslint-disable vue/require-component-is --> <!-- eslint-disable vue/require-component-is -->
<component v-bind="linkProps(to)"> <component v-bind="linkProps(to)">
<slot/> <slot />
</component> </component>
</template> </template>

View File

@ -1,6 +1,5 @@
<template> <template>
<div v-if="!item.hidden" class="menu-wrapper"> <div v-if="!item.hidden" class="menu-wrapper">
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow"> <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
<app-link :to="resolvePath(onlyOneChild.path)"> <app-link :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}"> <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
@ -9,19 +8,19 @@
</app-link> </app-link>
</template> </template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)"> <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title"> <template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="generateTitle(item.meta.title)" /> <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="generateTitle(item.meta.title)" />
</template> </template>
<sidebar-item <sidebar-item
v-for="child in item.children" v-for="child in item.children"
:key="child.path"
:is-nest="true" :is-nest="true"
:item="child" :item="child"
:key="child.path"
:base-path="resolvePath(child.path)" :base-path="resolvePath(child.path)"
class="nest-menu" /> class="nest-menu"
/>
</el-submenu> </el-submenu>
</div> </div>
</template> </template>

View File

@ -6,9 +6,10 @@
:background-color="variables.menuBg" :background-color="variables.menuBg"
:text-color="variables.menuText" :text-color="variables.menuText"
:active-text-color="variables.menuActiveText" :active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical" mode="vertical"
> >
<sidebar-item v-for="route in permission_routers" :key="route.path" :item="route" :base-path="route.path"/> <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu> </el-menu>
</el-scrollbar> </el-scrollbar>
</template> </template>
@ -22,7 +23,7 @@ export default {
components: { SidebarItem }, components: { SidebarItem },
computed: { computed: {
...mapGetters([ ...mapGetters([
'permission_routers', 'permission_routes',
'sidebar' 'sidebar'
]), ]),
variables() { variables() {

View File

@ -1,6 +1,6 @@
<template> <template>
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll"> <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
<slot/> <slot />
</el-scrollbar> </el-scrollbar>
</template> </template>

View File

@ -4,23 +4,32 @@
<router-link <router-link
v-for="tag in visitedViews" v-for="tag in visitedViews"
ref="tag" ref="tag"
:key="tag.path"
:class="isActive(tag)?'active':''" :class="isActive(tag)?'active':''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
:key="tag.path"
tag="span" tag="span"
class="tags-view-item" class="tags-view-item"
@click.middle.native="closeSelectedTag(tag)" @click.middle.native="closeSelectedTag(tag)"
@contextmenu.prevent.native="openMenu(tag,$event)"> @contextmenu.prevent.native="openMenu(tag,$event)"
>
{{ generateTitle(tag.title) }} {{ generateTitle(tag.title) }}
<span v-if="!tag.meta.affix" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" /> <span v-if="!tag.meta.affix" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
</router-link> </router-link>
</scroll-pane> </scroll-pane>
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu"> <ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)">{{ $t('tagsView.refresh') }}</li> <li @click="refreshSelectedTag(selectedTag)">
<li v-if="!(selectedTag.meta&&selectedTag.meta.affix)" @click="closeSelectedTag(selectedTag)">{{ {{ $t('tagsView.refresh') }}
$t('tagsView.close') }}</li> </li>
<li @click="closeOthersTags">{{ $t('tagsView.closeOthers') }}</li> <li v-if="!(selectedTag.meta&&selectedTag.meta.affix)" @click="closeSelectedTag(selectedTag)">
<li @click="closeAllTags(selectedTag)">{{ $t('tagsView.closeAll') }}</li> {{
$t('tagsView.close') }}
</li>
<li @click="closeOthersTags">
{{ $t('tagsView.closeOthers') }}
</li>
<li @click="closeAllTags(selectedTag)">
{{ $t('tagsView.closeAll') }}
</li>
</ul> </ul>
</div> </div>
</template> </template>
@ -45,8 +54,8 @@ export default {
visitedViews() { visitedViews() {
return this.$store.state.tagsView.visitedViews return this.$store.state.tagsView.visitedViews
}, },
routers() { routes() {
return this.$store.state.permission.routers return this.$store.state.permission.routes
} }
}, },
watch: { watch: {
@ -93,18 +102,18 @@ export default {
return tags return tags
}, },
initTags() { initTags() {
const affixTags = this.affixTags = this.filterAffixTags(this.routers) const affixTags = this.affixTags = this.filterAffixTags(this.routes)
for (const tag of affixTags) { for (const tag of affixTags) {
// Must have tag name // Must have tag name
if (tag.name) { if (tag.name) {
this.$store.dispatch('addVisitedView', tag) this.$store.dispatch('tagsView/addVisitedView', tag)
} }
} }
}, },
addTags() { addTags() {
const { name } = this.$route const { name } = this.$route
if (name) { if (name) {
this.$store.dispatch('addView', this.$route) this.$store.dispatch('tagsView/addView', this.$route)
} }
return false return false
}, },
@ -116,7 +125,7 @@ export default {
this.$refs.scrollPane.moveToTarget(tag) this.$refs.scrollPane.moveToTarget(tag)
// when query is different then update // when query is different then update
if (tag.to.fullPath !== this.$route.fullPath) { if (tag.to.fullPath !== this.$route.fullPath) {
this.$store.dispatch('updateVisitedView', this.$route) this.$store.dispatch('tagsView/updateVisitedView', this.$route)
} }
break break
} }
@ -124,7 +133,7 @@ export default {
}) })
}, },
refreshSelectedTag(view) { refreshSelectedTag(view) {
this.$store.dispatch('delCachedView', view).then(() => { this.$store.dispatch('tagsView/delCachedView', view).then(() => {
const { fullPath } = view const { fullPath } = view
this.$nextTick(() => { this.$nextTick(() => {
this.$router.replace({ this.$router.replace({
@ -134,7 +143,7 @@ export default {
}) })
}, },
closeSelectedTag(view) { closeSelectedTag(view) {
this.$store.dispatch('delView', view).then(({ visitedViews }) => { this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
if (this.isActive(view)) { if (this.isActive(view)) {
this.toLastView(visitedViews) this.toLastView(visitedViews)
} }
@ -142,12 +151,12 @@ export default {
}, },
closeOthersTags() { closeOthersTags() {
this.$router.push(this.selectedTag) this.$router.push(this.selectedTag)
this.$store.dispatch('delOthersViews', this.selectedTag).then(() => { this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
this.moveToCurrentTag() this.moveToCurrentTag()
}) })
}, },
closeAllTags(view) { closeAllTags(view) {
this.$store.dispatch('delAllViews').then(({ visitedViews }) => { this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
if (this.affixTags.some(tag => tag.path === view.path)) { if (this.affixTags.some(tag => tag.path === view.path)) {
return return
} }

View File

@ -2,3 +2,4 @@ export { default as Navbar } from './Navbar'
export { default as Sidebar } from './Sidebar/index.vue' export { default as Sidebar } from './Sidebar/index.vue'
export { default as TagsView } from './TagsView/index.vue' export { default as TagsView } from './TagsView/index.vue'
export { default as AppMain } from './AppMain' export { default as AppMain } from './AppMain'
export { default as Settings } from './Settings'

View File

@ -7,7 +7,7 @@ export default {
watch: { watch: {
$route(route) { $route(route) {
if (this.device === 'mobile' && this.sidebar.opened) { if (this.device === 'mobile' && this.sidebar.opened) {
store.dispatch('closeSideBar', { withoutAnimation: false }) store.dispatch('app/closeSideBar', { withoutAnimation: false })
} }
} }
}, },
@ -17,8 +17,8 @@ export default {
mounted() { mounted() {
const isMobile = this.isMobile() const isMobile = this.isMobile()
if (isMobile) { if (isMobile) {
store.dispatch('toggleDevice', 'mobile') store.dispatch('app/toggleDevice', 'mobile')
store.dispatch('closeSideBar', { withoutAnimation: true }) store.dispatch('app/closeSideBar', { withoutAnimation: true })
} }
}, },
methods: { methods: {
@ -29,10 +29,10 @@ export default {
resizeHandler() { resizeHandler() {
if (!document.hidden) { if (!document.hidden) {
const isMobile = this.isMobile() const isMobile = this.isMobile()
store.dispatch('toggleDevice', isMobile ? 'mobile' : 'desktop') store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
if (isMobile) { if (isMobile) {
store.dispatch('closeSideBar', { withoutAnimation: true }) store.dispatch('app/closeSideBar', { withoutAnimation: true })
} }
} }
} }

View File

@ -5,7 +5,7 @@ import Cookies from 'js-cookie'
import 'normalize.css/normalize.css' // A modern alternative to CSS resets import 'normalize.css/normalize.css' // A modern alternative to CSS resets
import Element from 'element-ui' import Element from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css' import './styles/element-variables.scss'
import '@/styles/index.scss' // global css import '@/styles/index.scss' // global css
@ -15,8 +15,8 @@ import router from './router'
import i18n from './lang' // Internationalization import i18n from './lang' // Internationalization
import './icons' // icon import './icons' // icon
import './errorLog' // error log
import './permission' // permission control import './permission' // permission control
import './utils/errorLog' // error log
import * as filters from './filters' // global filters import * as filters from './filters' // global filters

View File

@ -2,62 +2,69 @@ import router from './router'
import store from './store' import store from './store'
import { Message } from 'element-ui' import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // getToken from cookie import { getToken } from '@/utils/auth' // get token from cookie
NProgress.configure({ showSpinner: false })// NProgress Configuration NProgress.configure({ showSpinner: false }) // NProgress Configuration
// permission judge function const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist
function hasPermission(roles, permissionRoles) {
if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
if (!permissionRoles) return true
return roles.some(role => permissionRoles.indexOf(role) >= 0)
}
const whiteList = ['/login', '/auth-redirect']// no redirect whitelist router.beforeEach(async(to, from, next) => {
// start progress bar
NProgress.start()
router.beforeEach((to, from, next) => { // determine whether the user has logged in
NProgress.start() // start progress bar const hasToken = getToken()
if (getToken()) { // determine if there has token
/* has token*/ if (hasToken) {
if (to.path === '/login') { if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' }) next({ path: '/' })
NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it NProgress.done()
} else { } else {
if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息 // determine whether the user has obtained his permission roles through getInfo
store.dispatch('GetUserInfo').then(res => { // 拉取user_info const hasRoles = store.getters.roles && store.getters.roles.length > 0
const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop'] if (hasRoles) {
store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表 next()
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
}).catch((err) => {
store.dispatch('FedLogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
})
} else { } else {
// 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓ try {
if (hasPermission(store.getters.roles, to.meta.roles)) { // get user info
next() // note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
} else { const { roles } = await store.dispatch('user/getInfo')
next({ path: '/401', replace: true, query: { noGoBack: true }})
// generate accessible routes map based on roles
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
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}`)
NProgress.done()
} }
// 可删 ↑
} }
} }
} else { } else {
/* has no token*/ /* has no token*/
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next() next()
} else { } else {
next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页 // other pages that do not have permission to access are redirected to the login page.
NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it next(`/login?redirect=${to.path}`)
NProgress.done()
} }
} }
}) })
router.afterEach(() => { router.afterEach(() => {
NProgress.done() // finish progress bar // finish progress bar
NProgress.done()
}) })

View File

@ -4,12 +4,13 @@ import Router from 'vue-router'
Vue.use(Router) Vue.use(Router)
/* Layout */ /* Layout */
import Layout from '@/views/layout/Layout' import Layout from '@/layout/Layout'
/* Router Modules */ /* Router Modules */
import componentsRouter from './modules/components' import componentsRouter from './modules/components'
import chartsRouter from './modules/charts' import chartsRouter from './modules/charts'
import tableRouter from './modules/table' import tableRouter from './modules/table'
import treeTableRouter from './modules/tree-table'
import nestedRouter from './modules/nested' import nestedRouter from './modules/nested'
/** note: sub-menu only appear when children.length>=1 /** note: sub-menu only appear when children.length>=1
@ -32,7 +33,7 @@ import nestedRouter from './modules/nested'
affix: true if true, the tag will affix in the tags-view affix: true if true, the tag will affix in the tags-view
} }
**/ **/
export const constantRouterMap = [ export const constantRoutes = [
{ {
path: '/redirect', path: '/redirect',
component: Layout, component: Layout,
@ -80,7 +81,6 @@ export const constantRouterMap = [
{ {
path: '/documentation', path: '/documentation',
component: Layout, component: Layout,
redirect: '/documentation/index',
children: [ children: [
{ {
path: 'index', path: 'index',
@ -105,13 +105,7 @@ export const constantRouterMap = [
} }
] ]
export default new Router({ export const asyncRoutes = [
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap
})
export const asyncRouterMap = [
{ {
path: '/permission', path: '/permission',
component: Layout, component: Layout,
@ -140,6 +134,15 @@ export const asyncRouterMap = [
title: 'directivePermission' title: 'directivePermission'
// if do not set roles, means: this page does not require permission // if do not set roles, means: this page does not require permission
} }
},
{
path: 'role',
component: () => import('@/views/permission/role'),
name: 'RolePermission',
meta: {
title: 'rolePermission',
roles: ['admin']
}
} }
] ]
}, },
@ -162,6 +165,7 @@ export const asyncRouterMap = [
chartsRouter, chartsRouter,
nestedRouter, nestedRouter,
tableRouter, tableRouter,
treeTableRouter,
{ {
path: '/example', path: '/example',
@ -269,6 +273,12 @@ export const asyncRouterMap = [
name: 'SelectExcel', name: 'SelectExcel',
meta: { title: 'selectExcel' } meta: { title: 'selectExcel' }
}, },
{
path: 'export-merge-header',
component: () => import('@/views/excel/mergeHeader'),
name: 'MergeHeader',
meta: { title: 'mergeHeader' }
},
{ {
path: 'upload-excel', path: 'upload-excel',
component: () => import('@/views/excel/uploadExcel'), component: () => import('@/views/excel/uploadExcel'),
@ -367,3 +377,19 @@ export const asyncRouterMap = [
{ path: '*', redirect: '/404', hidden: true } { path: '*', redirect: '/404', hidden: true }
] ]
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
const router = createRouter()
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router

View File

@ -1,6 +1,6 @@
/** When your routing table is too long, you can split it into small modules**/ /** When your routing table is too long, you can split it into small modules**/
import Layout from '@/views/layout/Layout' import Layout from '@/layout/Layout'
const chartsRouter = { const chartsRouter = {
path: '/charts', path: '/charts',

View File

@ -1,6 +1,6 @@
/** When your routing table is too long, you can split it into small modules**/ /** When your routing table is too long, you can split it into small modules**/
import Layout from '@/views/layout/Layout' import Layout from '@/layout/Layout'
const componentsRouter = { const componentsRouter = {
path: '/components', path: '/components',

View File

@ -1,6 +1,6 @@
/** When your routing table is too long, you can split it into small modules**/ /** When your routing table is too long, you can split it into small modules**/
import Layout from '@/views/layout/Layout' import Layout from '@/layout/Layout'
const nestedRouter = { const nestedRouter = {
path: '/nested', path: '/nested',

View File

@ -1,6 +1,6 @@
/** When your routing table is too long, you can split it into small modules**/ /** When your routing table is too long, you can split it into small modules**/
import Layout from '@/views/layout/Layout' import Layout from '@/layout/Layout'
const tableRouter = { const tableRouter = {
path: '/table', path: '/table',
@ -30,18 +30,6 @@ const tableRouter = {
name: 'InlineEditTable', name: 'InlineEditTable',
meta: { title: 'inlineEditTable' } meta: { title: 'inlineEditTable' }
}, },
{
path: 'tree-table',
component: () => import('@/views/table/treeTable/treeTable'),
name: 'TreeTableDemo',
meta: { title: 'treeTable' }
},
{
path: 'custom-tree-table',
component: () => import('@/views/table/treeTable/customTreeTable'),
name: 'CustomTreeTableDemo',
meta: { title: 'customTreeTable' }
},
{ {
path: 'complex-table', path: 'complex-table',
component: () => import('@/views/table/complexTable'), component: () => import('@/views/table/complexTable'),

View File

@ -0,0 +1,29 @@
/** When your routing table is too long, you can split it into small modules**/
import Layout from '@/layout/Layout'
const treeTableRouter = {
path: '/tree-table',
component: Layout,
redirect: '/table/complex-table',
name: 'TreeTable',
meta: {
title: 'treeTable',
icon: 'tree-table'
},
children: [
{
path: 'index',
component: () => import('@/views/tree-table/index'),
name: 'TreeTableDemo',
meta: { title: 'treeTable' }
},
{
path: 'custom',
component: () => import('@/views/tree-table/custom'),
name: 'CustomTreeTableDemo',
meta: { title: 'customTreeTable' }
}
]
}
export default treeTableRouter

29
src/settings.js Normal file
View File

@ -0,0 +1,29 @@
export default {
title: 'vue-element-admin',
/**
* @type {boolean} true | false
* @description Whether show the settings right-panel
*/
showSettings: true,
/**
* @type {boolean} true | false
* @description Whether need tagsView
*/
tagsView: true,
/**
* @type {boolean} true | false
* @description Whether fix the header
*/
fixedHeader: true,
/**
* @type {string | array} 'production' | ['production','development']
* @description Need show err logs component.
* The default is only used in the production env
* If you want to also use it in dev, you can pass ['production','development']
*/
errorLog: 'production'
}

View File

@ -9,11 +9,9 @@ const getters = {
avatar: state => state.user.avatar, avatar: state => state.user.avatar,
name: state => state.user.name, name: state => state.user.name,
introduction: state => state.user.introduction, introduction: state => state.user.introduction,
status: state => state.user.status,
roles: state => state.user.roles, roles: state => state.user.roles,
setting: state => state.user.setting, permission_routes: state => state.permission.routes,
permission_routers: state => state.permission.routers, addRoutes: state => state.permission.addRoutes,
addRouters: state => state.permission.addRouters,
errorLogs: state => state.errorLog.logs errorLogs: state => state.errorLog.logs
} }
export default getters export default getters

View File

@ -1,59 +1,64 @@
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import { getLanguage } from '@/lang/index' import { getLanguage } from '@/lang/index'
const app = { const state = {
state: { sidebar: {
sidebar: { opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, withoutAnimation: false
withoutAnimation: false
},
device: 'desktop',
language: getLanguage(),
size: Cookies.get('size') || 'medium'
}, },
mutations: { device: 'desktop',
TOGGLE_SIDEBAR: state => { language: getLanguage(),
state.sidebar.opened = !state.sidebar.opened size: Cookies.get('size') || 'medium'
state.sidebar.withoutAnimation = false }
if (state.sidebar.opened) {
Cookies.set('sidebarStatus', 1) const mutations = {
} else { TOGGLE_SIDEBAR: state => {
Cookies.set('sidebarStatus', 0) state.sidebar.opened = !state.sidebar.opened
} state.sidebar.withoutAnimation = false
}, if (state.sidebar.opened) {
CLOSE_SIDEBAR: (state, withoutAnimation) => { Cookies.set('sidebarStatus', 1)
} else {
Cookies.set('sidebarStatus', 0) Cookies.set('sidebarStatus', 0)
state.sidebar.opened = false
state.sidebar.withoutAnimation = withoutAnimation
},
TOGGLE_DEVICE: (state, device) => {
state.device = device
},
SET_LANGUAGE: (state, language) => {
state.language = language
Cookies.set('language', language)
},
SET_SIZE: (state, size) => {
state.size = size
Cookies.set('size', size)
} }
}, },
actions: { CLOSE_SIDEBAR: (state, withoutAnimation) => {
toggleSideBar({ commit }) { Cookies.set('sidebarStatus', 0)
commit('TOGGLE_SIDEBAR') state.sidebar.opened = false
}, state.sidebar.withoutAnimation = withoutAnimation
closeSideBar({ commit }, { withoutAnimation }) { },
commit('CLOSE_SIDEBAR', withoutAnimation) TOGGLE_DEVICE: (state, device) => {
}, state.device = device
toggleDevice({ commit }, device) { },
commit('TOGGLE_DEVICE', device) SET_LANGUAGE: (state, language) => {
}, state.language = language
setLanguage({ commit }, language) { Cookies.set('language', language)
commit('SET_LANGUAGE', language) },
}, SET_SIZE: (state, size) => {
setSize({ commit }, size) { state.size = size
commit('SET_SIZE', size) Cookies.set('size', size)
}
} }
} }
export default app const actions = {
toggleSideBar({ commit }) {
commit('TOGGLE_SIDEBAR')
},
closeSideBar({ commit }, { withoutAnimation }) {
commit('CLOSE_SIDEBAR', withoutAnimation)
},
toggleDevice({ commit }, device) {
commit('TOGGLE_DEVICE', device)
},
setLanguage({ commit }, language) {
commit('SET_LANGUAGE', language)
},
setSize({ commit }, size) {
commit('SET_SIZE', size)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@ -1,17 +1,23 @@
const errorLog = {
state: { const state = {
logs: [] logs: []
}, }
mutations: {
ADD_ERROR_LOG: (state, log) => { const mutations = {
state.logs.push(log) ADD_ERROR_LOG: (state, log) => {
} state.logs.push(log)
},
actions: {
addErrorLog({ commit }, log) {
commit('ADD_ERROR_LOG', log)
}
} }
} }
export default errorLog const actions = {
addErrorLog({ commit }, log) {
commit('ADD_ERROR_LOG', log)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@ -1,4 +1,4 @@
import { asyncRouterMap, constantRouterMap } from '@/router' import { asyncRoutes, constantRoutes } from '@/router'
/** /**
* 通过meta.role判断是否与当前用户权限匹配 * 通过meta.role判断是否与当前用户权限匹配
@ -15,17 +15,17 @@ function hasPermission(roles, route) {
/** /**
* 递归过滤异步路由表返回符合用户角色权限的路由表 * 递归过滤异步路由表返回符合用户角色权限的路由表
* @param routes asyncRouterMap * @param routes asyncRoutes
* @param roles * @param roles
*/ */
function filterAsyncRouter(routes, roles) { export function filterAsyncRoutes(routes, roles) {
const res = [] const res = []
routes.forEach(route => { routes.forEach(route => {
const tmp = { ...route } const tmp = { ...route }
if (hasPermission(roles, tmp)) { if (hasPermission(roles, tmp)) {
if (tmp.children) { if (tmp.children) {
tmp.children = filterAsyncRouter(tmp.children, roles) tmp.children = filterAsyncRoutes(tmp.children, roles)
} }
res.push(tmp) res.push(tmp)
} }
@ -34,32 +34,36 @@ function filterAsyncRouter(routes, roles) {
return res return res
} }
const permission = { const state = {
state: { routes: [],
routers: [], addRoutes: []
addRouters: [] }
},
mutations: { const mutations = {
SET_ROUTERS: (state, routers) => { SET_ROUTES: (state, routes) => {
state.addRouters = routers state.addRoutes = routes
state.routers = constantRouterMap.concat(routers) state.routes = constantRoutes.concat(routes)
}
},
actions: {
GenerateRoutes({ commit }, data) {
return new Promise(resolve => {
const { roles } = data
let accessedRouters
if (roles.includes('admin')) {
accessedRouters = asyncRouterMap
} else {
accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
}
commit('SET_ROUTERS', accessedRouters)
resolve()
})
}
} }
} }
export default permission const actions = {
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes
if (roles.includes('admin')) {
accessedRoutes = asyncRoutes
} else {
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
}
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@ -0,0 +1,30 @@
import defaultSettings from '@/settings'
const { showSettings, tagsView, fixedHeader } = defaultSettings
const state = {
showSettings: showSettings,
tagsView: tagsView,
fixedHeader: fixedHeader
}
const mutations = {
CHANGE_SETTING: (state, { key, value }) => {
if (state.hasOwnProperty(key)) {
state[key] = value
}
}
}
const actions = {
changeSetting({ commit }, data) {
commit('CHANGE_SETTING', data)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@ -1,161 +1,166 @@
const tagsView = {
state: {
visitedViews: [],
cachedViews: []
},
mutations: {
ADD_VISITED_VIEW: (state, view) => {
if (state.visitedViews.some(v => v.path === view.path)) return
state.visitedViews.push(
Object.assign({}, view, {
title: view.meta.title || 'no-name'
})
)
},
ADD_CACHED_VIEW: (state, view) => {
if (state.cachedViews.includes(view.name)) return
if (!view.meta.noCache) {
state.cachedViews.push(view.name)
}
},
DEL_VISITED_VIEW: (state, view) => { const state = {
for (const [i, v] of state.visitedViews.entries()) { visitedViews: [],
if (v.path === view.path) { cachedViews: []
state.visitedViews.splice(i, 1) }
break
}
}
},
DEL_CACHED_VIEW: (state, view) => {
for (const i of state.cachedViews) {
if (i === view.name) {
const index = state.cachedViews.indexOf(i)
state.cachedViews.splice(index, 1)
break
}
}
},
DEL_OTHERS_VISITED_VIEWS: (state, view) => { const mutations = {
state.visitedViews = state.visitedViews.filter(v => { ADD_VISITED_VIEW: (state, view) => {
return v.meta.affix || v.path === view.path if (state.visitedViews.some(v => v.path === view.path)) return
state.visitedViews.push(
Object.assign({}, view, {
title: view.meta.title || 'no-name'
}) })
}, )
DEL_OTHERS_CACHED_VIEWS: (state, view) => { },
for (const i of state.cachedViews) { ADD_CACHED_VIEW: (state, view) => {
if (i === view.name) { if (state.cachedViews.includes(view.name)) return
const index = state.cachedViews.indexOf(i) if (!view.meta.noCache) {
state.cachedViews = state.cachedViews.slice(index, index + 1) state.cachedViews.push(view.name)
break }
} },
}
},
DEL_ALL_VISITED_VIEWS: state => { DEL_VISITED_VIEW: (state, view) => {
// keep affix tags for (const [i, v] of state.visitedViews.entries()) {
const affixTags = state.visitedViews.filter(tag => tag.meta.affix) if (v.path === view.path) {
state.visitedViews = affixTags state.visitedViews.splice(i, 1)
}, break
DEL_ALL_CACHED_VIEWS: state => { }
state.cachedViews = [] }
}, },
DEL_CACHED_VIEW: (state, view) => {
UPDATE_VISITED_VIEW: (state, view) => { for (const i of state.cachedViews) {
for (let v of state.visitedViews) { if (i === view.name) {
if (v.path === view.path) { const index = state.cachedViews.indexOf(i)
v = Object.assign(v, view) state.cachedViews.splice(index, 1)
break break
}
} }
} }
}, },
actions: {
addView({ dispatch }, view) {
dispatch('addVisitedView', view)
dispatch('addCachedView', view)
},
addVisitedView({ commit }, view) {
commit('ADD_VISITED_VIEW', view)
},
addCachedView({ commit }, view) {
commit('ADD_CACHED_VIEW', view)
},
delView({ dispatch, state }, view) { DEL_OTHERS_VISITED_VIEWS: (state, view) => {
return new Promise(resolve => { state.visitedViews = state.visitedViews.filter(v => {
dispatch('delVisitedView', view) return v.meta.affix || v.path === view.path
dispatch('delCachedView', view) })
resolve({ },
visitedViews: [...state.visitedViews], DEL_OTHERS_CACHED_VIEWS: (state, view) => {
cachedViews: [...state.cachedViews] for (const i of state.cachedViews) {
}) if (i === view.name) {
}) const index = state.cachedViews.indexOf(i)
}, state.cachedViews = state.cachedViews.slice(index, index + 1)
delVisitedView({ commit, state }, view) { break
return new Promise(resolve => { }
commit('DEL_VISITED_VIEW', view) }
resolve([...state.visitedViews]) },
})
},
delCachedView({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_CACHED_VIEW', view)
resolve([...state.cachedViews])
})
},
delOthersViews({ dispatch, state }, view) { DEL_ALL_VISITED_VIEWS: state => {
return new Promise(resolve => { // keep affix tags
dispatch('delOthersVisitedViews', view) const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
dispatch('delOthersCachedViews', view) state.visitedViews = affixTags
resolve({ },
visitedViews: [...state.visitedViews], DEL_ALL_CACHED_VIEWS: state => {
cachedViews: [...state.cachedViews] state.cachedViews = []
}) },
})
},
delOthersVisitedViews({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_OTHERS_VISITED_VIEWS', view)
resolve([...state.visitedViews])
})
},
delOthersCachedViews({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_OTHERS_CACHED_VIEWS', view)
resolve([...state.cachedViews])
})
},
delAllViews({ dispatch, state }, view) { UPDATE_VISITED_VIEW: (state, view) => {
return new Promise(resolve => { for (let v of state.visitedViews) {
dispatch('delAllVisitedViews', view) if (v.path === view.path) {
dispatch('delAllCachedViews', view) v = Object.assign(v, view)
resolve({ break
visitedViews: [...state.visitedViews], }
cachedViews: [...state.cachedViews]
})
})
},
delAllVisitedViews({ commit, state }) {
return new Promise(resolve => {
commit('DEL_ALL_VISITED_VIEWS')
resolve([...state.visitedViews])
})
},
delAllCachedViews({ commit, state }) {
return new Promise(resolve => {
commit('DEL_ALL_CACHED_VIEWS')
resolve([...state.cachedViews])
})
},
updateVisitedView({ commit }, view) {
commit('UPDATE_VISITED_VIEW', view)
} }
} }
} }
export default tagsView const actions = {
addView({ dispatch }, view) {
dispatch('addVisitedView', view)
dispatch('addCachedView', view)
},
addVisitedView({ commit }, view) {
commit('ADD_VISITED_VIEW', view)
},
addCachedView({ commit }, view) {
commit('ADD_CACHED_VIEW', view)
},
delView({ dispatch, state }, view) {
return new Promise(resolve => {
dispatch('delVisitedView', view)
dispatch('delCachedView', view)
resolve({
visitedViews: [...state.visitedViews],
cachedViews: [...state.cachedViews]
})
})
},
delVisitedView({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_VISITED_VIEW', view)
resolve([...state.visitedViews])
})
},
delCachedView({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_CACHED_VIEW', view)
resolve([...state.cachedViews])
})
},
delOthersViews({ dispatch, state }, view) {
return new Promise(resolve => {
dispatch('delOthersVisitedViews', view)
dispatch('delOthersCachedViews', view)
resolve({
visitedViews: [...state.visitedViews],
cachedViews: [...state.cachedViews]
})
})
},
delOthersVisitedViews({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_OTHERS_VISITED_VIEWS', view)
resolve([...state.visitedViews])
})
},
delOthersCachedViews({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_OTHERS_CACHED_VIEWS', view)
resolve([...state.cachedViews])
})
},
delAllViews({ dispatch, state }, view) {
return new Promise(resolve => {
dispatch('delAllVisitedViews', view)
dispatch('delAllCachedViews', view)
resolve({
visitedViews: [...state.visitedViews],
cachedViews: [...state.cachedViews]
})
})
},
delAllVisitedViews({ commit, state }) {
return new Promise(resolve => {
commit('DEL_ALL_VISITED_VIEWS')
resolve([...state.visitedViews])
})
},
delAllCachedViews({ commit, state }) {
return new Promise(resolve => {
commit('DEL_ALL_CACHED_VIEWS')
resolve([...state.cachedViews])
})
},
updateVisitedView({ commit }, view) {
commit('UPDATE_VISITED_VIEW', view)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@ -1,144 +1,128 @@
import { loginByUsername, logout, getUserInfo } from '@/api/login' import { login, logout, getInfo } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth' import { getToken, setToken, removeToken } from '@/utils/auth'
import router, { resetRouter } from '@/router'
const user = { const state = {
state: { token: getToken(),
user: '', name: '',
status: '', avatar: '',
code: '', introduction: '',
token: getToken(), roles: []
name: '', }
avatar: '',
introduction: '', const mutations = {
roles: [], SET_TOKEN: (state, token) => {
setting: { state.token = token
articlePlatform: []
}
}, },
SET_INTRODUCTION: (state, introduction) => {
mutations: { state.introduction = introduction
SET_CODE: (state, code) => {
state.code = code
},
SET_TOKEN: (state, token) => {
state.token = token
},
SET_INTRODUCTION: (state, introduction) => {
state.introduction = introduction
},
SET_SETTING: (state, setting) => {
state.setting = setting
},
SET_STATUS: (state, status) => {
state.status = status
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_ROLES: (state, roles) => {
state.roles = roles
}
}, },
SET_NAME: (state, name) => {
actions: { state.name = name
// 用户名登录 },
LoginByUsername({ commit }, userInfo) { SET_AVATAR: (state, avatar) => {
const username = userInfo.username.trim() state.avatar = avatar
return new Promise((resolve, reject) => { },
loginByUsername(username, userInfo.password).then(response => { SET_ROLES: (state, roles) => {
const data = response.data state.roles = roles
commit('SET_TOKEN', data.token)
setToken(response.data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 获取用户信息
GetUserInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getUserInfo(state.token).then(response => {
// 由于mockjs 不支持自定义状态码只能这样hack
if (!response.data) {
reject('Verification failed, please login again.')
}
const data = response.data
if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', data.roles)
} else {
reject('getInfo: roles must be a non-null array!')
}
commit('SET_NAME', data.name)
commit('SET_AVATAR', data.avatar)
commit('SET_INTRODUCTION', data.introduction)
resolve(response)
}).catch(error => {
reject(error)
})
})
},
// 第三方验证登录
// LoginByThirdparty({ commit, state }, code) {
// return new Promise((resolve, reject) => {
// commit('SET_CODE', code)
// loginByThirdparty(state.status, state.email, state.code).then(response => {
// commit('SET_TOKEN', response.data.token)
// setToken(response.data.token)
// resolve()
// }).catch(error => {
// reject(error)
// })
// })
// },
// 登出
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resolve()
}).catch(error => {
reject(error)
})
})
},
// 前端 登出
FedLogOut({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
})
},
// 动态修改权限
ChangeRoles({ commit, dispatch }, role) {
return new Promise(resolve => {
commit('SET_TOKEN', role)
setToken(role)
getUserInfo(role).then(response => {
const data = response.data
commit('SET_ROLES', data.roles)
commit('SET_NAME', data.name)
commit('SET_AVATAR', data.avatar)
commit('SET_INTRODUCTION', data.introduction)
dispatch('GenerateRoutes', data) // 动态修改权限后 重绘侧边菜单
resolve()
})
})
}
} }
} }
export default user const actions = {
// user login
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
commit('SET_TOKEN', data.token)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// get user info
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response
if (!data) {
reject('Verification failed, please Login again.')
}
const { roles, name, avatar, introduction } = data
// roles must be a non-empty array
if (!roles || roles.length <= 0) {
reject('getInfo: roles must be a non-null array!')
}
commit('SET_ROLES', roles)
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
commit('SET_INTRODUCTION', introduction)
resolve(data)
}).catch(error => {
reject(error)
})
})
},
// user logout
logout({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resetRouter()
resolve()
}).catch(error => {
reject(error)
})
})
},
// remove token
resetToken({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resolve()
})
},
// Dynamically modify permissions
changeRoles({ commit, dispatch }, role) {
return new Promise(async resolve => {
const token = role + '-token'
commit('SET_TOKEN', token)
setToken(token)
const { roles } = await dispatch('getInfo')
resetRouter()
// generate accessible routes map based on roles
const accessRoutes = await dispatch('permission/generateRoutes', roles, { root: true })
// dynamically add accessible routes
router.addRoutes(accessRoutes)
resolve()
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@ -0,0 +1,25 @@
/**
* I think element-ui's default theme color is too light for long-term use.
* So I modified the default color and you can modify it to your liking.
**/
/* theme color */
$--color-primary: #1890ff;
$--color-success: #13ce66;
$--color-warning: #FFBA00;
$--color-danger: #ff4949;
// $--color-info: #1E1E1E;
$--button-font-weight: 400;
// $--color-text-regular: #1f2d3d;
$--border-color-light: #dfe4ed;
$--border-color-lighter: #e6ebf5;
$--table-border:1px solid#dfe6ec;
/* icon font path, required */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import "~element-ui/packages/theme-chalk/src/index";

View File

@ -83,19 +83,26 @@
.hideSidebar { .hideSidebar {
.sidebar-container { .sidebar-container {
width: 36px !important; width: 54px !important;
} }
.main-container { .main-container {
margin-left: 36px; margin-left: 54px;
}
.svg-icon {
margin-right: 0px;
} }
.submenu-title-noDropdown { .submenu-title-noDropdown {
padding-left: 10px !important; padding: 0 !important;
position: relative; position: relative;
.el-tooltip { .el-tooltip {
padding: 0 10px !important; padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
} }
} }
@ -103,7 +110,10 @@
overflow: hidden; overflow: hidden;
&>.el-submenu__title { &>.el-submenu__title {
padding-left: 10px !important; padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
.el-submenu__icon-arrow { .el-submenu__icon-arrow {
display: none; display: none;

View File

@ -19,9 +19,10 @@ $menuHover:#263445;
$subMenuBg:#1f2d3d; $subMenuBg:#1f2d3d;
$subMenuHover:#001528; $subMenuHover:#001528;
$sideBarWidth: 180px; $sideBarWidth: 210px;
// the :export directive is the magic sauce for webpack // the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export { :export {
menuText: $menuText; menuText: $menuText;
menuActiveText: $menuActiveText; menuActiveText: $menuActiveText;

35
src/utils/errorLog.js Normal file
View File

@ -0,0 +1,35 @@
import Vue from 'vue'
import store from '@/store'
import { isString, isArray } from '@/utils/validate'
import settings from '@/settings'
// you can set in settings.js
// errorLog:'production' | ['production','development']
const { errorLog: needErrorLog } = settings
function checkNeed(arg) {
const env = process.env.NODE_ENV
if (isString(needErrorLog)) {
return env === needErrorLog
}
if (isArray(needErrorLog)) {
return needErrorLog.includes(env)
}
return false
}
if (checkNeed()) {
Vue.config.errorHandler = function(err, vm, info, a) {
// Don't ask me why I use Vue.nextTick, it just a hack.
// detail see https://forum.vuejs.org/t/dispatch-in-vue-config-errorhandler-has-some-problem/23500
Vue.nextTick(() => {
store.dispatch('errorLog/addErrorLog', {
err,
vm,
info,
url: window.location.href
})
console.error(err, info)
})
}
}

View File

@ -140,7 +140,8 @@ export function param2Obj(url) {
decodeURIComponent(search) decodeURIComponent(search)
.replace(/"/g, '\\"') .replace(/"/g, '\\"')
.replace(/&/g, '","') .replace(/&/g, '","')
.replace(/=/g, '":"') + .replace(/=/g, '":"')
.replace(/\+/g, ' ') +
'"}' '"}'
) )
} }
@ -277,7 +278,7 @@ export function debounce(func, wait, immediate) {
*/ */
export function deepClone(source) { export function deepClone(source) {
if (!source && typeof source !== 'object') { if (!source && typeof source !== 'object') {
throw new Error('error arguments', 'shallowClone') throw new Error('error arguments', 'deepClone')
} }
const targetObj = source.constructor === Array ? [] : {} const targetObj = source.constructor === Array ? [] : {}
Object.keys(source).forEach(keys => { Object.keys(source).forEach(keys => {
@ -299,3 +300,16 @@ export function createUniqueString() {
const randomNum = parseInt((1 + Math.random()) * 65536) + '' const randomNum = parseInt((1 + Math.random()) * 65536) + ''
return (+(randomNum + timestamp)).toString(32) return (+(randomNum + timestamp)).toString(32)
} }
export function hasClass(ele, cls) {
return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
}
export function addClass(ele, cls) {
if (!hasClass(ele, cls)) ele.className += ' ' + cls
}
export function removeClass(ele, cls) {
if (hasClass(ele, cls)) {
const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
ele.className = ele.className.replace(reg, ' ')
}
}

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