feat:vue-element-admin升级vue3

This commit is contained in:
有来技术 2021-11-22 23:52:23 +08:00
parent fe8a7e2c31
commit 0ec8710e6f
8 changed files with 486 additions and 414 deletions

View File

@ -1,175 +0,0 @@
<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>

View File

@ -1,94 +1,99 @@
<template> <template>
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll"> <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
<slot /> <slot/>
</el-scrollbar> </el-scrollbar>
</template> </template>
<script> <script lang="ts">
const tagAndTagSpacing = 4 // tagAndTagSpacing import {defineComponent, reactive, ref, toRefs, computed, onMounted, onBeforeUnmount, getCurrentInstance} from "vue";
export default { export default defineComponent({
name: 'ScrollPane', emits: ['scroll'],
data() { setup(_, context) {
return { const scrollContainer = ref(null)
left: 0 const scrollWrapper = computed(() => {
} return (scrollContainer.value as any).$refs.wrap as HTMLElement
}, })
computed: { const {ctx} = getCurrentInstance() as any
scrollWrapper() { const tagAndTagSpacing = 4
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 const state = reactive({
let lastTag = null handleScroll: (e: WheelEvent) => {
const eventDelta = (e as any).wheelDelta || -e.deltaY * 40
scrollWrapper.value.scrollLeft = scrollWrapper.value.scrollLeft + eventDelta / 4
},
moveToCurrentTag: (currentTag: HTMLElement) => {
const container = (scrollContainer.value as any).$el as HTMLElement
const containerWidth = container.offsetWidth
const tagList = ctx.$parent.$refs.tag as any[]
let firstTag = null
let lastTag = null
// find first tag and last tag // find first tag and last tag
if (tagList.length > 0) { if (tagList.length > 0) {
firstTag = tagList[0] firstTag = tagList[0]
lastTag = tagList[tagList.length - 1] lastTag = tagList[tagList.length - 1]
} }
if (firstTag === currentTag) { if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0 scrollWrapper.value.scrollLeft = 0
} else if (lastTag === currentTag) { } else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth scrollWrapper.value.scrollLeft = scrollWrapper.value.scrollWidth - containerWidth
} else { } else {
// find preTag and nextTag // find preTag and nextTag
const currentIndex = tagList.findIndex(item => item === currentTag) const currentIndex = tagList.findIndex(item => item === currentTag)
const prevTag = tagList[currentIndex - 1] const prevTag = tagList[currentIndex - 1]
const nextTag = 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
// the tag's offsetLeft after of nextTag if (afterNextTagOffsetLeft > scrollWrapper.value.scrollLeft + containerWidth) {
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing scrollWrapper.value.scrollLeft = afterNextTagOffsetLeft - containerWidth
} else if (beforePrevTagOffsetLeft < scrollWrapper.value.scrollLeft) {
// the tag's offsetLeft before of prevTag scrollWrapper.value.scrollLeft = beforePrevTagOffsetLeft
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing }
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
} }
} }
})
const emitScroll = () => {
context.emit('scroll')
}
onMounted(() => {
scrollWrapper.value.addEventListener('scroll', emitScroll, true)
})
onBeforeUnmount(() => {
scrollWrapper.value.removeEventListener('scroll', emitScroll)
})
return {
scrollContainer,
...toRefs(state)
} }
} }
} })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.scroll-container {
.el-scrollbar__bar {
bottom: 0px;
}
.el-scrollbar__wrap {
height: 49px;
}
}
.scroll-container { .scroll-container {
white-space: nowrap; white-space: nowrap;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
::v-deep {
.el-scrollbar__bar {
bottom: 0px;
}
.el-scrollbar__wrap {
height: 49px;
}
}
} }
</style> </style>

View File

