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

update

parent 7b147146
......@@ -17,10 +17,12 @@
"axios": "0.18.1",
"core-js": "3.6.5",
"element-ui": "2.13.2",
"fuse.js": "^7.0.0",
"js-cookie": "2.2.0",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"screenfull": "^6.0.2",
"vue": "2.6.10",
"vue-router": "3.0.6",
"vuex": "3.1.0"
......
......@@ -16,19 +16,19 @@ export function menuAdd(parameter) {
})
}
export function menuEdit(parameter) {
export function menuEdit(id, parameter) {
return request({
url: '/menu/add',
url: '/menu/' + id + '/edit',
method: 'put',
data: parameter
})
}
export function menuDetail(parameter) {
export function menuDetail(id) {
return request({
url: '/menu/detail',
method: 'get',
data: parameter
url: '/menu/' + id + '/detail',
method: 'get'
})
}
......
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() {
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>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<router-view :key="key" />
<keep-alive :include="cachedViews">
<router-view :key="key" />
</keep-alive>
</transition>
</section>
</template>
......@@ -10,6 +12,9 @@
export default {
name: 'AppMain',
computed: {
cachedViews() {
return this.$store.state.tagsView.cachedViews
},
key() {
return this.$route.path
}
......@@ -17,17 +22,29 @@ export default {
}
</script>
<style scoped>
<style lang="scss" scoped>
.app-main {
/*50 = navbar */
/* 50= navbar 50 */
min-height: calc(100vh - 50px);
width: 100%;
position: relative;
overflow: hidden;
}
.fixed-header+.app-main {
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 lang="scss">
......
<template>
<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">
<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">
<img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
<i class="el-icon-caret-bottom" />
</div>
<el-dropdown-menu slot="dropdown" class="user-dropdown">
<el-dropdown-menu slot="dropdown">
<router-link to="/profile/index">
<el-dropdown-item>Profile</el-dropdown-item>
</router-link>
<router-link to="/">
<el-dropdown-item>
Home
</el-dropdown-item>
<el-dropdown-item>Dashboard</el-dropdown-item>
</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>
</a>
<a target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/#/">
......@@ -35,16 +49,25 @@
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
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 {
components: {
Breadcrumb,
Hamburger
Hamburger,
ErrorLog,
// Screenfull,
SizeSelect,
Search
},
computed: {
...mapGetters([
'sidebar',
'avatar'
'avatar',
'device'
])
},
methods: {
......@@ -84,6 +107,11 @@ export default {
float: left;
}
.errLog-container {
display: inline-block;
vertical-align: top;
}
.right-menu {
float: right;
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 {
},
data() {
return {
title: 'Vue Admin Template',
title: 'Vue Element Admin',
logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png'
}
}
......
......@@ -12,7 +12,7 @@
:collapse-transition="false"
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-scrollbar>
</div>
......@@ -26,16 +26,17 @@ import variables from '@/styles/variables.scss'
export default {
components: { SidebarItem, Logo },
data() {
return {
// base
menus: []
}
},
computed: {
...mapGetters([
'sidebar',
'addRouters'
'addRouters',
'sidebar'
]),
routes() {
// return this.$router.options.routes
const routes = this.addRouters.find(item => item.path === '/')
return (routes && routes.children) || []
},
activeMenu() {
const route = this.$route
const { meta, path } = route
......@@ -54,6 +55,11 @@ export default {
isCollapse() {
return !this.sidebar.opened
}
},
created() {
const routes = this.addRouters.find(item => item.path === '/')
this.menus = (routes && routes.children) || []
}
}
</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 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 @@
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<sidebar class="sidebar-container" />
<div class="main-container">
<div :class="{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 { Navbar, Sidebar, AppMain } from './components'
import RightPanel from '@/components/RightPanel'
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
export default {
name: 'Layout',
components: {
AppMain,
Navbar,
RightPanel,
Settings,
Sidebar,
AppMain
TagsView
},
mixins: [ResizeMixin],
computed: {
sidebar() {
return this.$store.state.app.sidebar
},
device() {
return this.$store.state.app.device
},
fixedHeader() {
return this.$store.state.settings.fixedHeader
},
...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,
......@@ -59,11 +66,13 @@ export default {
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar{
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
......
......@@ -27,26 +27,21 @@ router.beforeEach(async(to, from, next) => {
NProgress.done()
} else {
if (!store.getters.menuSet) {
store.dispatch('GenerateRoutes', {}).then(() => {
// 动态添加可访问路由表
// 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)
try {
await store.dispatch('user/getInfo')
if (to.path === redirect) {
// set the replace: true so the navigation will not leave a history record
store.dispatch('GenerateRoutes', {}).then(() => {
resetRouter() // 重置路由 防止退出重新登录或者 token 过期后页面未刷新,导致的路由重复添加
router.addRoutes(store.getters.addRouters)
// 请求带有 redirect 重定向时,登录自动重定向到该地址
next({ ...to, replace: true })
} else {
// 跳转到目的路由
next({ path: redirect })
}
})
})
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('user/resetToken')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
} else {
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 = {
// 权限管理
// SystemPowerRoleList: () => import('@/views/system/power/role/List'),
// UserList: () => import('@/views/power/user/list'),
'RouteView': RouteView,
'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')
}
import * as userService from '@/api/user'
import { constantRouterComponents, constantRouterPath } from '@/router/config'
import { constantRouterComponents } from '@/router/config'
import Layout from '@/layout'
import RouteView from '@/layout/RouteView'
// 前端未找到页面路由(固定不用改)
const notFoundRouter = {
......@@ -11,12 +12,12 @@ const notFoundRouter = {
// 根级菜单
const rootRouter = {
path: '/',
router: '/',
key: 'Root',
component: Layout,
redirect: '/dashboard',
children: [{
path: 'dashboard',
router: '/dashboard',
key: 'Dashboard',
title: '首页',
icon: 'dashboard',
......@@ -63,11 +64,11 @@ export const generatorDynamicRouter = () => {
export const generator = (routerMap, parent) => {
return routerMap.map(item => {
// 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 currentRouter = {
// 如果路由设置了 path,则作为默认 path,否则 路由地址 动态拼接生成如 /dashboard/workplace
path: path,
path: item.router || (item.key + '_' + (item.id || '')),
// 路由名称,建议唯一
name: item.key + (item.id || ''),
// 该路由对应页面的 组件 :方案1
......@@ -100,12 +101,37 @@ export const generator = (routerMap, parent) => {
// Recursion
currentRouter.children = generator(item.children, currentRouter)
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
})
}
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 = {
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
......@@ -12,5 +23,13 @@ module.exports = {
* @type {boolean} true | false
* @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 = {
sidebar: state => state.app.sidebar,
size: state => state.app.size,
device: state => state.app.device,
visitedViews: state => state.tagsView.visitedViews,
cachedViews: state => state.tagsView.cachedViews,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
addRouters: state => state.permission.addRouters,
multiTab: state => state.app.multiTab,
menuSet: state => state.permission.menuSet
menuSet: state => state.permission.menuSet,
errorLogs: state => state.errorLog.logs
}
export default getters
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import app from './modules/app'
import settings from './modules/settings'
import user from './modules/user'
import permission from './modules/async-router'
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({
modules: {
app,
settings,
user,
permission
},
modules,
getters
})
......
......@@ -5,7 +5,8 @@ const state = {
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
withoutAnimation: false
},
device: 'desktop'
device: 'desktop',
size: Cookies.get('size') || 'medium'
}
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'
const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings
const state = {
theme: variables.theme,
showSettings: showSettings,
tagsView: tagsView,
fixedHeader: fixedHeader,
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) {
* @param {string} url
* @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) {
const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
if (!search) {
......@@ -115,3 +178,180 @@ export function param2Obj(url) {
})
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'
import { Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// create an axios instance
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
if (store.getters.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
......@@ -47,6 +44,7 @@ service.interceptors.response.use(
},
error => {
console.log('err' + error) // for debug
const data = error.response.data
const token = getToken()
switch (error.response.status) {
case 422:
......@@ -63,7 +61,7 @@ service.interceptors.response.use(
break
default:
Message({
message: error.message,
message: data.message || error.message,
type: 'error',
duration: 5 * 1000
})
......
......@@ -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 @@
</template>
<script>
import { validUsername } from '@/utils/validate'
// import { validUsername } from '@/utils/validate'
export default {
name: 'Login',
data() {
const validateUsername = (rule, value, callback) => {
if (!validUsername(value)) {
callback(new Error('Please enter the correct user name'))
} else {
callback()
}
callback()
}
const validatePassword = (rule, value, callback) => {
if (value.length < 6) {
......
<template>
<div class="app-container">
<el-row :gutter="20">
<el-col :span="10">
<div class="block">
<el-tree
:data="treeData"
node-key="id"
default-expand-all
:expand-on-click-node="false"
>
<span slot-scope="{ node, data }" class="custom-tree-node">
<span>{{ node.label }}</span>
<span>
<el-button
type="text"
size="mini"
@click="() => edit(data)"
>
编辑
</el-button>
<el-button
type="text"
size="mini"
@click="() => remove(node, data)"
>
删除
</el-button>
</span>
</span>
</el-tree>
</div>
</el-col>
<el-col :span="8">
<el-form ref="form" :model="form" label-width="120px">
<el-form-item label="父级菜单">
<el-cascader
v-model="form.parent_id"
:options="treeSelectData"
:props="{ checkStrictly: true }"
:show-all-levels="false"
:emit-path="false"
clearable
<div class="block">
<el-tree
:data="treeData"
node-key="id"
default-expand-all
:expand-on-click-node="false"
>
<span slot-scope="{ node, data }" class="custom-tree-node">
<span>{{ node.label }}</span>
<span>
<el-button
type="text"
size="mini"
@click="() => add(node, data)"
>
新增
</el-button>
<el-button
type="text"
size="mini"
@click="() => edit(node, data)"
>
编辑
</el-button>
<el-button
type="text"
size="mini"
@click="() => remove(node, data)"
>
删除
</el-button>
</span>
</span>
</el-tree>
</div>
<el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible">
<el-form ref="menuForm" :model="form" label-width="120px">
<el-form-item label="父级菜单" prop="parent_id">
<el-cascader
ref="parent_select"
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-form-item label="标题">
<el-input v-model="form.title" placeholder="输入标题" />
</el-form-item>
<el-form-item label="路由地址">
<el-select v-model="form.key" placeholder="请选择路由地址">
<el-option
v-for="item in routePath"
:key="item.key"
:label="item.path"
:value="item.key"
/>
</el-select>
</el-form-item>
<el-form-item label="后端接口地址">
<el-input v-model="form.path" placeholder="输入接口地址" />
</el-form-item>
<el-form-item label="图标">
<el-input v-model="form.icon" placeholder="输入图标" />
</el-form-item>
<el-form-item label="是否菜单">
<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>
</el-select>
</el-form-item>
<el-form-item label="后端接口地址" prop="path">
<el-input v-model="form.path" placeholder="输入接口地址" />
</el-form-item>
<el-form-item label="图标" prop="icon">
<el-input v-model="form.icon" placeholder="输入图标" />
</el-form-item>
<el-form-item label="是否菜单" prop="is_menu">
<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>
<div style="text-align:right;">
<el-button type="danger" @click="dialogFormVisible=false">取消</el-button>
<el-button type="primary" @click="onSubmit">提交</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { menuList, menuAdd, menuDel } from '@/api/menu'
import { constantRouterPath } from '@/router/config'
import { menuList, menuAdd, menuDel, menuEdit, menuDetail } from '@/api/menu'
import { constantRouterComponents } from '@/router/config'
import { Message } from 'element-ui'
export default {
name: 'List',
data() {
return {
form_menu_id: 0,
form_parent_menu_id: 0,
dialogFormVisible: false,
dialogStatus: '',
textMap: {
update: 'Edit',
create: 'Create'
},
treeData: [],
treeSelectData: [],
form: {
parent_id: 0,
parent_id: '',
title: '',
key: '',
key: 'Dashboard',
path: '',
icon: '',
is_menu: 1
......@@ -101,10 +115,10 @@ export default {
computed: {
routePath() {
const map = []
for (const key in constantRouterPath) {
for (const key in constantRouterComponents) {
map.push({
key: key,
path: constantRouterPath[key]
path: key
})
}
return map
......@@ -114,22 +128,29 @@ export default {
this.init()
},
methods: {
resetForm() {
this.form = {
parent_id: '',
title: '',
key: 'Dashboard',
path: '',
icon: '',
is_menu: 1
}
},
init() {
// 注册功能页路由
menuList().then(res => {
const menus = res.data
const childrenNav = []
this.listToTree(menus, childrenNav, 0)
this.treeData = childrenNav
const treeRootSelectData = {
value: 0,
label: 'ROOT',
children: []
}
const treeChildSelectData = []
const treeChildSelectData = []
this.listToSelectTree(menus, treeChildSelectData, 0)
treeRootSelectData.children = treeChildSelectData
this.treeSelectData = [treeRootSelectData]
this.treeSelectData = treeChildSelectData
this.dialogFormVisible = false
})
},
......@@ -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) {
const parent = node.parent
const children = parent.data.children || parent.data
const index = children.findIndex(d => d.id === data.id)
children.splice(index, 1)
this.resetForm()
this.form_menu_id = data.id
this.dialogStatus = 'edit'
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) {
this.$confirm('确认删除?')
this.$confirm('确定要删除嘛?', '删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(_ => {
const menu_id = data.id
menuDel(menu_id).then(res => {
......@@ -194,10 +250,18 @@ export default {
}).catch(_ => {})
},
onSubmit() {
menuAdd(this.form).then(res => {
Message.success('已添加')
this.init()
})
if (this.dialogStatus === 'create') {
menuAdd(this.form).then(res => {
this.$refs['menuForm'].resetFields()
Message.success('已添加')
this.init()
})
} else {
menuEdit(this.form_menu_id, this.form).then(res => {
Message.success('已修改')
this.init()
})
}
}
}
}
......
This diff is collapsed.
<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