Commit fff74c5a authored by 朱招明's avatar 朱招明

update

parent 7b147146
...@@ -17,10 +17,12 @@ ...@@ -17,10 +17,12 @@
"axios": "0.18.1", "axios": "0.18.1",
"core-js": "3.6.5", "core-js": "3.6.5",
"element-ui": "2.13.2", "element-ui": "2.13.2",
"fuse.js": "^7.0.0",
"js-cookie": "2.2.0", "js-cookie": "2.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": "^6.0.2",
"vue": "2.6.10", "vue": "2.6.10",
"vue-router": "3.0.6", "vue-router": "3.0.6",
"vuex": "3.1.0" "vuex": "3.1.0"
......
...@@ -16,19 +16,19 @@ export function menuAdd(parameter) { ...@@ -16,19 +16,19 @@ export function menuAdd(parameter) {
}) })
} }
export function menuEdit(parameter) { export function menuEdit(id, parameter) {
return request({ return request({
url: '/menu/add', url: '/menu/' + id + '/edit',
method: 'put', method: 'put',
data: parameter data: parameter
}) })
} }
export function menuDetail(parameter) { export function menuDetail(id) {
return request({ return request({
url: '/menu/detail', url: '/menu/' + id + '/detail',
method: 'get', method: 'get'
data: parameter
}) })
} }
......
import request from '@/utils/request'
export function roleList(parameter) {
return request({
url: '/role/list',
method: 'get',
data: parameter
})
}
export function roleAdd(parameter) {
return request({
url: '/role/add',
method: 'post',
data: parameter
})
}
export function roleEdit(id, parameter) {
return request({
url: '/role/' + id + '/edit',
method: 'put',
data: parameter
})
}
export function roleDetail(id) {
return request({
url: '/role/' + id + '/detail',
method: 'get'
})
}
export function roleDel(id) {
return request({
url: '/role/' + id + '/delete',
method: 'delete'
})
}
...@@ -29,3 +29,43 @@ export function getConfigs() { ...@@ -29,3 +29,43 @@ export function getConfigs() {
method: 'get' method: 'get'
}) })
} }
export function userList(parameter) {
return request({
url: '/user/list',
method: 'get',
params: parameter
})
}
export function userAdd(parameter) {
return request({
url: '/user/add',
method: 'post',
data: parameter
})
}
export function userEdit(id, parameter) {
return request({
url: '/user/' + id + '/edit',
method: 'put',
data: parameter
})
}
export function userDetail(id) {
return request({
url: '/user/' + id + '/detail',
method: 'get'
})
}
export function userDel(id) {
return request({
url: '/user/' + id + '/delete',
method: 'delete'
})
}
<template>
<div v-if="errorLogs.length>0">
<el-badge :is-dot="true" style="line-height: 25px;margin-top: -5px;" @click.native="dialogTableVisible=true">
<el-button style="padding: 8px 10px;" size="small" type="danger">
<svg-icon icon-class="bug" />
</el-button>
</el-badge>
<el-dialog :visible.sync="dialogTableVisible" width="80%" append-to-body>
<div slot="title">
<span style="padding-right: 10px;">Error Log</span>
<el-button size="mini" type="primary" icon="el-icon-delete" @click="clearAll">Clear All</el-button>
</div>
<el-table :data="errorLogs" border>
<el-table-column label="Message">
<template slot-scope="{row}">
<div>
<span class="message-title">Msg:</span>
<el-tag type="danger">
{{ row.err.message }}
</el-tag>
</div>
<br>
<div>
<span class="message-title" style="padding-right: 10px;">Info: </span>
<el-tag type="warning">
{{ row.vm.$vnode.tag }} error in {{ row.info }}
</el-tag>
</div>
<br>
<div>
<span class="message-title" style="padding-right: 16px;">Url: </span>
<el-tag type="success">
{{ row.url }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="Stack">
<template slot-scope="scope">
{{ scope.row.err.stack }}
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'ErrorLog',
data() {
return {
dialogTableVisible: false
}
},
computed: {
errorLogs() {
return this.$store.getters.errorLogs
}
},
methods: {
clearAll() {
this.dialogTableVisible = false
this.$store.dispatch('errorLog/clearErrorLog')
}
}
}
</script>
<style scoped>
.message-title {
font-size: 16px;
color: #333;
font-weight: bold;
padding-right: 8px;
}
</style>
<template>
<div :class="{'show':show}" class="header-search">
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
<el-select
ref="headerSearchSelect"
v-model="search"
:remote-method="querySearch"
filterable
default-first-option
remote
placeholder="Search"
class="header-search-select"
@change="change"
>
<el-option v-for="item in options" :key="item.path" :value="item" :label="item.title.join(' > ')" />
</el-select>
</div>
</template>
<script>
// fuse is a lightweight fuzzy-search module
// make search results more in line with expectations
import Fuse from 'fuse.js'
import path from 'path'
export default {
name: 'HeaderSearch',
data() {
return {
search: '',
options: [],
searchPool: [],
show: false,
fuse: undefined
}
},
computed: {
routes() {
return this.$store.getters.addRouters
}
},
watch: {
routes() {
this.searchPool = this.generateRoutes(this.routes)
},
searchPool(list) {
this.initFuse(list)
},
show(value) {
if (value) {
document.body.addEventListener('click', this.close)
} else {
document.body.removeEventListener('click', this.close)
}
}
},
mounted() {
this.searchPool = this.generateRoutes(this.routes)
},
methods: {
click() {
this.show = !this.show
if (this.show) {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
}
},
close() {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
this.options = []
this.show = false
},
change(val) {
this.$router.push(val.path)
this.search = ''
this.options = []
this.$nextTick(() => {
this.show = false
})
},
initFuse(list) {
this.fuse = new Fuse(list, {
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [{
name: 'title',
weight: 0.7
}, {
name: 'path',
weight: 0.3
}]
})
},
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
generateRoutes(routes, basePath = '/', prefixTitle = []) {
let res = []
for (const router of routes) {
// skip hidden router
if (router.hidden) { continue }
const data = {
path: path.resolve(basePath, router.path),
title: [...prefixTitle]
}
if (router.meta && router.meta.title) {
data.title = [...data.title, router.meta.title]
if (router.redirect !== 'noRedirect') {
// only push the routes with title
// special case: need to exclude parent router without redirect
res.push(data)
}
}
// recursive child routes
if (router.children) {
const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
if (tempRoutes.length >= 1) {
res = [...res, ...tempRoutes]
}
}
}
return res
},
querySearch(query) {
if (query !== '') {
this.options = this.fuse.search(query)
} else {
this.options = []
}
}
}
}
</script>
<style lang="scss" scoped>
.header-search {
font-size: 0 !important;
.search-icon {
cursor: pointer;
font-size: 18px;
vertical-align: middle;
}
.header-search-select {
font-size: 18px;
transition: width 0.2s;
width: 0;
overflow: hidden;
background: transparent;
border-radius: 0;
display: inline-block;
vertical-align: middle;
::v-deep .el-input__inner {
border-radius: 0;
border: 0;
padding-left: 0;
padding-right: 0;
box-shadow: none !important;
border-bottom: 1px solid #d9d9d9;
vertical-align: middle;
}
}
&.show {
.header-search-select {
width: 210px;
margin-left: 10px;
}
}
}
</style>
<template>
<div ref="rightPanel" :class="{show:show}" class="rightPanel-container">
<div class="rightPanel-background" />
<div class="rightPanel">
<div class="handle-button" :style="{'top':buttonTop+'px','background-color':theme}" @click="show=!show">
<i :class="show?'el-icon-close':'el-icon-setting'" />
</div>
<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: 250,
type: Number
}
},
data() {
return {
show: false
}
},
computed: {
theme() {
return this.$store.state.settings.theme
}
},
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 lang="scss" scoped>
.rightPanel-background {
position: fixed;
top: 0;
left: 0;
opacity: 0;
transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
background: rgba(0, 0, 0, .2);
z-index: -1;
}
.rightPanel {
width: 100%;
max-width: 260px;
height: 100vh;
position: fixed;
top: 0;
right: 0;
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, .05);
transition: all .25s cubic-bezier(.7, .3, .1, 1);
transform: translate(100%);
background: #fff;
z-index: 40000;
}
.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 {
width: 48px;
height: 48px;
position: absolute;
left: -48px;
text-align: center;
font-size: 24px;
border-radius: 6px 0 0 6px !important;
z-index: 0;
pointer-events: auto;
cursor: pointer;
color: #fff;
line-height: 48px;
i {
font-size: 24px;
line-height: 48px;
}
}
</style>
<template>
<div>
<svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
</div>
</template>
<script>
import screenfull from 'screenfull'
export default {
name: 'Screenfull',
data() {
return {
isFullscreen: false
}
},
mounted() {
this.init()
},
beforeDestroy() {
this.destroy()
},
methods: {
click() {
if (!screenfull.enabled) {
this.$message({
message: 'you browser can not work',
type: 'warning'
})
return false
}
screenfull.toggle()
},
change() {
this.isFullscreen = screenfull.isFullscreen
},
init() {
if (screenfull.enabled) {
screenfull.on('change', this.change)
}
},
destroy() {
if (screenfull.enabled) {
screenfull.off('change', this.change)
}
}
}
}
</script>
<style scoped>
.screenfull-svg {
display: inline-block;
cursor: pointer;
fill: #5a5e66;;
width: 20px;
height: 20px;
vertical-align: 10px;
}
</style>
<template>
<el-dropdown trigger="click" @command="handleSetSize">
<div>
<svg-icon class-name="size-icon" icon-class="size" />
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value">
{{
item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
export default {
data() {
return {
sizeOptions: [
{ label: 'Default', value: 'default' },
{ label: 'Medium', value: 'medium' },
{ label: 'Small', value: 'small' },
{ label: 'Mini', value: 'mini' }
]
}
},
computed: {
size() {
return this.$store.getters.size
}
},
methods: {
handleSetSize(size) {
this.$ELEMENT.size = size
this.$store.dispatch('app/setSize', size)
this.refreshView()
this.$message({
message: 'Switch Size Success',
type: 'success'
})
},
refreshView() {
// In order to make the cached page re-rendered
this.$store.dispatch('tagsView/delAllCachedViews', this.$route)
const { fullPath } = this.$route
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + fullPath
})
})
}
}
}
</script>
<template>
<el-color-picker
v-model="theme"
:predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
class="theme-picker"
popper-class="theme-picker-dropdown"
/>
</template>
<script>
const version = require('element-ui/package.json').version // element-ui version from node_modules
const ORIGINAL_THEME = '#409EFF' // default color
export default {
data() {
return {
chalk: '', // content of theme-chalk css
theme: ''
}
},
computed: {
defaultTheme() {
return this.$store.state.settings.theme
}
},
watch: {
defaultTheme: {
handler: function(val, oldVal) {
this.theme = val
},
immediate: true
},
async theme(val) {
const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
if (typeof val !== 'string') return
const themeCluster = this.getThemeCluster(val.replace('#', ''))
const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
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) => {
return () => {
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
let styleTag = document.getElementById(id)
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', id)
document.head.appendChild(styleTag)
}
styleTag.innerText = newStyle
}
}
if (!this.chalk) {
const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
await this.getCSSString(url, 'chalk')
}
const chalkHandler = getHandler('chalk', 'chalk-style')
chalkHandler()
const styles = [].slice.call(document.querySelectorAll('style'))
.filter(style => {
const text = style.innerText
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
})
styles.forEach(style => {
const { innerText } = style
if (typeof innerText !== 'string') return
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
})
this.$emit('change', val)
$message.close()
}
},
methods: {
updateStyle(style, oldCluster, newCluster) {
let newStyle = style
oldCluster.forEach((color, index) => {
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
})
return newStyle
},
getCSSString(url, variable) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
resolve()
}
}
xhr.open('GET', url)
xhr.send()
})
},
getThemeCluster(theme) {
const tintColor = (color, tint) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
if (tint === 0) { // when primary color is in its rgb space
return [red, green, blue].join(',')
} else {
red += Math.round(tint * (255 - red))
green += Math.round(tint * (255 - green))
blue += Math.round(tint * (255 - blue))
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
}
const shadeColor = (color, shade) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
red = Math.round((1 - shade) * red)
green = Math.round((1 - shade) * green)
blue = Math.round((1 - shade) * blue)
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
const clusters = [theme]
for (let i = 0; i <= 9; i++) {
clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
}
clusters.push(shadeColor(theme, 0.1))
return clusters
}
}
}
</script>
<style>
.theme-message,
.theme-picker-dropdown {
z-index: 99999 !important;
}
.theme-picker .el-color-picker__trigger {
height: 26px !important;
width: 26px !important;
padding: 2px;
}
.theme-picker-dropdown .el-color-dropdown__link-btn {
display: none;
}
</style>
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M124.884 109.812L94.256 79.166c-.357-.357-.757-.629-1.129-.914a50.366 50.366 0 0 0 8.186-27.59C101.327 22.689 78.656 0 50.67 0 22.685 0 0 22.688 0 50.663c0 27.989 22.685 50.663 50.656 50.663 10.186 0 19.643-3.03 27.6-8.201.286.385.557.771.9 1.114l30.628 30.632a10.633 10.633 0 0 0 7.543 3.129c2.728 0 5.457-1.043 7.543-3.115 4.171-4.157 4.171-10.915.014-15.073M50.671 85.338C31.557 85.338 16 69.78 16 50.663c0-19.102 15.557-34.661 34.67-34.661 19.115 0 34.657 15.559 34.657 34.675 0 19.102-15.557 34.661-34.656 34.661"/></svg>
\ No newline at end of file
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h54.796v18.286H36.531V128H18.265V73.143H0V54.857zm127.857-36.571H91.935V128H72.456V18.286H36.534V0h91.326l-.003 18.286z"/></svg>
\ No newline at end of file
<template> <template>
<section class="app-main"> <section class="app-main">
<transition name="fade-transform" mode="out-in"> <transition name="fade-transform" mode="out-in">
<router-view :key="key" /> <keep-alive :include="cachedViews">
<router-view :key="key" />
</keep-alive>
</transition> </transition>
</section> </section>
</template> </template>
...@@ -10,6 +12,9 @@ ...@@ -10,6 +12,9 @@
export default { export default {
name: 'AppMain', name: 'AppMain',
computed: { computed: {
cachedViews() {
return this.$store.state.tagsView.cachedViews
},
key() { key() {
return this.$route.path return this.$route.path
} }
...@@ -17,17 +22,29 @@ export default { ...@@ -17,17 +22,29 @@ export default {
} }
</script> </script>
<style scoped> <style lang="scss" scoped>
.app-main { .app-main {
/*50 = navbar */ /* 50= navbar 50 */
min-height: calc(100vh - 50px); min-height: calc(100vh - 50px);
width: 100%; width: 100%;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.fixed-header+.app-main { .fixed-header+.app-main {
padding-top: 50px; padding-top: 50px;
} }
.hasTagsView {
.app-main {
/* 84 = navbar + tags-view = 50 + 34 */
min-height: calc(100vh - 84px);
}
.fixed-header+.app-main {
padding-top: 84px;
}
}
</style> </style>
<style lang="scss"> <style lang="scss">
......
<template> <template>
<div class="navbar"> <div class="navbar">
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" /> <hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<breadcrumb class="breadcrumb-container" /> <breadcrumb id="breadcrumb-container" class="breadcrumb-container" />
<div class="right-menu"> <div class="right-menu">
<el-dropdown class="avatar-container" trigger="click"> <template v-if="device!=='mobile'">
<search id="header-search" class="right-menu-item" />
<error-log class="errLog-container right-menu-item hover-effect" />
<!-- <screenfull id="screenfull" class="right-menu-item hover-effect" />-->
<el-tooltip content="Global Size" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>
</template>
<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" class="user-dropdown"> <el-dropdown-menu slot="dropdown">
<router-link to="/profile/index">
<el-dropdown-item>Profile</el-dropdown-item>
</router-link>
<router-link to="/"> <router-link to="/">
<el-dropdown-item> <el-dropdown-item>Dashboard</el-dropdown-item>
Home
</el-dropdown-item>
</router-link> </router-link>
<a target="_blank" href="https://github.com/PanJiaChen/vue-admin-template/"> <a target="_blank" href="https://github.com/PanJiaChen/vue-element-admin/">
<el-dropdown-item>Github</el-dropdown-item> <el-dropdown-item>Github</el-dropdown-item>
</a> </a>
<a target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/#/"> <a target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/#/">
...@@ -35,16 +49,25 @@ ...@@ -35,16 +49,25 @@
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb' import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger' import Hamburger from '@/components/Hamburger'
import ErrorLog from '@/components/ErrorLog'
// import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch'
export default { export default {
components: { components: {
Breadcrumb, Breadcrumb,
Hamburger Hamburger,
ErrorLog,
// Screenfull,
SizeSelect,
Search
}, },
computed: { computed: {
...mapGetters([ ...mapGetters([
'sidebar', 'sidebar',
'avatar' 'avatar',
'device'
]) ])
}, },
methods: { methods: {
...@@ -84,6 +107,11 @@ export default { ...@@ -84,6 +107,11 @@ export default {
float: left; float: left;
} }
.errLog-container {
display: inline-block;
vertical-align: top;
}
.right-menu { .right-menu {
float: right; float: right;
height: 100%; height: 100%;
......
<template>
<div class="drawer-container">
<div>
<h3 class="drawer-title">Page style setting</h3>
<div class="drawer-item">
<span>Theme Color</span>
<theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange" />
</div>
<div class="drawer-item">
<span>Open Tags-View</span>
<el-switch v-model="tagsView" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>Fixed Header</span>
<el-switch v-model="fixedHeader" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>Sidebar Logo</span>
<el-switch v-model="sidebarLogo" class="drawer-switch" />
</div>
</div>
</div>
</template>
<script>
import ThemePicker from '@/components/ThemePicker'
export default {
components: { ThemePicker },
data() {
return {}
},
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
})
}
},
sidebarLogo: {
get() {
return this.$store.state.settings.sidebarLogo
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'sidebarLogo',
value: val
})
}
}
},
methods: {
themeChange(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'theme',
value: val
})
}
}
}
</script>
<style 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>
...@@ -24,7 +24,7 @@ export default { ...@@ -24,7 +24,7 @@ export default {
}, },
data() { data() {
return { return {
title: 'Vue Admin Template', title: 'Vue Element Admin',
logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png' logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png'
} }
} }
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
:collapse-transition="false" :collapse-transition="false"
mode="vertical" mode="vertical"
> >
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" /> <sidebar-item v-for="route in menus" :key="route.path" :item="route" :base-path="route.path" />
</el-menu> </el-menu>
</el-scrollbar> </el-scrollbar>
</div> </div>
...@@ -26,16 +26,17 @@ import variables from '@/styles/variables.scss' ...@@ -26,16 +26,17 @@ import variables from '@/styles/variables.scss'
export default { export default {
components: { SidebarItem, Logo }, components: { SidebarItem, Logo },
data() {
return {
// base
menus: []
}
},
computed: { computed: {
...mapGetters([ ...mapGetters([
'sidebar', 'addRouters',
'addRouters' 'sidebar'
]), ]),
routes() {
// return this.$router.options.routes
const routes = this.addRouters.find(item => item.path === '/')
return (routes && routes.children) || []
},
activeMenu() { activeMenu() {
const route = this.$route const route = this.$route
const { meta, path } = route const { meta, path } = route
...@@ -54,6 +55,11 @@ export default { ...@@ -54,6 +55,11 @@ export default {
isCollapse() { isCollapse() {
return !this.sidebar.opened return !this.sidebar.opened
} }
},
created() {
const routes = this.addRouters.find(item => item.path === '/')
this.menus = (routes && routes.children) || []
} }
} }
</script> </script>
<template>
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
<slot />
</el-scrollbar>
</template>
<script>
const tagAndTagSpacing = 4 // tagAndTagSpacing
export default {
name: 'ScrollPane',
data() {
return {
left: 0
}
},
computed: {
scrollWrapper() {
return this.$refs.scrollContainer.$refs.wrap
}
},
mounted() {
this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
},
beforeDestroy() {
this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
},
methods: {
handleScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = this.scrollWrapper
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
},
emitScroll() {
this.$emit('scroll')
},
moveToTarget(currentTag) {
const $container = this.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth
const $scrollWrapper = this.scrollWrapper
const tagList = this.$parent.$refs.tag
let firstTag = null
let lastTag = null
// find first tag and last tag
if (tagList.length > 0) {
firstTag = tagList[0]
lastTag = tagList[tagList.length - 1]
}
if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
} else {
// find preTag and nextTag
const currentIndex = tagList.findIndex(item => item === currentTag)
const prevTag = tagList[currentIndex - 1]
const nextTag = tagList[currentIndex + 1]
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
}
}
}
}
}
</script>
<style lang="scss" scoped>
.scroll-container {
white-space: nowrap;
position: relative;
overflow: hidden;
width: 100%;
::v-deep {
.el-scrollbar__bar {
bottom: 0px;
}
.el-scrollbar__wrap {
height: 49px;
}
}
}
</style>
<template>
<div id="tags-view-container" class="tags-view-container">
<scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
<router-link
v-for="tag in visitedViews"
ref="tag"
:key="tag.path"
:class="isActive(tag)?'active':''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
class="tags-view-item"
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
@contextmenu.prevent.native="openMenu(tag,$event)"
>
{{ tag.title }}
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
</router-link>
</scroll-pane>
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)">Refresh</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">Close</li>
<li @click="closeOthersTags">Close Others</li>
<li @click="closeAllTags(selectedTag)">Close All</li>
</ul>
</div>
</template>
<script>
import ScrollPane from './ScrollPane'
import path from 'path'
export default {
components: { ScrollPane },
data() {
return {
visible: false,
top: 0,
left: 0,
selectedTag: {},
affixTags: []
}
},
computed: {
visitedViews() {
return this.$store.state.tagsView.visitedViews
},
routes() {
return this.$store.state.permission.addRouters
}
},
watch: {
$route() {
this.addTags()
this.moveToCurrentTag()
},
visible(value) {
if (value) {
document.body.addEventListener('click', this.closeMenu)
} else {
document.body.removeEventListener('click', this.closeMenu)
}
}
},
mounted() {
this.initTags()
this.addTags()
},
methods: {
isActive(route) {
return route.path === this.$route.path
},
isAffix(tag) {
return tag.meta && tag.meta.affix
},
filterAffixTags(routes, basePath = '/') {
let tags = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path)
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta }
})
}
if (route.children) {
const tempTags = this.filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
}
}
})
return tags
},
initTags() {
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
for (const tag of affixTags) {
// Must have tag name
if (tag.name) {
this.$store.dispatch('tagsView/addVisitedView', tag)
}
}
},
addTags() {
const { name } = this.$route
if (name) {
this.$store.dispatch('tagsView/addView', this.$route)
}
return false
},
moveToCurrentTag() {
const tags = this.$refs.tag
this.$nextTick(() => {
for (const tag of tags) {
if (tag.to.path === this.$route.path) {
this.$refs.scrollPane.moveToTarget(tag)
// when query is different then update
if (tag.to.fullPath !== this.$route.fullPath) {
this.$store.dispatch('tagsView/updateVisitedView', this.$route)
}
break
}
}
})
},
refreshSelectedTag(view) {
this.$store.dispatch('tagsView/delCachedView', view).then(() => {
const { fullPath } = view
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + fullPath
})
})
})
},
closeSelectedTag(view) {
this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews, view)
}
})
},
closeOthersTags() {
this.$router.push(this.selectedTag)
this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
this.moveToCurrentTag()
})
},
closeAllTags(view) {
this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
if (this.affixTags.some(tag => tag.path === view.path)) {
return
}
this.toLastView(visitedViews, view)
})
},
toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
this.$router.push(latestView.fullPath)
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === 'Dashboard') {
// to reload home page
this.$router.replace({ path: '/redirect' + view.fullPath })
} else {
this.$router.push('/')
}
}
},
openMenu(tag, e) {
const menuMinWidth = 105
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
const offsetWidth = this.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const left = e.clientX - offsetLeft + 15 // 15: margin right
if (left > maxLeft) {
this.left = maxLeft
} else {
this.left = left
}
this.top = e.clientY
this.visible = true
this.selectedTag = tag
},
closeMenu() {
this.visible = false
},
handleScroll() {
this.closeMenu()
}
}
}
</script>
<style lang="scss" scoped>
.tags-view-container {
height: 34px;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
.tags-view-wrapper {
.tags-view-item {
display: inline-block;
position: relative;
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
background-color: #42b983;
color: #fff;
border-color: #42b983;
&::before {
content: '';
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 2px;
}
}
}
}
.contextmenu {
margin: 0;
background: #fff;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
}
</style>
<style lang="scss">
//reset element css of el-icon-close
.tags-view-wrapper {
.tags-view-item {
.el-icon-close {
width: 16px;
height: 16px;
vertical-align: 2px;
border-radius: 50%;
text-align: center;
transition: all .3s cubic-bezier(.645, .045, .355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(.6);
display: inline-block;
vertical-align: -3px;
}
&:hover {
background-color: #b4bccc;
color: #fff;
}
}
}
}
</style>
export { default as Navbar } from './Navbar'
export { default as Sidebar } from './Sidebar'
export { default as AppMain } from './AppMain' export { default as AppMain } from './AppMain'
export { default as Navbar } from './Navbar'
export { default as Settings } from './Settings'
export { default as Sidebar } from './Sidebar/index.vue'
export { default as TagsView } from './TagsView/index.vue'
...@@ -2,37 +2,44 @@ ...@@ -2,37 +2,44 @@
<div :class="classObj" class="app-wrapper"> <div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" /> <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<sidebar class="sidebar-container" /> <sidebar class="sidebar-container" />
<div class="main-container"> <div :class="{hasTagsView:needTagsView}" class="main-container">
<div :class="{'fixed-header':fixedHeader}"> <div :class="{'fixed-header':fixedHeader}">
<navbar /> <navbar />
<tags-view v-if="needTagsView" />
</div> </div>
<app-main /> <app-main />
<right-panel v-if="showSettings">
<settings />
</right-panel>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { Navbar, Sidebar, AppMain } from './components' import RightPanel from '@/components/RightPanel'
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import ResizeMixin from './mixin/ResizeHandler' import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
export default { export default {
name: 'Layout', name: 'Layout',
components: { components: {
AppMain,
Navbar, Navbar,
RightPanel,
Settings,
Sidebar, Sidebar,
AppMain TagsView
}, },
mixins: [ResizeMixin], mixins: [ResizeMixin],
computed: { computed: {
sidebar() { ...mapState({
return this.$store.state.app.sidebar sidebar: state => state.app.sidebar,
}, device: state => state.app.device,
device() { showSettings: state => state.settings.showSettings,
return this.$store.state.app.device needTagsView: state => state.settings.tagsView,
}, fixedHeader: state => state.settings.fixedHeader
fixedHeader() { }),
return this.$store.state.settings.fixedHeader
},
classObj() { classObj() {
return { return {
hideSidebar: !this.sidebar.opened, hideSidebar: !this.sidebar.opened,
...@@ -59,11 +66,13 @@ export default { ...@@ -59,11 +66,13 @@ export default {
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%; width: 100%;
&.mobile.openSidebar{
&.mobile.openSidebar {
position: fixed; position: fixed;
top: 0; top: 0;
} }
} }
.drawer-bg { .drawer-bg {
background: #000; background: #000;
opacity: 0.3; opacity: 0.3;
......
...@@ -27,26 +27,21 @@ router.beforeEach(async(to, from, next) => { ...@@ -27,26 +27,21 @@ router.beforeEach(async(to, from, next) => {
NProgress.done() NProgress.done()
} else { } else {
if (!store.getters.menuSet) { if (!store.getters.menuSet) {
store.dispatch('GenerateRoutes', {}).then(() => { try {
// 动态添加可访问路由表 await store.dispatch('user/getInfo')
// VueRouter@3.5.0+ New API
resetRouter() // 重置路由 防止退出重新登录或者 token 过期后页面未刷新,导致的路由重复添加
// const addRouters = store.getters.addRouters
// for (const r of addRouters) {
// router.addRoutes(r)
// }
router.addRoutes(store.getters.addRouters)
// 请求带有 redirect 重定向时,登录自动重定向到该地址
const redirect = decodeURIComponent(from.query.redirect || to.path)
if (to.path === redirect) { store.dispatch('GenerateRoutes', {}).then(() => {
// set the replace: true so the navigation will not leave a history record resetRouter() // 重置路由 防止退出重新登录或者 token 过期后页面未刷新,导致的路由重复添加
router.addRoutes(store.getters.addRouters)
// 请求带有 redirect 重定向时,登录自动重定向到该地址
next({ ...to, replace: true }) next({ ...to, replace: true })
} else { })
// 跳转到目的路由 } catch (error) {
next({ path: redirect }) // remove token and go to login page to re-login
} await store.dispatch('user/resetToken')
}) next(`/login?redirect=${to.path}`)
NProgress.done()
}
} else { } else {
next() next()
} }
......
import RouteView from '@/layout/RouteView' // 路由标识对应页面
// 路由表
export const constantRouterPath = {
'Dashboard': '/dashboard',
'System': '/system',
'SystemPower': '/system_power',
'SystemPowerRoleList': '/system_power_role_list',
'SystemPowerUserList': '/system_power_user_list',
'SystemPowerMenuList': '/system_power_menu_list'
}
// 路由对应页面
export const constantRouterComponents = { export const constantRouterComponents = {
// 权限管理 // 权限管理
// SystemPowerRoleList: () => import('@/views/system/power/role/List'), // SystemPowerRoleList: () => import('@/views/system/power/role/List'),
// UserList: () => import('@/views/power/user/list'), // UserList: () => import('@/views/power/user/list'),
'RouteView': RouteView,
'Dashboard': () => import('@/views/dashboard/index'), 'Dashboard': () => import('@/views/dashboard/index'),
'SystemPowerRoleList': () => import('@/views/system/power/role/List'),
'SystemPowerUserList': () => import('@/views/system/power/user/List'),
'SystemPowerMenuList': () => import('@/views/system/power/menu/List') 'SystemPowerMenuList': () => import('@/views/system/power/menu/List')
} }
import * as userService from '@/api/user' import * as userService from '@/api/user'
import { constantRouterComponents, constantRouterPath } from '@/router/config' import { constantRouterComponents } from '@/router/config'
import Layout from '@/layout' import Layout from '@/layout'
import RouteView from '@/layout/RouteView'
// 前端未找到页面路由(固定不用改) // 前端未找到页面路由(固定不用改)
const notFoundRouter = { const notFoundRouter = {
...@@ -11,12 +12,12 @@ const notFoundRouter = { ...@@ -11,12 +12,12 @@ const notFoundRouter = {
// 根级菜单 // 根级菜单
const rootRouter = { const rootRouter = {
path: '/', router: '/',
key: 'Root', key: 'Root',
component: Layout, component: Layout,
redirect: '/dashboard', redirect: '/dashboard',
children: [{ children: [{
path: 'dashboard', router: '/dashboard',
key: 'Dashboard', key: 'Dashboard',
title: '首页', title: '首页',
icon: 'dashboard', icon: 'dashboard',
...@@ -63,11 +64,11 @@ export const generatorDynamicRouter = () => { ...@@ -63,11 +64,11 @@ export const generatorDynamicRouter = () => {
export const generator = (routerMap, parent) => { export const generator = (routerMap, parent) => {
return routerMap.map(item => { return routerMap.map(item => {
// const { show, hideChildren, hiddenHeaderContent, target, icon } = item.meta || {} // const { show, hideChildren, hiddenHeaderContent, target, icon } = item.meta || {}
const path = item.path || (constantRouterPath[item.key] ?? '/') // const path = item.path || item.key + (item.id || '')
const component = item.component || constantRouterComponents[item.key] const component = item.component || constantRouterComponents[item.key]
const currentRouter = { const currentRouter = {
// 如果路由设置了 path,则作为默认 path,否则 路由地址 动态拼接生成如 /dashboard/workplace // 如果路由设置了 path,则作为默认 path,否则 路由地址 动态拼接生成如 /dashboard/workplace
path: path, path: item.router || (item.key + '_' + (item.id || '')),
// 路由名称,建议唯一 // 路由名称,建议唯一
name: item.key + (item.id || ''), name: item.key + (item.id || ''),
// 该路由对应页面的 组件 :方案1 // 该路由对应页面的 组件 :方案1
...@@ -100,12 +101,37 @@ export const generator = (routerMap, parent) => { ...@@ -100,12 +101,37 @@ export const generator = (routerMap, parent) => {
// Recursion // Recursion
currentRouter.children = generator(item.children, currentRouter) currentRouter.children = generator(item.children, currentRouter)
if (item.key !== 'Root') { if (item.key !== 'Root') {
currentRouter.component = constantRouterComponents['RouteView'] currentRouter.component = RouteView
} }
} }
// 是否有功能页菜单
// const actions = constantActionRouterComponents[item.key]
// if (actions) {
// const children_action = generatorAction(actions, currentRouter)
// const children_menu = currentRouter.children || []
// console.log(children_action, children_menu)
// currentRouter.children = [...children_menu, ...children_action]
// }
return currentRouter return currentRouter
}) })
} }
export const generatorAction = (routerMap, parent) => {
return routerMap.map(item => {
return {
path: item.path,
name: parent.name + '.' + item.name,
component: item.component,
hidden: true,
meta: {
title: item.title,
icon: item.icon,
activeMenu: parent.path
}
}
})
}
/** /**
* 数组转树形结构 * 数组转树形结构
......
module.exports = { module.exports = {
title: 'OA',
title: 'Vue Admin Template', /**
* @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 * @type {boolean} true | false
...@@ -12,5 +23,13 @@ module.exports = { ...@@ -12,5 +23,13 @@ module.exports = {
* @type {boolean} true | false * @type {boolean} true | false
* @description Whether show the logo in sidebar * @description Whether show the logo in sidebar
*/ */
sidebarLogo: false sidebarLogo: false,
/**
* @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'
} }
const getters = { const getters = {
sidebar: state => state.app.sidebar, sidebar: state => state.app.sidebar,
size: state => state.app.size,
device: state => state.app.device, device: state => state.app.device,
visitedViews: state => state.tagsView.visitedViews,
cachedViews: state => state.tagsView.cachedViews,
token: state => state.user.token, token: state => state.user.token,
avatar: state => state.user.avatar, avatar: state => state.user.avatar,
name: state => state.user.name, name: state => state.user.name,
addRouters: state => state.permission.addRouters, addRouters: state => state.permission.addRouters,
multiTab: state => state.app.multiTab, multiTab: state => state.app.multiTab,
menuSet: state => state.permission.menuSet menuSet: state => state.permission.menuSet,
errorLogs: state => state.errorLog.logs
} }
export default getters export default getters
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import getters from './getters' import getters from './getters'
import app from './modules/app'
import settings from './modules/settings'
import user from './modules/user'
import permission from './modules/async-router'
Vue.use(Vuex) Vue.use(Vuex)
// https://webpack.js.org/guides/dependency-management/#requirecontext
const modulesFiles = require.context('./modules', true, /\.js$/)
// you do not need `import app from './modules/app'`
// it will auto require all vuex module from modules file
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
// set './app.js' => 'app'
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = modulesFiles(modulePath)
modules[moduleName] = value.default
return modules
}, {})
const store = new Vuex.Store({ const store = new Vuex.Store({
modules: { modules,
app,
settings,
user,
permission
},
getters getters
}) })
......
...@@ -5,7 +5,8 @@ const state = { ...@@ -5,7 +5,8 @@ const state = {
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
withoutAnimation: false withoutAnimation: false
}, },
device: 'desktop' device: 'desktop',
size: Cookies.get('size') || 'medium'
} }
const mutations = { const mutations = {
......
const state = {
logs: []
}
const mutations = {
ADD_ERROR_LOG: (state, log) => {
state.logs.push(log)
},
CLEAR_ERROR_LOG: (state) => {
state.logs.splice(0)
}
}
const actions = {
addErrorLog({ commit }, log) {
commit('ADD_ERROR_LOG', log)
},
clearErrorLog({ commit }) {
commit('CLEAR_ERROR_LOG')
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
import variables from '@/styles/element-variables.scss'
import defaultSettings from '@/settings' import defaultSettings from '@/settings'
const { showSettings, fixedHeader, sidebarLogo } = defaultSettings const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings
const state = { const state = {
theme: variables.theme,
showSettings: showSettings, showSettings: showSettings,
tagsView: tagsView,
fixedHeader: fixedHeader, fixedHeader: fixedHeader,
sidebarLogo: sidebarLogo sidebarLogo: sidebarLogo
} }
......
const state = {
visitedViews: [],
cachedViews: []
}
const 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) => {
for (const [i, v] of state.visitedViews.entries()) {
if (v.path === view.path) {
state.visitedViews.splice(i, 1)
break
}
}
},
DEL_CACHED_VIEW: (state, view) => {
const index = state.cachedViews.indexOf(view.name)
index > -1 && state.cachedViews.splice(index, 1)
},
DEL_OTHERS_VISITED_VIEWS: (state, view) => {
state.visitedViews = state.visitedViews.filter(v => {
return v.meta.affix || v.path === view.path
})
},
DEL_OTHERS_CACHED_VIEWS: (state, view) => {
const index = state.cachedViews.indexOf(view.name)
if (index > -1) {
state.cachedViews = state.cachedViews.slice(index, index + 1)
} else {
// if index = -1, there is no cached tags
state.cachedViews = []
}
},
DEL_ALL_VISITED_VIEWS: state => {
// keep affix tags
const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
state.visitedViews = affixTags
},
DEL_ALL_CACHED_VIEWS: state => {
state.cachedViews = []
},
UPDATE_VISITED_VIEW: (state, view) => {
for (let v of state.visitedViews) {
if (v.path === view.path) {
v = Object.assign(v, view)
break
}
}
}
}
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
}
/**
* 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";
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
theme: $--color-primary;
}
...@@ -98,6 +98,69 @@ export function formatTime(time, option) { ...@@ -98,6 +98,69 @@ export function formatTime(time, option) {
* @param {string} url * @param {string} url
* @returns {Object} * @returns {Object}
*/ */
export function getQueryObject(url) {
url = url == null ? window.location.href : url
const search = url.substring(url.lastIndexOf('?') + 1)
const obj = {}
const reg = /([^?&=]+)=([^?&=]*)/g
search.replace(reg, (rs, $1, $2) => {
const name = decodeURIComponent($1)
let val = decodeURIComponent($2)
val = String(val)
obj[name] = val
return rs
})
return obj
}
/**
* @param {string} input value
* @returns {number} output value
*/
export function byteLength(str) {
// returns the byte length of an utf8 string
let s = str.length
for (var i = str.length - 1; i >= 0; i--) {
const code = str.charCodeAt(i)
if (code > 0x7f && code <= 0x7ff) s++
else if (code > 0x7ff && code <= 0xffff) s += 2
if (code >= 0xDC00 && code <= 0xDFFF) i--
}
return s
}
/**
* @param {Array} actual
* @returns {Array}
*/
export function cleanArray(actual) {
const newArray = []
for (let i = 0; i < actual.length; i++) {
if (actual[i]) {
newArray.push(actual[i])
}
}
return newArray
}
/**
* @param {Object} json
* @returns {Array}
*/
export function param(json) {
if (!json) return ''
return cleanArray(
Object.keys(json).map(key => {
if (json[key] === undefined) return ''
return encodeURIComponent(key) + '=' + encodeURIComponent(json[key])
})
).join('&')
}
/**
* @param {string} url
* @returns {Object}
*/
export function param2Obj(url) { export function param2Obj(url) {
const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
if (!search) { if (!search) {
...@@ -115,3 +178,180 @@ export function param2Obj(url) { ...@@ -115,3 +178,180 @@ export function param2Obj(url) {
}) })
return obj return obj
} }
/**
* @param {string} val
* @returns {string}
*/
export function html2Text(val) {
const div = document.createElement('div')
div.innerHTML = val
return div.textContent || div.innerText
}
/**
* Merges two objects, giving the last one precedence
* @param {Object} target
* @param {(Object|Array)} source
* @returns {Object}
*/
export function objectMerge(target, source) {
if (typeof target !== 'object') {
target = {}
}
if (Array.isArray(source)) {
return source.slice()
}
Object.keys(source).forEach(property => {
const sourceProperty = source[property]
if (typeof sourceProperty === 'object') {
target[property] = objectMerge(target[property], sourceProperty)
} else {
target[property] = sourceProperty
}
})
return target
}
/**
* @param {HTMLElement} element
* @param {string} className
*/
export function toggleClass(element, className) {
if (!element || !className) {
return
}
let classString = element.className
const nameIndex = classString.indexOf(className)
if (nameIndex === -1) {
classString += '' + className
} else {
classString =
classString.substr(0, nameIndex) +
classString.substr(nameIndex + className.length)
}
element.className = classString
}
/**
* @param {string} type
* @returns {Date}
*/
export function getTime(type) {
if (type === 'start') {
return new Date().getTime() - 3600 * 1000 * 24 * 90
} else {
return new Date(new Date().toDateString())
}
}
/**
* @param {Function} func
* @param {number} wait
* @param {boolean} immediate
* @return {*}
*/
export function debounce(func, wait, immediate) {
let timeout, args, context, timestamp, result
const later = function() {
// 据上一次触发时间间隔
const last = +new Date() - timestamp
// 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last)
} else {
timeout = null
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
if (!immediate) {
result = func.apply(context, args)
if (!timeout) context = args = null
}
}
}
return function(...args) {
context = this
timestamp = +new Date()
const callNow = immediate && !timeout
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait)
if (callNow) {
result = func.apply(context, args)
context = args = null
}
return result
}
}
/**
* This is just a simple version of deep copy
* Has a lot of edge cases bug
* If you want to use a perfect deep copy, use lodash's _.cloneDeep
* @param {Object} source
* @returns {Object}
*/
export function deepClone(source) {
if (!source && typeof source !== 'object') {
throw new Error('error arguments', 'deepClone')
}
const targetObj = source.constructor === Array ? [] : {}
Object.keys(source).forEach(keys => {
if (source[keys] && typeof source[keys] === 'object') {
targetObj[keys] = deepClone(source[keys])
} else {
targetObj[keys] = source[keys]
}
})
return targetObj
}
/**
* @param {Array} arr
* @returns {Array}
*/
export function uniqueArr(arr) {
return Array.from(new Set(arr))
}
/**
* @returns {string}
*/
export function createUniqueString() {
const timestamp = +new Date() + ''
const randomNum = parseInt((1 + Math.random()) * 65536) + ''
return (+(randomNum + timestamp)).toString(32)
}
/**
* Check if an element has a class
* @param {HTMLElement} elm
* @param {string} cls
* @returns {boolean}
*/
export function hasClass(ele, cls) {
return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
}
/**
* Add class to element
* @param {HTMLElement} elm
* @param {string} cls
*/
export function addClass(ele, cls) {
if (!hasClass(ele, cls)) ele.className += ' ' + cls
}
/**
* Remove class from element
* @param {HTMLElement} elm
* @param {string} cls
*/
export function removeClass(ele, cls) {
if (hasClass(ele, cls)) {
const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
ele.className = ele.className.replace(reg, ' ')
}
}
...@@ -2,19 +2,16 @@ import axios from 'axios' ...@@ -2,19 +2,16 @@ import axios from 'axios'
import { Message } from 'element-ui' import { Message } from 'element-ui'
import store from '@/store' import store from '@/store'
import { getToken } from '@/utils/auth' import { getToken } from '@/utils/auth'
// create an axios instance // create an axios instance
const service = axios.create({ const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests // withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout timeout: 5000 // request timeout
}) })
// request interceptor // request interceptor
service.interceptors.request.use( service.interceptors.request.use(
config => { config => {
// do something before request is sent // do something before request is sent
if (store.getters.token) { if (store.getters.token) {
// let each request carry token // let each request carry token
// ['X-Token'] is a custom headers key // ['X-Token'] is a custom headers key
...@@ -47,6 +44,7 @@ service.interceptors.response.use( ...@@ -47,6 +44,7 @@ service.interceptors.response.use(
}, },
error => { error => {
console.log('err' + error) // for debug console.log('err' + error) // for debug
const data = error.response.data
const token = getToken() const token = getToken()
switch (error.response.status) { switch (error.response.status) {
case 422: case 422:
...@@ -63,7 +61,7 @@ service.interceptors.response.use( ...@@ -63,7 +61,7 @@ service.interceptors.response.use(
break break
default: default:
Message({ Message({
message: error.message, message: data.message || error.message,
type: 'error', type: 'error',
duration: 5 * 1000 duration: 5 * 1000
}) })
......
...@@ -18,3 +18,18 @@ export function listToTree(list, tree, parentId) { ...@@ -18,3 +18,18 @@ export function listToTree(list, tree, parentId) {
} }
}) })
} }
export function deepClone(source) {
if (!source && typeof source !== 'object') {
throw new Error('error arguments', 'deepClone')
}
const targetObj = source.constructor === Array ? [] : {}
Object.keys(source).forEach(keys => {
if (source[keys] && typeof source[keys] === 'object') {
targetObj[keys] = deepClone(source[keys])
} else {
targetObj[keys] = source[keys]
}
})
return targetObj
}
...@@ -53,17 +53,13 @@ ...@@ -53,17 +53,13 @@
</template> </template>
<script> <script>
import { validUsername } from '@/utils/validate' // import { validUsername } from '@/utils/validate'
export default { export default {
name: 'Login', name: 'Login',
data() { data() {
const validateUsername = (rule, value, callback) => { const validateUsername = (rule, value, callback) => {
if (!validUsername(value)) { callback()
callback(new Error('Please enter the correct user name'))
} else {
callback()
}
} }
const validatePassword = (rule, value, callback) => { const validatePassword = (rule, value, callback) => {
if (value.length < 6) { if (value.length < 6) {
......
<template> <template>
<div class="app-container"> <div class="app-container">
<el-row :gutter="20"> <div class="block">
<el-col :span="10"> <el-tree
<div class="block"> :data="treeData"
<el-tree node-key="id"
:data="treeData" default-expand-all
node-key="id" :expand-on-click-node="false"
default-expand-all >
:expand-on-click-node="false" <span slot-scope="{ node, data }" class="custom-tree-node">
> <span>{{ node.label }}</span>
<span slot-scope="{ node, data }" class="custom-tree-node"> <span>
<span>{{ node.label }}</span> <el-button
<span> type="text"
<el-button size="mini"
type="text" @click="() => add(node, data)"
size="mini" >
@click="() => edit(data)" 新增
> </el-button>
编辑 <el-button
</el-button> type="text"
<el-button size="mini"
type="text" @click="() => edit(node, data)"
size="mini" >
@click="() => remove(node, data)" 编辑
> </el-button>
删除 <el-button
</el-button> type="text"
</span> size="mini"
</span> @click="() => remove(node, data)"
</el-tree> >
</div> 删除
</el-col> </el-button>
<el-col :span="8"> </span>
<el-form ref="form" :model="form" label-width="120px"> </span>
<el-form-item label="父级菜单"> </el-tree>
<el-cascader </div>
v-model="form.parent_id" <el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible">
:options="treeSelectData" <el-form ref="menuForm" :model="form" label-width="120px">
:props="{ checkStrictly: true }" <el-form-item label="父级菜单" prop="parent_id">
:show-all-levels="false" <el-cascader
:emit-path="false" ref="parent_select"
clearable v-model="form.parent_id"
:options="treeSelectData"
:props="{ expandTrigger: 'hover' ,checkStrictly: true ,emitPath:false}"
:show-all-levels="false"
clearable
/>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="输入标题" />
</el-form-item>
<el-form-item label="前端页面" prop="key">
<el-select v-model="form.key" placeholder="请选择前端页面">
<el-option
v-for="item in routePath"
:key="item.key"
:label="item.path"
:value="item.key"
/> />
</el-form-item> </el-select>
<el-form-item label="标题"> </el-form-item>
<el-input v-model="form.title" placeholder="输入标题" /> <el-form-item label="后端接口地址" prop="path">
</el-form-item> <el-input v-model="form.path" placeholder="输入接口地址" />
<el-form-item label="路由地址"> </el-form-item>
<el-select v-model="form.key" placeholder="请选择路由地址"> <el-form-item label="图标" prop="icon">
<el-option <el-input v-model="form.icon" placeholder="输入图标" />
v-for="item in routePath" </el-form-item>
:key="item.key" <el-form-item label="是否菜单" prop="is_menu">
:label="item.path" <el-radio-group v-model="form.is_menu">
:value="item.key" <el-radio :label="1"></el-radio>
/> <el-radio :label="0"></el-radio>
</el-select> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="后端接口地址"> </el-form>
<el-input v-model="form.path" placeholder="输入接口地址" /> <div style="text-align:right;">
</el-form-item> <el-button type="danger" @click="dialogFormVisible=false">取消</el-button>
<el-form-item label="图标"> <el-button type="primary" @click="onSubmit">提交</el-button>
<el-input v-model="form.icon" placeholder="输入图标" /> </div>
</el-form-item>
<el-form-item label="是否菜单"> </el-dialog>
<el-radio-group v-model="form.is_menu">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">提交</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div> </div>
</template> </template>
<script> <script>
import { menuList, menuAdd, menuDel } from '@/api/menu' import { menuList, menuAdd, menuDel, menuEdit, menuDetail } from '@/api/menu'
import { constantRouterPath } from '@/router/config' import { constantRouterComponents } from '@/router/config'
import { Message } from 'element-ui' import { Message } from 'element-ui'
export default { export default {
name: 'List', name: 'List',
data() { data() {
return { return {
form_menu_id: 0,
form_parent_menu_id: 0,
dialogFormVisible: false,
dialogStatus: '',
textMap: {
update: 'Edit',
create: 'Create'
},
treeData: [], treeData: [],
treeSelectData: [], treeSelectData: [],
form: { form: {
parent_id: 0, parent_id: '',
title: '', title: '',
key: '', key: 'Dashboard',
path: '', path: '',
icon: '', icon: '',
is_menu: 1 is_menu: 1
...@@ -101,10 +115,10 @@ export default { ...@@ -101,10 +115,10 @@ export default {
computed: { computed: {
routePath() { routePath() {
const map = [] const map = []
for (const key in constantRouterPath) { for (const key in constantRouterComponents) {
map.push({ map.push({
key: key, key: key,
path: constantRouterPath[key] path: key
}) })
} }
return map return map
...@@ -114,22 +128,29 @@ export default { ...@@ -114,22 +128,29 @@ export default {
this.init() this.init()
}, },
methods: { methods: {
resetForm() {
this.form = {
parent_id: '',
title: '',
key: 'Dashboard',
path: '',
icon: '',
is_menu: 1
}
},
init() { init() {
// 注册功能页路由
menuList().then(res => { menuList().then(res => {
const menus = res.data const menus = res.data
const childrenNav = [] const childrenNav = []
this.listToTree(menus, childrenNav, 0) this.listToTree(menus, childrenNav, 0)
this.treeData = childrenNav this.treeData = childrenNav
const treeRootSelectData = {
value: 0,
label: 'ROOT',
children: []
}
const treeChildSelectData = []
const treeChildSelectData = []
this.listToSelectTree(menus, treeChildSelectData, 0) this.listToSelectTree(menus, treeChildSelectData, 0)
treeRootSelectData.children = treeChildSelectData this.treeSelectData = treeChildSelectData
this.treeSelectData = [treeRootSelectData]
this.dialogFormVisible = false
}) })
}, },
...@@ -174,14 +195,49 @@ export default { ...@@ -174,14 +195,49 @@ export default {
} }
}) })
}, },
add(node, data) {
this.resetForm()
this.form_parent_menu_id = data.id
this.form = {
parent_id: this.form_parent_menu_id,
title: '',
key: 'Dashboard',
path: '',
icon: '',
is_menu: 1
}
this.dialogStatus = 'create'
this.dialogFormVisible = true
this.$nextTick(() => {
this.$refs['menuForm'].clearValidate()
})
},
edit(node, data) { edit(node, data) {
const parent = node.parent this.resetForm()
const children = parent.data.children || parent.data this.form_menu_id = data.id
const index = children.findIndex(d => d.id === data.id) this.dialogStatus = 'edit'
children.splice(index, 1) this.dialogFormVisible = true
this.$nextTick(() => {
this.$refs['menuForm'].clearValidate()
})
menuDetail(this.form_menu_id).then(res => {
const menu_data = res.data
this.form = {
parent_id: menu_data.parent_id,
title: menu_data.title,
key: menu_data.key,
path: menu_data.path,
icon: menu_data.icon,
is_menu: menu_data.is_menu
}
})
}, },
remove(node, data) { remove(node, data) {
this.$confirm('确认删除?') this.$confirm('确定要删除嘛?', '删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(_ => { .then(_ => {
const menu_id = data.id const menu_id = data.id
menuDel(menu_id).then(res => { menuDel(menu_id).then(res => {
...@@ -194,10 +250,18 @@ export default { ...@@ -194,10 +250,18 @@ export default {
}).catch(_ => {}) }).catch(_ => {})
}, },
onSubmit() { onSubmit() {
menuAdd(this.form).then(res => { if (this.dialogStatus === 'create') {
Message.success('已添加') menuAdd(this.form).then(res => {
this.init() this.$refs['menuForm'].resetFields()
}) Message.success('已添加')
this.init()
})
} else {
menuEdit(this.form_menu_id, this.form).then(res => {
Message.success('已修改')
this.init()
})
}
} }
} }
} }
......
<template> <template>
<page-header-wrapper> <div class="app-container">
<a-card :bordered="false"> <el-button type="primary" @click="handleAddRole">添加</el-button>
<div class="table-page-search-wrapper">
<a-form layout="inline">
<a-row :gutter="48">
<a-col :md="8" :sm="24">
<a-form-item label="规则编号">
<a-input v-model="queryParam.id" placeholder=""/>
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<a-form-item label="使用状态">
<a-select v-model="queryParam.status" placeholder="请选择" default-value="0">
<a-select-option value="0">全部</a-select-option>
<a-select-option value="1">关闭</a-select-option>
<a-select-option value="2">运行中</a-select-option>
</a-select>
</a-form-item>
</a-col>
<template v-if="advanced">
<a-col :md="8" :sm="24">
<a-form-item label="调用次数">
<a-input-number v-model="queryParam.callNo" style="width: 100%"/>
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<a-form-item label="更新日期">
<a-date-picker v-model="queryParam.date" style="width: 100%" placeholder="请输入更新日期"/>
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<a-form-item label="使用状态">
<a-select v-model="queryParam.useStatus" placeholder="请选择" default-value="0">
<a-select-option value="0">全部</a-select-option>
<a-select-option value="1">关闭</a-select-option>
<a-select-option value="2">运行中</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :md="8" :sm="24">
<a-form-item label="使用状态">
<a-select placeholder="请选择" default-value="0">
<a-select-option value="0">全部</a-select-option>
<a-select-option value="1">关闭</a-select-option>
<a-select-option value="2">运行中</a-select-option>
</a-select>
</a-form-item>
</a-col>
</template>
<a-col :md="!advanced && 8 || 24" :sm="24">
<span class="table-page-search-submitButtons" :style="advanced && { float: 'right', overflow: 'hidden' } || {} ">
<a-button type="primary" @click="$refs.table.refresh(true)">查询</a-button>
<a-button style="margin-left: 8px" @click="() => this.queryParam = {}">重置</a-button>
<a @click="toggleAdvanced" style="margin-left: 8px">
{{ advanced ? '收起' : '展开' }}
<a-icon :type="advanced ? 'up' : 'down'"/>
</a>
</span>
</a-col>
</a-row>
</a-form>
</div>
<div class="table-operator">
<a-button type="primary" icon="plus" @click="handleAdd">新建</a-button>
<a-dropdown v-action:edit v-if="selectedRowKeys.length > 0">
<a-menu slot="overlay">
<a-menu-item key="1"><a-icon type="delete" />删除</a-menu-item>
<!-- lock | unlock -->
<a-menu-item key="2"><a-icon type="lock" />锁定</a-menu-item>
</a-menu>
<a-button style="margin-left: 8px">
批量操作 <a-icon type="down" />
</a-button>
</a-dropdown>
</div>
<s-table <el-table :data="data" style="width: 100%;margin-top:30px;" border>
ref="table" <el-table-column align="center" label="ID" width="220">
size="default" <template slot-scope="scope">
rowKey="key" {{ scope.row.id }}
:columns="columns" </template>
:data="loadData" </el-table-column>
:alert="true" <el-table-column align="center" label="角色名称" width="220">
:rowSelection="rowSelection" <template slot-scope="scope">
showPagination="auto" {{ scope.row.name }}
> </template>
<span slot="serial" slot-scope="text, record, index"> </el-table-column>
{{ index + 1 }} <el-table-column align="header-center" label="角色描述">
</span> <template slot-scope="scope">
<span slot="status" slot-scope="text"> {{ scope.row.slug }}
<a-badge :status="text | statusTypeFilter" :text="text | statusFilter" /> </template>
</span> </el-table-column>
<span slot="description" slot-scope="text"> <el-table-column align="center" label="操作">
<ellipsis :length="4" tooltip>{{ text }}</ellipsis> <template slot-scope="scope">
</span> <el-button type="primary" size="small" @click="handleEdit(scope)">编辑</el-button>
<el-button v-if="!scope.row.is_admin" type="danger" size="small" @click="handleDelete(scope)">删除</el-button>
</template>
</el-table-column>
</el-table>
<span slot="action" slot-scope="text, record"> <el-dialog :visible.sync="dialogVisible" :title="dialogType==='edit'?'编辑':'添加'">
<template> <el-form :model="role" label-width="80px" label-position="left">
<a @click="handleEdit(record)">配置</a> <el-form-item label="名称">
<a-divider type="vertical" /> <el-input v-model="role.name" placeholder="角色名称" />
<a @click="handleSub(record)">订阅报警</a> </el-form-item>
</template> <el-form-item label="描述">
</span> <el-input
</s-table> v-model="role.slug"
:autosize="{ minRows: 2, maxRows: 4}"
<create-form type="textarea"
ref="createModal" placeholder="角色描述"
:visible="visible" />
:loading="confirmLoading" </el-form-item>
:model="mdl" <el-form-item v-if="!role.is_admin" label="权限">
@cancel="handleCancel" <el-tree
@ok="handleOk" ref="tree"
/> :check-strictly="checkStrictly"
<step-by-step-modal ref="modal" @ok="handleOk"/> default-expand-all
</a-card> :expand-on-click-node="false"
</page-header-wrapper> :data="menuTree"
:props="defaultProps"
show-checkbox
node-key="id"
class="permission-tree"
/>
</el-form-item>
</el-form>
<div style="text-align:right;">
<el-button type="danger" @click="dialogVisible=false">取消</el-button>
<el-button type="primary" @click="confirmRole">提交</el-button>
</div>
</el-dialog>
</div>
</template> </template>
<script> <script>
import moment from 'moment' import { roleAdd, roleDel, roleEdit, roleList } from '@/api/role'
import { STable, Ellipsis } from '@/components' import { deepClone } from '@/utils/util'
import { getRoleList, getServiceList } from '@/api/manage' import { menuList } from '@/api/menu'
import { Message } from 'element-ui'
// import StepByStepModal from './modules/StepByStepModal'
// import CreateForm from './modules/CreateForm'
const columns = [
{
title: '#',
scopedSlots: { customRender: 'serial' }
},
{
title: '规则编号',
dataIndex: 'no'
},
{
title: '描述',
dataIndex: 'description',
scopedSlots: { customRender: 'description' }
},
{
title: '服务调用次数',
dataIndex: 'callNo',
sorter: true,
needTotal: true,
customRender: (text) => text + ' 次'
},
{
title: '状态',
dataIndex: 'status',
scopedSlots: { customRender: 'status' }
},
{
title: '更新时间',
dataIndex: 'updatedAt',
sorter: true
},
{
title: '操作',
dataIndex: 'action',
width: '150px',
scopedSlots: { customRender: 'action' }
}
]
const statusMap = {
0: {
status: 'default',
text: '关闭'
},
1: {
status: 'processing',
text: '运行中'
},
2: {
status: 'success',
text: '已上线'
},
3: {
status: 'error',
text: '异常'
}
}
export default { export default {
name: 'TableList', data() {
components: {
STable,
Ellipsis
// CreateForm,
// StepByStepModal
},
data () {
this.columns = columns
return { return {
// create model role: {
visible: false, name: '',
confirmLoading: false, slug: '',
mdl: null, menus: [],
// 高级搜索 展开/关闭 is_admin: false
advanced: false,
// 查询参数
queryParam: {},
// 加载数据方法 必须为 Promise 对象
loadData: parameter => {
const requestParameters = Object.assign({}, parameter, this.queryParam)
console.log('loadData request parameters:', requestParameters)
return getServiceList(requestParameters)
.then(res => {
return res.result
})
}, },
selectedRowKeys: [], menuTreeData: [],
selectedRows: [] data: [],
} dialogVisible: false,
}, dialogType: 'new',
filters: { checkStrictly: false,
statusFilter (type) { defaultProps: {
return statusMap[type].text children: 'children',
}, label: 'title'
statusTypeFilter (type) { }
return statusMap[type].status
} }
}, },
created () {
getRoleList({ t: new Date() })
},
computed: { computed: {
rowSelection () { menuTree() {
return { return this.menuTreeData
selectedRowKeys: this.selectedRowKeys,
onChange: this.onSelectChange
}
} }
}, },
created() {
this.getRoles()
this.getMenu()
},
methods: { methods: {
handleAdd () { async getMenu() {
this.mdl = null const res = await menuList()
this.visible = true const menus = res.data
const childrenNav = []
this.listToTree(menus, childrenNav, 0)
this.menuTreeData = childrenNav
}, },
handleEdit (record) { listToTree(list, tree, parentId) {
this.visible = true list.forEach(item => {
this.mdl = { ...record } // 判断是否为父级菜单
}, if (item.parent_id === parentId) {
handleOk () { const child = {
const form = this.$refs.createModal.form ...item,
this.confirmLoading = true // key: item.key || item.name,
form.validateFields((errors, values) => { children: []
if (!errors) {
console.log('values', values)
if (values.id > 0) {
// 修改 e.g.
new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 1000)
}).then(res => {
this.visible = false
this.confirmLoading = false
// 重置表单数据
form.resetFields()
// 刷新表格
this.$refs.table.refresh()
this.$message.info('修改成功')
})
} else {
// 新增
new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 1000)
}).then(res => {
this.visible = false
this.confirmLoading = false
// 重置表单数据
form.resetFields()
// 刷新表格
this.$refs.table.refresh()
this.$message.info('新增成功')
})
} }
} else { // 迭代 list, 找到当前菜单相符合的所有子菜单
this.confirmLoading = false this.listToTree(list, child.children, item.id)
// 删掉不存在 children 值的属性
if (child.children.length <= 0) {
delete child.children
}
// 加入到树中
tree.push(child)
} }
}) })
}, },
handleCancel () {
this.visible = false
const form = this.$refs.createModal.form async getRoles() {
form.resetFields() // 清理表单数据(可不做) const res = await roleList()
this.data = res.data
}, },
handleSub (record) {
if (record.status !== 0) { handleAddRole() {
this.$message.info(`${record.no} 订阅成功`) this.role = Object.assign({})
} else { if (this.$refs.tree) {
this.$message.error(`${record.no} 订阅失败,规则已关闭`) this.$refs.tree.setCheckedNodes([])
} }
this.dialogType = 'new'
this.dialogVisible = true
}, },
onSelectChange (selectedRowKeys, selectedRows) { handleEdit(scope) {
this.selectedRowKeys = selectedRowKeys this.dialogType = 'edit'
this.selectedRows = selectedRows this.dialogVisible = true
this.checkStrictly = true
this.role = deepClone(scope.row)
console.log(this.role)
this.$nextTick(() => {
// set checked state of a node not affects its father and child nodes
this.checkStrictly = false
if (!this.role.is_admin) {
this.$refs.tree.setCheckedKeys(this.role.menus)
}
})
}, },
toggleAdvanced () { handleDelete({ $index, row }) {
this.advanced = !this.advanced this.$confirm('确定要删除嘛?', '删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async() => {
await roleDel(row.id)
this.data.splice($index, 1)
this.$message({
type: 'success',
message: '已删除'
})
})
.catch(err => { console.error(err) })
}, },
resetSearchForm () { async confirmRole() {
this.queryParam = { const isEdit = this.dialogType === 'edit'
date: moment(new Date()) const CheckedKeys = this.$refs.tree.getCheckedKeys()
const HalfCheckedKeys = this.$refs.tree.getHalfCheckedKeys()
this.role.menus = [...CheckedKeys, ...HalfCheckedKeys]
console.log(this.role.menus)
if (isEdit) {
await roleEdit(this.role.id, this.role)
for (let index = 0; index < this.data.length; index++) {
if (this.data[index].id === this.role.id) {
this.data.splice(index, 1, Object.assign({}, this.role))
break
}
}
Message.success('已修改')
} else {
const { data } = await roleAdd(this.role)
this.role.id = data.id
this.data.push(this.role)
Message.success('已添加')
} }
this.dialogVisible = false
} }
} }
} }
</script> </script>
<style lang="scss" scoped>
.app-container {
.roles-table {
margin-top: 30px;
}
.permission-tree {
margin-bottom: 30px;
}
}
</style>
<template>
<div class="app-container">
<el-button type="primary" @click="handleAddRole">添加</el-button>
<el-table :data="data" style="width: 100%;margin-top:30px;" border>
<el-table-column align="center" label="ID" width="220">
<template slot-scope="scope">
{{ scope.row.id }}
</template>
</el-table-column>
<el-table-column align="center" label="用户名" width="220">
<template slot-scope="scope">
{{ scope.row.username }}
</template>
</el-table-column>
<el-table-column align="center" label="名称" width="220">
<template slot-scope="scope">
{{ scope.row.name }}
</template>
</el-table-column>
<el-table-column align="center" label="角色" width="220">
<template slot-scope="scope">
{{ scope.row.roles.data.name }}
</template>
</el-table-column>
<el-table-column align="header-center" label="头像">
<template slot-scope="scope">
{{ scope.row.avatar }}
</template>
</el-table-column>
<el-table-column align="header-center" label="创建时间">
<template slot-scope="scope">
{{ scope.row.created_at }}
</template>
</el-table-column>
<el-table-column align="center" label="操作">
<template slot-scope="scope">
<el-button type="primary" size="small" @click="handleEdit(scope)">编辑</el-button>
<el-button v-if="!scope.row.is_admin" type="danger" size="small" @click="handleDelete(scope)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog :visible.sync="dialogVisible" :title="dialogType==='edit'?'编辑':'添加'">
<el-form :model="user" label-width="80px" label-position="left">
<el-form-item label="用户名">
<el-input v-model="user.username" placeholder="用户名" />
</el-form-item>
<el-form-item v-if="dialogType==='new'" label="密码">
<el-input v-model="user.password" placeholder="不填默认123456" />
</el-form-item>
<el-form-item v-if="dialogType==='edit'" label="密码">
<el-input v-model="user.password" placeholder="为空不修改" />
</el-form-item>
<el-form-item label="名称">
<el-input v-model="user.name" placeholder="名称" />
</el-form-item>
<el-form-item v-if="!user.is_admin" label="角色">
<el-select v-model="user.role_id" placeholder="请选择">
<el-option
v-for="item in roleData"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
<div style="text-align:right;">
<el-button type="danger" @click="dialogVisible=false">取消</el-button>
<el-button type="primary" @click="confirmRole">提交</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { userAdd, userDel, userEdit, userList } from '@/api/user'
import { deepClone } from '@/utils/util'
import { Message } from 'element-ui'
import { roleList } from '@/api/role'
export default {
data() {
return {
user: {
username: '',
password: '',
name: '',
avatar: '',
role_id: 1,
is_admin: false
},
data: [],
roleData: [],
dialogVisible: false,
dialogType: 'new',
checkStrictly: false,
defaultProps: {
children: 'children',
label: 'title'
}
}
},
computed: {
},
created() {
this.getRoles()
this.getUsers()
},
methods: {
async getRoles() {
const res = await roleList()
this.roleData = res.data
},
async getUsers() {
const res = await userList({ include: 'roles' })
this.data = res.data
},
handleAddRole() {
this.user = Object.assign({})
this.dialogType = 'new'
this.dialogVisible = true
},
handleEdit(scope) {
this.dialogType = 'edit'
this.dialogVisible = true
const row = deepClone(scope.row)
// this.user = row
this.user = {
id: row.id,
username: row.username,
password: '',
name: row.name,
avatar: row.avatar,
role_id: row.roles.data.id,
is_admin: row.is_admin
}
},
handleDelete({ $index, row }) {
this.$confirm('确定要删除嘛?', '删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async() => {
await userDel(row.id)
this.data.splice($index, 1)
this.$message({
type: 'success',
message: '已删除'
})
})
.catch(err => { console.error(err) })
},
async confirmRole() {
const isEdit = this.dialogType === 'edit'
if (isEdit) {
await userEdit(this.user.id, this.user)
const newData = this.user
newData.roles = {
data: {
id: this.user.role_id,
name: this.roleData.find(item => item.id === this.user.role_id).name
}
}
for (let index = 0; index < this.data.length; index++) {
if (this.data[index].id === this.user.id) {
this.data.splice(index, 1, Object.assign({}, newData))
break
}
}
Message.success('已修改')
} else {
const { data } = await userAdd(this.user)
this.user.id = data.id
const newData = this.user
newData.create_at = data.create_at
newData.roles = {
data: {
id: data.role.id,
name: data.role.name
}
}
this.data.push(newData)
Message.success('已添加')
}
this.dialogVisible = false
}
}
}
</script>
<style lang="scss" scoped>
.app-container {
.roles-table {
margin-top: 30px;
}
.permission-tree {
margin-bottom: 30px;
}
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment