提交 fe8a7e2c 编写于 作者: 有来技术

feat:vue-element-admin升级改造vue3

上级 0091a5fa
......@@ -433,12 +433,12 @@ export default service
<script lang="ts">
import {defineComponent, computed} from 'vue'
import {userStore} from '@/store';
import {useStore} from '@/store';
export default defineComponent({
setup() {
const store = userStore()
const store = useStore()
const num = computed(()=>{
return store.state.count
})
......
......@@ -16,6 +16,7 @@
},
"devDependencies": {
"@types/node": "^16.11.7",
"@types/nprogress": "^0.2.0",
"@vitejs/plugin-vue": "^1.9.3",
"sass": "^1.43.4",
"typescript": "^4.4.3",
......
import request from '@/utils/request'
export function getRouteList() {
return request({
url: '/youlai-admin/api/v1/menus/route',
method: 'get'
})
}
\ No newline at end of file
......@@ -14,12 +14,11 @@
<script lang="ts">
import {defineComponent, onBeforeMount, reactive, toRefs} from "vue";
import {defineComponent, onBeforeMount, reactive, toRefs,watch} from "vue";
import {compile} from 'path-to-regexp'
import {useRoute, RouteLocationMatched} from "vue-router";
import router from "@router";
export default defineComponent({
setup() {
const currentRoute = useRoute();
......@@ -29,65 +28,50 @@ export default defineComponent({
return toPath(params)
}
const state = reactive({
breadcrumbs: [] as Array<RouteLocationMatched>,
getBreadcrumb:()=>{
levelList: [] as Array<RouteLocationMatched>,
getBreadcrumb: () => {
let matched = currentRoute.matched.filter((item) => item.meta && item.meta.title)
const first = matched[0]
if (!state.isDashboard(first)) {
matched = [{path: '/dashboard', meta: {title: 'dashboard'}} as any].concat(matched)
}
state.levelList = matched.filter((item) => {
return item.mate && item.meta.title && item.meta.breadcrumb !== false
})
},
isDashboard(route: RouteLocationMatched) {
const name = route && route.name
if (!name) {
return false
}
return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
},
handleLink(item: any) {
const {redirect, path} = item
if (redirect) {
router.push(redirect)
return
}
router.push(pathCompile(path))
}
})
}
})
export default {
data() {
return {
levelList: null
}
},
watch: {
$route() {
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
// only show routes with meta.title
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
const first = matched[0]
if (!this.isDashboard(first)) {
matched = [{path: '/dashboard', meta: {title: 'Dashboard'}}].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
},
pathCompile(path) {
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
const {params} = this.$route
var toPath = pathToRegexp.compile(path)
return toPath(params)
},
handleLink(item) {
const {redirect, path} = item
if (redirect) {
this.$router.push(redirect)
watch(() => currentRoute.path, (path) => {
if (path.startsWith('/redirect/')) {
return
}
this.$router.push(this.pathCompile(path))
state.getBreadcrumb()
})
onBeforeMount(() => {
state.getBreadcrumb()
})
return {
...toRefs(state)
}
}
}
</script>
})
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
......
<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>
<template>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<router-view :key="key" />
<router-view :key="key"/>
</transition>
</section>
</template>
<script>
export default {
name: 'AppMain',
computed: {
key() {
return this.$route.path
<script lang="ts">
import {useRoute} from "vue-router";
import {defineComponent} from "vue";
export default defineComponent({
setup() {
const route = useRoute()
const key = () => {
return route.path
}
return {
key
}
}
}
})
</script>
<style scoped>
......@@ -25,7 +34,8 @@ export default {
position: relative;
overflow: hidden;
}
.fixed-header+.app-main {
.fixed-header + .app-main {
padding-top: 50px;
}
</style>
......
<template>
<div class="navbar">
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar"/>
<breadcrumb class="breadcrumb-container" />
<breadcrumb class="breadcrumb-container"/>
<div class="right-menu">
<el-dropdown class="avatar-container" trigger="click">
<div class="avatar-wrapper">
<img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
<i class="el-icon-caret-bottom" />
<i class="el-icon-caret-bottom"/>
</div>
<el-dropdown-menu slot="dropdown" class="user-dropdown">
<router-link to="/">
......@@ -32,7 +32,9 @@
</template>
<script>
import { mapGetters } from 'vuex'
import {reactive,computed,toRefs} from "vue";
import {useStore} from "@/store";
import {useRoute,useRouter} from "vue-router"
import Breadcrumb from '@/components/Breadcrumb/index.vue'
import Hamburger from '@/components/Hamburger/index.vue'
......@@ -41,19 +43,35 @@ export default {
Breadcrumb,
Hamburger
},
computed: {
...mapGetters([
'sidebar',
'avatar'
])
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
async logout() {
await this.$store.dispatch('user/logout')
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
setup() {
const store = useStore()
const route = useRoute()
const router = useRouter()
const sidebar = computed(() => {
return store.state.app.sidebar
})
const device = computed(() => {
return store.state.app.device.toString()
})
const avatar = computed(() => {
return store.state.user.avatar
})
const state=reactive({
toggleSideBar:()=>{
store.dispatch('app/toggleSideBar', false)
},
logout:()=>{
store.dispatch('user/logout')
router.push(`/login?redirect=${route.fullPath}`)
}
})
return{
sidebar,
device,
avatar,
...toRefs(state)
}
}
}
......@@ -65,7 +83,7 @@ export default {
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
.hamburger-container {
line-height: 46px;
......@@ -73,7 +91,7 @@ export default {
float: left;
cursor: pointer;
transition: background .3s;
-webkit-tap-highlight-color:transparent;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(0, 0, 0, .025)
......
<template>
<div class="drawer-container">
<div>
<h3 class="drawer-title">系统布局配置</h3>
<div class="drawer-item">
<span>主题色</span>
<theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange"/>
</div>
<div class="drawer-item">
<span>开启 Tags-View</span>
<el-switch v-model="tagsView" class="drawer-switch"/>
</div>
<div class="drawer-item">
<span>固定 Header</span>
<el-switch v-model="fixedHeader" class="drawer-switch"/>
</div>
<div class="drawer-item">
<span>侧边栏 Logo</span>
<el-switch v-model="sidebarLogo" class="drawer-switch"/>
</div>
</div>
</div>
</template>
<script>
import ThemePicker from '@/components/ThemePicker/index.vue'
import {defineComponent, reactive, toRefs, watch} from "vue"
import {useStore} from '@/store'
export default defineComponent({
components: {ThemePicker},
setup() {
const store = useStore()
const state = reactive({
fixedHeader:store.state.settings.fixedHeader,
tagsView:store.state.settings.tagsView,
sidebarLogo:store.state.settings.sidebarLogo,
themeChange: (val) => {
store.dispatch('settings/changeSetting', { key: 'theme', val })
}
})
watch(()=>state.fixedHeader,(value)=>{
store.dispatch('settings/changeSetting',{ key: 'fixedHeader', value })
})
watch(() => state.tagsView, (value) => {
store.dispatch('settings/changeSetting', { key: 'showTagsView', value })
})
watch(() => state.sidebarLogo, (value) => {
store.dispatch('settings/changeSetting', { key: 'showSidebarLogo', value })
})
return {
...toRefs(state)
}
}
})
</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
}
.job-link {
display: block;
position: absolute;
width: 100%;
left: 0;
bottom: 0;
}
}
</style>
<template>
<div :class="{'has-logo':showLogo}">
<logo v-if="showLogo" :collapse="isCollapse" />
<logo v-if="showLogo" :collapse="isCollapse"/>
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
<sidebar-item v-for="route in routes" :key="route.path" :item="route"
:base-path="route.path"/>
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Logo from './Logo.vue'
import {computed, defineComponent} from "vue";
import SidebarItem from './SidebarItem.vue'
import SidebarLogo from './Logo.vue'
import variables from '@styles/variables.scss'
import {useStore} from '@/store'
import {useRoute} from 'vue-router'
export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'sidebar'
]),
routes() {
return this.$router.options.routes
},
activeMenu() {
const route = this.$route
const { meta, path } = route
export default defineComponent({
components: {
SidebarItem,
SidebarLogo
},
setup() {
const store = useStore()
const route = useRoute()
const sidebar = computed(() => {
return store.state.app.sidebar
})
const showLogo = computed(() => {
return store.state.settings.sidebarLogo
})
const activeMenu = computed(() => {
const {meta, path} = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
})
const isCollapse = computed(() => {
return !sidebar.value.opened
})
return {
sidebar,
route,
showLogo,
variables,
activeMenu,
isCollapse
}
}
}
})
</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)"
>
{{ generateTitle(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)">{{ $t('tagsView.refresh') }}</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">{{ $t('tagsView.close') }}</li>
<li @click="closeOthersTags">{{ $t('tagsView.closeOthers') }}</li>
<li @click="closeAllTags(selectedTag)">{{ $t('tagsView.closeAll') }}</li>
</ul>
</div>
</template>
<script>
import ScrollPane from './ScrollPane'
import { generateTitle } from '@/utils/i18n'
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.routes
}
},
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: {
generateTitle, // generateTitle by vue-i18n
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>
......@@ -2,54 +2,63 @@
<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 ResizeMixin from './mixin/ResizeHandler'
import {computed, defineComponent, onBeforeMount, onBeforeUnmount, onMounted, reactive, toRefs} from "vue";
import {Navbar, Sidebar, AppMain} from './components'
import {
sidebar,
device,
resizeMounted,
addEventListenerOnResize,
removeEventListenerResize,
watchRouter
} from './mixin/ResizeHandler'
import {useStore} from "@store";
export default {
export default defineComponent({
name: 'Layout',
components: {
Navbar,
Sidebar,
AppMain
},
mixins: [ResizeMixin],
computed: {
sidebar() {
return this.$store.state.app.sidebar
},
device() {
return this.$store.state.app.device
},
fixedHeader() {
return true
},
classObj() {
setup() {
const store = useStore()
const state = reactive({
handleClickOutside: () => {
store.dispatch('app/closeSideBar', false)
}
})
const classObj = computed(() => {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
hideSidebar: !sidebar.opened,
openSidebar: sidebar.opened,
withoutAnimation: sidebar.withoutAnimation,
mobile: device === 'mobile'
}
}
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
})
}
}
</script>
})
</script>
<style lang="scss" scoped>
@import "@styles/mixin.scss";
@import "@styles/variables.scss";
......@@ -59,11 +68,13 @@ export default {
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar{
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
......
import {userStore} from '@/store'
import {useStore} from '@/store'
import {computed, watch} from "vue";
import { useRoute } from 'vue-router'
const store = userStore()
const store = useStore()
const {body} = document
const WIDTH = 992 // refer to Bootstrap's responsive design
......
import router from "@router";
import NProgress from 'nprogress';
import {Local} from "@utils/storage";
import {useStore} from "@store";
import {ElMessage} from "element-plus";
NProgress.configure({showSpinner: false})
// 白名单
const whiteList = ['/login', '/auth-redirect']
router.beforeEach(async (to, form, next) => {
NProgress.start()
const store = useStore()
const hasToken = Local.get(`token`)
if (hasToken) {
// 如果登录成功,跳转到首页
if (to.path === '/login') {
next({path: '/'})
NProgress.done()
} else {
const hasGetUserInfo = store.state.user.roles.length > 0
if (hasGetUserInfo) {
next()
} else {
try {
await store.dispatch('user/getUserInfo')
const roles = store.state.user.roles
await store.dispatch('permission/generateRoutes', roles)
store.state.permission.addRoutes.forEach(route => {
router.addRoute(route)
})
next({...to, replace: true})
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('user/resetToken')
ElMessage.error(error as any || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
NProgress.done()
})
\ No newline at end of file
import {createRouter, createWebHashHistory, RouteRecordRaw} from 'vue-router'
import Layout from '@/layout/index.vue'
const routes: Array<RouteRecordRaw> = [
export const constantRoutes: Array<RouteRecordRaw> = [
{
path: '/redirect',
component: Layout,
meta: {hidden: true},
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/redirect/index.vue')
}
]
},
{
path: '/login',
name: 'Login',
component: () => import('@views/login/index.vue'),
component: () => import('@/views/login/index.vue'),
meta: {title: '登录'}
}, {
},
{
path: '/404',
component: () => import('@/views/error-page/404.vue'),
meta: {hidden: true}
},
{
path: '/401',
component: () => import('@/views/error-page/401.vue'),
meta: {hidden: true}
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
......@@ -22,9 +44,15 @@ const routes: Array<RouteRecordRaw> = [
}
]
const router = createRouter({
history: createWebHashHistory(),
routes: routes
routes: constantRoutes
})
export function resetRouter() {
const newRouter = router;
(router as any).matcher = (newRouter as any).matcher
}
export default router
\ No newline at end of file
interface DefaultSettings {
title: string,
showSettings: boolean,
tagsView: boolean,
fixedHeader: boolean,
sidebarLogo: boolean,
errorLog: string
}
const defaultSettings: DefaultSettings = {
title: 'vue3-element-admin',
showSettings: false,
tagsView: true,
fixedHeader: false,
sidebarLogo: false,
errorLog: 'production'
}
export default defaultSettings
\ No newline at end of file
......@@ -21,7 +21,7 @@ export const key: InjectionKey<Store<RootStateTypes>> = Symbol()
export const store = createStore<RootStateTypes>({modules})
export function userStore(){
export function useStore(){
return baseUseStore(key)
}
import {RouteRecordRaw} from "vue-router";
// 接口类型声明
export interface UserState {
token: string,
......@@ -16,8 +18,24 @@ export interface AppState {
}
}
export interface SettingState {
theme: string,
tagsView: boolean,
fixedHeader: boolean,
showSettings: boolean,
sidebarLogo: boolean
}
export interface PermissionState{
routes:RouteRecordRaw[]
addRoutes: RouteRecordRaw[]
}
// 顶级类型声明
export interface RootStateTypes {
user: UserState,
app:AppState
app: AppState,
setting: SettingState,
permission:PermissionState
}
\ No newline at end of file
import {Module} from "vuex";
import {RootStateTypes, AppState} from "@store/interface";
import {AppState,RootStateTypes} from "@store/interface";
import {Local} from "@utils/storage";
const appModule: Module<AppState, RootStateTypes> = {
......
import {Module} from "vuex";
import {PermissionState, RootStateTypes} from "@store/interface";
import {RouteRecordRaw} from 'vue-router'
import {constantRoutes} from '@/router'
import {getRouteList} from "@api/system/menu";
const hasPermission = (roles: string[], route: RouteRecordRaw) => {
// 超级管理员放行
if (roles.includes('ROOT')) {
return true
}
if (route.meta && route.meta.roles) {
return roles.some(role => {
if (route.meta?.roles !== undefined) {
return (route.meta.roles as string[]).includes(role);
}
})
} else {
return true
}
}
export const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
const res: RouteRecordRaw[] = []
routes.forEach(route => {
const tmp = {...route}
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
const permissionModule: Module<PermissionState, RootStateTypes> = {
namespaced: true,
state: {
routes: [],
addRoutes: []
},
mutations: {
SET_ROUTES: (state: PermissionState, routes: RouteRecordRaw[]) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
},
actions: {
generateRoutes({commit}, roles: string[]) {
return new Promise((resolve, reject) => {
getRouteList().then(response => {
const asyncRoutes = response.data
let accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
}).catch(error => {
reject(error)
})
})
}
}
}
export default permissionModule;
import {Module} from "vuex";
import {SettingState, RootStateTypes} from "@store/interface";
import defaultSettings from '../../settings'
const {showSettings, tagsView, fixedHeader, sidebarLogo} = defaultSettings
const settingModule: Module<SettingState, RootStateTypes> = {
namespaced: true,
state: {
theme: '',
showSettings: showSettings,
tagsView: tagsView,
fixedHeader: fixedHeader,
sidebarLogo: sidebarLogo,
},
mutations: {
CHANGE_SETTING: (state: SettingState, payload: { key: string, value: any }) => {
const {key, value} = payload
switch (key) {
case 'theme':
state.theme = value
break
case 'showSettings':
state.showSettings = value
break
case 'fixedHeader':
state.fixedHeader = value
break
case 'tagsView':
state.tagsView = value
break
case 'sidebarLogo':
state.sidebarLogo = value
break
default:
break
}
}
},
actions: {
changeSetting({commit}, data) {
commit('CHANGE_SETTING', data)
}
}
}
export default settingModule;
import {Module} from "vuex";
import {UserState, RootStateTypes} from "@store/interface";
import {Local} from "@utils/storage";
import {getUserInfo, login, logout} from "@api/login"
import {getUserInfo, login, logout} from "@api/login";
import {resetRouter} from "@router";
const getDefaultState = () => {
return {
......@@ -107,13 +109,24 @@ const userModule: Module<UserState, RootStateTypes> = {
logout().then(() => {
Local.remove('token')
commit('RESET_STATE')
resetRouter()
resolve(null)
}).catch(error => {
reject(error)
})
}))
}
},
/**
* 清除 Token
*/
resetToken({commit}){
return new Promise(resolve=>{
Local.remove('token')
commit('RESET_STATE')
resolve(null)
})
}
}
}
......
<template>
<div class="errPage-container">
<el-button icon="el-icon-arrow-left" class="pan-back-btn" @click="back">
返回
</el-button>
<el-row>
<el-col :span="12">
<h1 class="text-jumbo text-ginormous">
Oops!
</h1>
gif来源<a href="https://zh.airbnb.com/" target="_blank">airbnb</a> 页面
<h2>你没有权限去该页面</h2>
<h6>如有不满请联系你领导</h6>
<ul class="list-unstyled">
<li>或者你可以去:</li>
<li class="link-type">
<router-link to="/dashboard">
回首页
</router-link>
</li>
<li class="link-type">
<a href="https://www.taobao.com/">随便看看</a>
</li>
<li><a href="#" @click.prevent="dialogVisible=true">点我看图</a></li>
</ul>
</el-col>
<el-col :span="12">
<img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream.">
</el-col>
</el-row>
<el-dialog :visible.sync="dialogVisible" title="随便看">
<img :src="ewizardClap" class="pan-img">
</el-dialog>
</div>
</template>
<script>
import errGif from '@/assets/401_images/401.gif'
export default {
name: 'Page401',
data() {
return {
errGif: errGif + '?' + +new Date(),
ewizardClap: 'https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646',
dialogVisible: false
}
},
methods: {
back() {
if (this.$route.query.noGoBack) {
this.$router.push({ path: '/dashboard' })
} else {
this.$router.go(-1)
}
}
}
}
</script>
<style lang="scss" scoped>
.errPage-container {
width: 800px;
max-width: 100%;
margin: 100px auto;
.pan-back-btn {
background: #008489;
color: #fff;
border: none!important;
}
.pan-gif {
margin: 0 auto;
display: block;
}
.pan-img {
display: block;
margin: 0 auto;
width: 100%;
}
.text-jumbo {
font-size: 60px;
font-weight: 700;
color: #484848;
}
.list-unstyled {
font-size: 14px;
li {
padding-bottom: 5px;
}
a {
color: #008489;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
</style>
<template>
<div class="wscn-http404-container">
<div class="wscn-http404">
<div class="pic-404">
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
</div>
<div class="bullshit">
<div class="bullshit__oops">OOPS!</div>
<div class="bullshit__info">All rights reserved
<a style="color:#20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a>
</div>
<div class="bullshit__headline">{{ message }}</div>
<div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div>
<a href="" class="bullshit__return-home">Back to home</a>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Page404',
computed: {
message() {
return 'The webmaster said that you can not enter this page...'
}
}
}
</script>
<style lang="scss" scoped>
.wscn-http404-container{
transform: translate(-50%,-50%);
position: absolute;
top: 40%;
left: 50%;
}
.wscn-http404 {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic-404 {
position: relative;
float: left;
width: 600px;
overflow: hidden;
&__parent {
width: 100%;
}
&__child {
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
&.mid {
width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1.2s;
}
&.right {
width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline {
font-size: 20px;
line-height: 24px;
color: #222;
font-weight: bold;
opacity: 0;
margin-bottom: 10px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
}
}
</style>
<script lang="ts">
import {defineComponent} from "vue";
import {useRoute, useRouter} from "vue-router";
export default defineComponent({
created() {
const {params, query} = useRoute()
const {path} = params
useRouter().replace({path: '/' + path, query})
},
render(h) {
return h()
}
})
</script>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册