@ -2,80 +2,142 @@
<div id="tags-view-container" class="tags-view-container"> <div id="tags-view-container" class="tags-view-container">
<scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll"> <scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
<router-link <router-link
v-for="tag in visitedViews" v-for="tag in visitedViews"
ref="tag" ref="tag"
:key="tag.path" :key="tag.path"
:class="isActive(tag)?'active':''" :class="isActive(tag)?'active':''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span" tag="span"
class="tags-view-item" class="tags-view-item"
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''" @click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
@contextmenu.prevent.native="openMenu(tag,$event)" @contextmenu.prevent.native="openMenu(tag,$event)"
> >
{{ generateTitle(tag.title) }} {{ tag.meta.title }}
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" /> <span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)"/>
</router-link> </router-link>
</scroll-pane> </scroll-pane>
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu"> <ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)">{{ $t('tagsView.refresh') }}</li> <li @click="refreshSelectedTag(selectedTag)">刷新</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">{{ $t('tagsView.close') }}</li> <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li>
<li @click="closeOthersTags">{{ $t('tagsView.closeOthers') }}</li> <li @click="closeOthersTags">关闭其它</li>
<li @click="closeAllTags(selectedTag)">{{ $t('tagsView.closeAll') }}</li> <li @click="closeAllTags(selectedTag)">关闭所有</li>
</ul> </ul>
</div> </div>
</template> </template>
<script> <script lang="ts">
import ScrollPane from './ScrollPane' import ScrollPane from './ScrollPane.vue'
import { generateTitle } from '@/utils/i18n'
import path from 'path' import path from 'path'
import {useStore} from "@store";
import {
defineComponent,
computed,
getCurrentInstance,
nextTick,
onBeforeMount,
reactive,
ref,
toRefs,
watch
} from "vue";
import {RouteRecordRaw, useRoute, useRouter} from 'vue-router'
import {TagView} from "@store/interface";
export default { export default defineComponent({
components: { ScrollPane }, components: {ScrollPane},
data() { setup() {
return { const store = useStore()
const router = useRouter()
const instance = getCurrentInstance()
const currentRoute = useRoute()
const scrollPaneRef = ref(null)
const {ctx} = instance
const toLastView=(visitedViews,view)=>{
const latestView = visitedViews.slice(-1)[0]
if (latestView && latestView.fullPath) {
router.push(latestView.fullPath)
} else {
if (view.name === 'Dashboard') {
router.push({path: '/redirect' + view.fullPath})
} else {
router.push('/')
}
}
}
const state =reactive({
visible: false, visible: false,
top: 0, top: 0,
left: 0, left: 0,
selectedTag: {}, selectedTag: {},
affixTags: [] affixTags: [],
} isActive: (route) => {
}, return route.path === currentRoute.path
computed: { },
visitedViews() { isAffix: (tag) => {
return this.$store.state.tagsView.visitedViews return tag.meta && tag.meta.affix
}, },
routes() { refreshSelectedTag: (view: TagView) => {
return this.$store.state.permission.routes store.dispatch('tagsView/delCachedView', view)
} const { fullPath } = view
}, nextTick(() => {
watch: { router.replace({ path: '/redirect' + fullPath }).catch(err => {
$route() { console.warn(err)
this.addTags() })
this.moveToCurrentTag() })
}, },
visible(value) { closeSelectedTag: (view: TagView) => {
if (value) { store.dispatch('tagsView/delView', view)
document.body.addEventListener('click', this.closeMenu) if (state.isActive(view)) {
} else { toLastView(store.state.tagsView.visitedViews, view)
document.body.removeEventListener('click', this.closeMenu) }
},
closeOthersTags: () => {
if (state.selectedTag.fullPath !== currentRoute.path && state.selectedTag.fullPath !== undefined) {
router.push(state.selectedTag.fullPath)
}
store.dispatch('tagsView/delOthersViews', state.selectedTag as TagView)
},
closeAllTags: (view: TagView) => {
store.dispatch('tagsView/delAllViews', undefined)
if (state.affixTags.some(tag => tag.path === currentRoute.path)) {
return
}
toLastView(store.state.tagsView.visitedViews, view)
},
openMenu: (tag: TagView, e: MouseEvent) => {
const menuMinWidth = 105
const offsetLeft = ctx.$el.getBoundingClientRect().left // container margin left
const offsetWidth = ctx.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const left = e.clientX - offsetLeft + 15 // 15: margin right
if (left > maxLeft) {
state.left = maxLeft
} else {
state.left = left
}
state.top = e.clientY
state.visible = true
state.selectedTag = tag
},
closeMenu: () => {
state.visible = false
},
handleScroll: () => {
state.closeMenu()
} }
}
}, })
mounted() {
this.initTags() const visitedViews = computed(() => {
this.addTags() return store.state.tagsView.visitedViews
}, })
methods: { const routes = computed(() => store.state.permission.routes)
generateTitle, // generateTitle by vue-i18n
isActive(route) { const filterAffixTags = (routes: RouteRecordRaw[], basePath = '/') => {
return route.path === this.$route.path let tags: TagView[] = []
},
isAffix(tag) {
return tag.meta && tag.meta.affix
},
filterAffixTags(routes, basePath = '/') {
let tags = []
routes.forEach(route => { routes.forEach(route => {
if (route.meta && route.meta.affix) { if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path) const tagPath = path.resolve(basePath, route.path)
@ -86,117 +148,81 @@ export default {
meta: { ...route.meta } meta: { ...route.meta }
}) })
} }
if (route.children) { if (route.children) {
const tempTags = this.filterAffixTags(route.children, route.path) const childTags = filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) { if (childTags.length >= 1) {
tags = [...tags, ...tempTags] tags = tags.concat(childTags)
} }
} }
}) })
return tags return tags
}, }
initTags() {
const affixTags = this.affixTags = this.filterAffixTags(this.routes) const initTags = () => {
for (const tag of affixTags) { state.affixTags = filterAffixTags(routes.value)
for (const tag of state.affixTags) {
// Must have tag name // Must have tag name
if (tag.name) { if (tag.name) {
this.$store.dispatch('tagsView/addVisitedView', tag) store.dispatch('tagsView/addVisitedView', tag as TagView)
} }
} }
}, }
addTags() {
const { name } = this.$route const addTags = () => {
if (name) { if (currentRoute.name) {
this.$store.dispatch('tagsView/addView', this.$route) store.dispatch('tagsView/addView', currentRoute)
} }
return false return false
}, }
moveToCurrentTag() {
const tags = this.$refs.tag const moveToCurrentTag = () => {
this.$nextTick(() => { const tags = instance?.refs.tag as any[]
nextTick(() => {
if (tags === null || tags === undefined || !Array.isArray(tags)) { return }
for (const tag of tags) { for (const tag of tags) {
if (tag.to.path === this.$route.path) { if ((tag.to as TagView).path === currentRoute.path) {
this.$refs.scrollPane.moveToTarget(tag) (scrollPaneRef.value as any).moveToCurrentTag(tag)
// when query is different then update // When query is different then update
if (tag.to.fullPath !== this.$route.fullPath) { if ((tag.to as TagView).fullPath !== currentRoute.fullPath) {
this.$store.dispatch('tagsView/updateVisitedView', this.$route) store.dispatch('tagsView/updateVisitedView', currentRoute)
} }
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) { watch(() => currentRoute.name, () => {
this.left = maxLeft if (currentRoute.name !== 'Login') {
} else { addTags()
this.left = left moveToCurrentTag()
} }
})
this.top = e.clientY watch(() => state.visible, (value) => {
this.visible = true if (value) {
this.selectedTag = tag document.body.addEventListener('click', state.closeMenu)
}, } else {
closeMenu() { document.body.removeEventListener('click', state.closeMenu)
this.visible = false }
}, })
handleScroll() {
this.closeMenu() // life cricle
onBeforeMount(() => {
initTags()
addTags()
})
return {
visitedViews,
routes,
scrollPaneRef,
...toRefs(state)
} }
} }
} })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -206,6 +232,7 @@ export default {
background: #fff; background: #fff;
border-bottom: 1px solid #d8dce5; 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); 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-wrapper {
.tags-view-item { .tags-view-item {
display: inline-block; display: inline-block;
@ -220,16 +247,20 @@ export default {
font-size: 12px; font-size: 12px;
margin-left: 5px; margin-left: 5px;
margin-top: 4px; margin-top: 4px;
&:first-of-type { &:first-of-type {
margin-left: 15px; margin-left: 15px;
} }
&:last-of-type { &:last-of-type {
margin-right: 15px; margin-right: 15px;
} }
&.active { &.active {
background-color: #42b983; background-color: #42b983;
color: #fff; color: #fff;
border-color: #42b983; border-color: #42b983;
&::before { &::before {
content: ''; content: '';
background: #fff; background: #fff;
@ -243,6 +274,7 @@ export default {
} }
} }
} }
.contextmenu { .contextmenu {
margin: 0; margin: 0;
background: #fff; background: #fff;
@ -255,10 +287,12 @@ export default {
font-weight: 400; font-weight: 400;
color: #333; color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
li { li {
margin: 0; margin: 0;
padding: 7px 16px; padding: 7px 16px;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: #eee; background: #eee;
} }
@ -279,11 +313,13 @@ export default {
text-align: center; text-align: center;
transition: all .3s cubic-bezier(.645, .045, .355, 1); transition: all .3s cubic-bezier(.645, .045, .355, 1);
transform-origin: 100% 50%; transform-origin: 100% 50%;
&:before { &:before {
transform: scale(.6); transform: scale(.6);
display: inline-block; display: inline-block;
vertical-align: -3px; vertical-align: -3px;
} }
&:hover { &:hover {
background-color: #b4bccc; background-color: #b4bccc;
color: #fff; color: #fff;

View File

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

View File

@ -1,15 +1,15 @@
<template> <template>
<div :class="classObj" class="app-wrapper"> <div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" /> <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<sidebar class="sidebar-container" /> <sidebar class="sidebar-container"/>
<div :class="{hasTagsView:needTagsView}" class="main-container"> <div :class="{hasTagsView:needTagsView}" class="main-container">
<div :class="{'fixed-header':fixedHeader}"> <div :class="{'fixed-header':fixedHeader}">
<navbar /> <navbar/>
<tags-view v-if="needTagsView" /> <tags-view v-if="needTagsView"/>
</div> </div>
<app-main /> <app-main/>
<right-panel v-if="showSettings"> <right-panel v-if="showSettings">
<settings /> <settings/>
</right-panel> </right-panel>
</div> </div>
</div> </div>
@ -18,27 +18,30 @@
<script> <script>
import {computed, defineComponent, onBeforeMount, onBeforeUnmount, onMounted, reactive, toRefs} from "vue"; import {computed, defineComponent, onBeforeMount, onBeforeUnmount, onMounted, reactive, toRefs} from "vue";
import {Navbar, Sidebar, AppMain} from './components' import {AppMain,Navbar, Settings,Sidebar,TagsView } from './components'
import { import resize from './mixin/ResizeHandler'
sidebar,
device,
resizeMounted,
addEventListenerOnResize,
removeEventListenerResize,
watchRouter
} from './mixin/ResizeHandler'
import {useStore} from "@store"; import {useStore} from "@store";
export default defineComponent({ export default defineComponent({
name: 'Layout', name: 'Layout',
components: { components: {
AppMain,
Navbar, Navbar,
Settings,
Sidebar, Sidebar,
AppMain TagsView
}, },
setup() { setup() {
const store = useStore() const store = useStore()
const {
sidebar,
device,
resizeMounted,
addEventListenerOnResize,
removeEventListenerResize,
watchRouter
} = resize()
const state = reactive({ const state = reactive({
handleClickOutside: () => { handleClickOutside: () => {
@ -54,8 +57,41 @@ export default defineComponent({
mobile: device === 'mobile' mobile: device === 'mobile'
} }
}) })
} const showSettings=computed(()=>{
return store.state.settings.showSettings
})
const needTagsView=computed(()=>{
return store.state.settings.tagsView
})
const fixedHeader=computed(()=>{
return store.state.settings.fixedHeader
})
watchRouter()
onBeforeMount(()=>{
addEventListenerOnResize()
})
onMounted(()=>{
resizeMounted()
})
onBeforeUnmount(()=>{
removeEventListenerResize()
})
return{
classObj,
sidebar,
showSettings,
needTagsView,
fixedHeader,
...toRefs(state)
}
}
}) })
</script> </script>

View File

@ -44,7 +44,6 @@ export default function () {
window.removeEventListener('resize', resizeHandler) window.removeEventListener('resize', resizeHandler)
} }
const currentRoute = useRoute() const currentRoute = useRoute()
const watchRouter = watch(() => currentRoute.name, () => { const watchRouter = watch(() => currentRoute.name, () => {
if (store.state.app.device === 'mobile' && store.state.app.sidebar.opened) { if (store.state.app.device === 'mobile' && store.state.app.sidebar.opened) {

View File

@ -1,4 +1,4 @@
import {RouteRecordRaw} from "vue-router"; import {RouteRecordRaw,RouteLocationNormalized} from "vue-router";
// 接口类型声明 // 接口类型声明
export interface UserState { export interface UserState {
@ -32,10 +32,21 @@ export interface PermissionState{
addRoutes: RouteRecordRaw[] addRoutes: RouteRecordRaw[]
} }
export interface TagView extends Partial<RouteLocationNormalized> {
title?: string
}
export interface TagsViewState{
visitedViews: TagView[],
cachedViews: (string|undefined)[]
}
// 顶级类型声明 // 顶级类型声明
export interface RootStateTypes { export interface RootStateTypes {
user: UserState, user: UserState,
app: AppState, app: AppState,
setting: SettingState, setting: SettingState,
permission:PermissionState permission:PermissionState,
tagsView:TagsViewState
} }

View File

@ -0,0 +1,158 @@
import {Module} from "vuex";
import {TagsViewState,RootStateTypes} from "@store/interface";
const tagsViewModule: Module<TagsViewState, RootStateTypes> = {
namespaced: true,
state: {
visitedViews: [],
cachedViews: []
},
mutations: {
ADD_VISITED_VIEW: (state, view) => {
if (state.visitedViews.some(v => v.path === view.path)) return
state.visitedViews.push(
Object.assign({}, view, {
title: view.meta?.title || 'no-name'
})
)
},
ADD_CACHED_VIEW: (state, view) => {
if (state.cachedViews.includes(view.name)) return
if (!view.meta.noCache) {
state.cachedViews.push(view.name)
}
},
DEL_VISITED_VIEW: (state, view) => {
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
}
}
}
},
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 tagsViewModule;