feat:vue-element-admin升级vue3
This commit is contained in:
parent
fe8a7e2c31
commit
0ec8710e6f
|
|
@ -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>
|
||||
|
|
@ -1,45 +1,31 @@
|
|||
<template>
|
||||
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
|
||||
<slot />
|
||||
<slot/>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const tagAndTagSpacing = 4 // tagAndTagSpacing
|
||||
<script lang="ts">
|
||||
import {defineComponent, reactive, ref, toRefs, computed, onMounted, onBeforeUnmount, getCurrentInstance} from "vue";
|
||||
|
||||
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
|
||||
export default defineComponent({
|
||||
emits: ['scroll'],
|
||||
setup(_, context) {
|
||||
const scrollContainer = ref(null)
|
||||
const scrollWrapper = computed(() => {
|
||||
return (scrollContainer.value as any).$refs.wrap as HTMLElement
|
||||
})
|
||||
const {ctx} = getCurrentInstance() as any
|
||||
const tagAndTagSpacing = 4
|
||||
|
||||
const state = reactive({
|
||||
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
|
||||
|
||||
|
|
@ -50,45 +36,64 @@ export default {
|
|||
}
|
||||
|
||||
if (firstTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = 0
|
||||
scrollWrapper.value.scrollLeft = 0
|
||||
} else if (lastTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
|
||||
scrollWrapper.value.scrollLeft = scrollWrapper.value.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
|
||||
if (afterNextTagOffsetLeft > scrollWrapper.value.scrollLeft + containerWidth) {
|
||||
scrollWrapper.value.scrollLeft = afterNextTagOffsetLeft - containerWidth
|
||||
} else if (beforePrevTagOffsetLeft < scrollWrapper.value.scrollLeft) {
|
||||
scrollWrapper.value.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>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.scroll-container {
|
||||
.el-scrollbar__bar {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.el-scrollbar__wrap {
|
||||
height: 49px;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
::v-deep {
|
||||
.el-scrollbar__bar {
|
||||
bottom: 0px;
|
||||
}
|
||||
.el-scrollbar__wrap {
|
||||
height: 49px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -12,70 +12,132 @@
|
|||
@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)" />
|
||||
{{ tag.meta.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>
|
||||
<li @click="refreshSelectedTag(selectedTag)">刷新</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li>
|
||||
<li @click="closeOthersTags">关闭其它</li>
|
||||
<li @click="closeAllTags(selectedTag)">关闭所有</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScrollPane from './ScrollPane'
|
||||
import { generateTitle } from '@/utils/i18n'
|
||||
<script lang="ts">
|
||||
import ScrollPane from './ScrollPane.vue'
|
||||
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 {
|
||||
components: { ScrollPane },
|
||||
data() {
|
||||
return {
|
||||
export default defineComponent({
|
||||
components: {ScrollPane},
|
||||
setup() {
|
||||
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,
|
||||
top: 0,
|
||||
left: 0,
|
||||
selectedTag: {},
|
||||
affixTags: []
|
||||
}
|
||||
affixTags: [],
|
||||
isActive: (route) => {
|
||||
return route.path === currentRoute.path
|
||||
},
|
||||
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) {
|
||||
isAffix: (tag) => {
|
||||
return tag.meta && tag.meta.affix
|
||||
},
|
||||
filterAffixTags(routes, basePath = '/') {
|
||||
let tags = []
|
||||
refreshSelectedTag: (view: TagView) => {
|
||||
store.dispatch('tagsView/delCachedView', view)
|
||||
const { fullPath } = view
|
||||
nextTick(() => {
|
||||
router.replace({ path: '/redirect' + fullPath }).catch(err => {
|
||||
console.warn(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
closeSelectedTag: (view: TagView) => {
|
||||
store.dispatch('tagsView/delView', view)
|
||||
if (state.isActive(view)) {
|
||||
toLastView(store.state.tagsView.visitedViews, view)
|
||||
}
|
||||
},
|
||||
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()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
const visitedViews = computed(() => {
|
||||
return store.state.tagsView.visitedViews
|
||||
})
|
||||
const routes = computed(() => store.state.permission.routes)
|
||||
|
||||
const filterAffixTags = (routes: RouteRecordRaw[], basePath = '/') => {
|
||||
let tags: TagView[] = []
|
||||
|
||||
routes.forEach(route => {
|
||||
if (route.meta && route.meta.affix) {
|
||||
const tagPath = path.resolve(basePath, route.path)
|
||||
|
|
@ -86,117 +148,81 @@ export default {
|
|||
meta: { ...route.meta }
|
||||
})
|
||||
}
|
||||
|
||||
if (route.children) {
|
||||
const tempTags = this.filterAffixTags(route.children, route.path)
|
||||
if (tempTags.length >= 1) {
|
||||
tags = [...tags, ...tempTags]
|
||||
const childTags = filterAffixTags(route.children, route.path)
|
||||
if (childTags.length >= 1) {
|
||||
tags = tags.concat(childTags)
|
||||
}
|
||||
}
|
||||
})
|
||||
return tags
|
||||
},
|
||||
initTags() {
|
||||
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
|
||||
for (const tag of affixTags) {
|
||||
}
|
||||
|
||||
const initTags = () => {
|
||||
state.affixTags = filterAffixTags(routes.value)
|
||||
for (const tag of state.affixTags) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
this.$store.dispatch('tagsView/addVisitedView', tag)
|
||||
store.dispatch('tagsView/addVisitedView', tag as TagView)
|
||||
}
|
||||
}
|
||||
},
|
||||
addTags() {
|
||||
const { name } = this.$route
|
||||
if (name) {
|
||||
this.$store.dispatch('tagsView/addView', this.$route)
|
||||
}
|
||||
|
||||
const addTags = () => {
|
||||
if (currentRoute.name) {
|
||||
store.dispatch('tagsView/addView', currentRoute)
|
||||
}
|
||||
return false
|
||||
},
|
||||
moveToCurrentTag() {
|
||||
const tags = this.$refs.tag
|
||||
this.$nextTick(() => {
|
||||
}
|
||||
|
||||
const moveToCurrentTag = () => {
|
||||
const tags = instance?.refs.tag as any[]
|
||||
nextTick(() => {
|
||||
if (tags === null || tags === undefined || !Array.isArray(tags)) { return }
|
||||
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)
|
||||
if ((tag.to as TagView).path === currentRoute.path) {
|
||||
(scrollPaneRef.value as any).moveToCurrentTag(tag)
|
||||
// When query is different then update
|
||||
if ((tag.to as TagView).fullPath !== currentRoute.fullPath) {
|
||||
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) {
|
||||
this.left = maxLeft
|
||||
} else {
|
||||
this.left = left
|
||||
}
|
||||
|
||||
this.top = e.clientY
|
||||
this.visible = true
|
||||
this.selectedTag = tag
|
||||
},
|
||||
closeMenu() {
|
||||
this.visible = false
|
||||
},
|
||||
handleScroll() {
|
||||
this.closeMenu()
|
||||
watch(() => currentRoute.name, () => {
|
||||
if (currentRoute.name !== 'Login') {
|
||||
addTags()
|
||||
moveToCurrentTag()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => state.visible, (value) => {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', state.closeMenu)
|
||||
} else {
|
||||
document.body.removeEventListener('click', state.closeMenu)
|
||||
}
|
||||
})
|
||||
|
||||
// life cricle
|
||||
onBeforeMount(() => {
|
||||
initTags()
|
||||
addTags()
|
||||
})
|
||||
|
||||
return {
|
||||
visitedViews,
|
||||
routes,
|
||||
scrollPaneRef,
|
||||
...toRefs(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
@ -206,6 +232,7 @@ export default {
|
|||
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;
|
||||
|
|
@ -220,16 +247,20 @@ export default {
|
|||
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;
|
||||
|
|
@ -243,6 +274,7 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contextmenu {
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
|
|
@ -255,10 +287,12 @@ export default {
|
|||
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;
|
||||
}
|
||||
|
|
@ -279,11 +313,13 @@ export default {
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export { default as Navbar } from './Navbar.vue'
|
||||
export { default as Sidebar } from './Sidebar/index.vue'
|
||||
export { default as AppMain } from './AppMain.vue'
|
||||
export { default as Settings } from './Settings/index.vue'
|
||||
export { default as TagsView } from './TagsView/index.vue'
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
<template>
|
||||
<div :class="classObj" class="app-wrapper">
|
||||
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||
<sidebar class="sidebar-container" />
|
||||
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
|
||||
<sidebar class="sidebar-container"/>
|
||||
<div :class="{hasTagsView:needTagsView}" class="main-container">
|
||||
<div :class="{'fixed-header':fixedHeader}">
|
||||
<navbar />
|
||||
<tags-view v-if="needTagsView" />
|
||||
<navbar/>
|
||||
<tags-view v-if="needTagsView"/>
|
||||
</div>
|
||||
<app-main />
|
||||
<app-main/>
|
||||
<right-panel v-if="showSettings">
|
||||
<settings />
|
||||
<settings/>
|
||||
</right-panel>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -18,27 +18,30 @@
|
|||
|
||||
<script>
|
||||
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 {AppMain,Navbar, Settings,Sidebar,TagsView } from './components'
|
||||
import resize from './mixin/ResizeHandler'
|
||||
import {useStore} from "@store";
|
||||
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Layout',
|
||||
components: {
|
||||
AppMain,
|
||||
Navbar,
|
||||
Settings,
|
||||
Sidebar,
|
||||
AppMain
|
||||
TagsView
|
||||
},
|
||||
setup() {
|
||||
const store = useStore()
|
||||
const {
|
||||
sidebar,
|
||||
device,
|
||||
resizeMounted,
|
||||
addEventListenerOnResize,
|
||||
removeEventListenerResize,
|
||||
watchRouter
|
||||
} = resize()
|
||||
|
||||
const state = reactive({
|
||||
handleClickOutside: () => {
|
||||
|
|
@ -54,8 +57,41 @@ export default defineComponent({
|
|||
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>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ export default function () {
|
|||
window.removeEventListener('resize', resizeHandler)
|
||||
}
|
||||
|
||||
|
||||
const currentRoute = useRoute()
|
||||
const watchRouter = watch(() => currentRoute.name, () => {
|
||||
if (store.state.app.device === 'mobile' && store.state.app.sidebar.opened) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {RouteRecordRaw} from "vue-router";
|
||||
import {RouteRecordRaw,RouteLocationNormalized} from "vue-router";
|
||||
|
||||
// 接口类型声明
|
||||
export interface UserState {
|
||||
|
|
@ -32,10 +32,21 @@ export interface PermissionState{
|
|||
addRoutes: RouteRecordRaw[]
|
||||
}
|
||||
|
||||
export interface TagView extends Partial<RouteLocationNormalized> {
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface TagsViewState{
|
||||
visitedViews: TagView[],
|
||||
cachedViews: (string|undefined)[]
|
||||
}
|
||||
|
||||
|
||||
// 顶级类型声明
|
||||
export interface RootStateTypes {
|
||||
user: UserState,
|
||||
app: AppState,
|
||||
setting: SettingState,
|
||||
permission:PermissionState
|
||||
permission:PermissionState,
|
||||
tagsView:TagsViewState
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue