This commit is contained in:
huzhushan 2021-04-02 15:45:06 +08:00
parent e570a7e509
commit f4eaebdb41
15 changed files with 574 additions and 47 deletions

View File

@ -12,6 +12,6 @@ $subMenuBg: #1f2d3d; // 子菜单背景颜色
$subMenuHover: #001528; // 鼠标经过子菜单时的背景颜色 $subMenuHover: #001528; // 鼠标经过子菜单时的背景颜色
$collapseMenuActiveBg: #1f2d3d; // 菜单宽度折叠后已选中菜单的背景颜色 $collapseMenuActiveBg: #1f2d3d; // 菜单宽度折叠后已选中菜单的背景颜色
$collapseMenuActiveColor: $menuTextColor; // 菜单宽度折叠后已选中菜单的文字颜色 $collapseMenuActiveColor: $menuTextColor; // 菜单宽度折叠后已选中菜单的文字颜色
$collapseMenuActiveBorderBg: $mainColor; // 菜单宽度折叠后已选中菜单的边框颜色 $collapseMenuActiveBorderColor: $mainColor; // 菜单宽度折叠后已选中菜单的边框颜色
$collapseMenuActiveBorderWidth: 2px; // 菜单宽度折叠后已选中菜单的边框宽度 $collapseMenuActiveBorderWidth: 2px; // 菜单宽度折叠后已选中菜单的边框宽度
$arrowColor: #909399; // 展开/收起箭头颜色 $arrowColor: #909399; // 展开/收起箭头颜色

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,42 @@
<template>
<div class="main">
<router-view v-slot="{ Component }">
<keep-alive :include="cacheList">
<component
:is="Component"
:key="key"
/>
</keep-alive>
</router-view>
</div>
</template>
<script>
import { computed, defineComponent } from "vue";
import { useRoute } from "vue-router";
import { useStore } from "vuex";
export default defineComponent({
setup() {
const store = useStore();
const route = useRoute();
const cacheList = computed(() => store.state.tags.cacheList);
const key = computed(() => route.path);
return {
cacheList,
key,
};
},
});
</script>
<style lang="scss" scoped>
.main {
flex: 1;
background: #f0f2f5;
padding: 16px;
overflow: auto;
}
</style>

View File

@ -92,7 +92,7 @@ export default defineComponent({
top: 0; top: 0;
width: $collapseMenuActiveBorderWidth; width: $collapseMenuActiveBorderWidth;
height: 100%; height: 100%;
background-color: $collapseMenuActiveBorderBg; background-color: $collapseMenuActiveBorderColor;
} }
} }
} }

View File

@ -1,9 +0,0 @@
<template>
<div class="tabs"></div>
</template>
<style lang="scss" scoped>
.tabs {
height: 32px;
border-bottom: 1px solid #eaeaea;
}
</style>

View File

@ -0,0 +1,26 @@
<template>
<el-scrollbar
ref="scrollContainer"
:vertical="false"
class="scroll-container"
>
<slot />
</el-scrollbar>
</template>
<style lang="scss" scoped>
.scroll-container {
white-space: nowrap;
position: relative;
overflow: hidden;
width: 100%;
::v-deep(.el-scrollbar__bar) {
bottom: 0px;
}
::v-deep(.el-scrollbar__wrap) {
height: 49px;
}
}
</style>

View File

@ -0,0 +1,327 @@
<template>
<div
id="tags-view-container"
class="tags-view-container"
>
<scroll-bar
ref="scrollBar"
class="tags-view-wrapper"
@scroll="handleScroll"
>
<router-link
v-for="tag in tagList"
ref="tags"
: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="!isAffix(tag)?closeSelectedTag(tag):''"
@contextmenu.prevent="openMenu(tag,$event)"
>
{{ tag.title }}
<span
v-if="!isAffix(tag)"
class="el-icon-refresh"
@click.prevent.stop="refreshSelectedTag(tag)"
/>
<span
v-if="!isAffix(tag)"
class="el-icon-close"
@click.prevent.stop="closeSelectedTag(tag)"
/>
</router-link>
</scroll-bar>
<ul
v-show="visible"
:style="{left:left+'px',top:top+'px'}"
class="contextmenu"
>
<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 ScrollBar from "./ScrollBar.vue";
import path from "path";
import {
computed,
defineComponent,
reactive,
watch,
toRefs,
onMounted,
onBeforeUnmount,
ref,
nextTick,
} from "vue";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
export default defineComponent({
name: "Tagsbar",
components: { ScrollBar },
setup() {
const store = useStore();
const router = useRouter();
const route = router.currentRoute;
const tags = ref(null);
const scrollBar = ref(null);
const tagList = computed(() => store.state.tags.tagList);
const routes = computed(() => router.getRoutes());
const state = reactive({
visible: false,
top: 0,
left: 0,
selectedTag: {},
affixTags: [],
isReload: false,
isActive(tag) {
return tag.path === route.value.path;
},
isAffix(tag) {
return !!tag.meta && !!tag.meta.affix;
},
filterAffixTags(routes) {
return routes.filter((route) => !!route.meta && !!route.meta.affix);
},
initTags() {
const affixTags = (state.affixTags = state.filterAffixTags(
routes.value
));
for (const tag of affixTags) {
// Must have tag name
if (!!tag.name) {
store.dispatch("tags/addTagList", tag);
}
}
},
closeTag(tag) {
store.dispatch("tags/delTag", tag);
},
addTags() {
if (route.value.name) {
store.dispatch("tags/addTag", route.value);
}
return false;
},
saveActivePosition(oldRoute) {
const index = tagList.value.findIndex(
(item) => item.fullPath === oldRoute.fullPath
);
store.dispatch("tags/saveActivePosition", Math.max(0, index));
},
moveToCurrentTag(callback) {
nextTick(() => {
for (const tag of tagList.value) {
if (tag.path === route.value.path) {
// scrollBar.value.moveToTarget(tag);
if (tag.fullPath !== route.value.fullPath) {
store.dispatch("tags/updateTagList", route.value);
}
callback && callback();
break;
}
}
});
},
reload() {
// this.refreshSelectedTag(this.$route)
state.isReload = true;
},
refreshSelectedTag(tag) {
store.dispatch("tags/delCacheList", tag).then(() => {
const { fullPath } = tag;
nextTick(() => {
router.replace({
path: "/redirect" + fullPath,
});
});
});
},
closeSelectedTag(tag) {
const closedTagIndex = tagList.value.findIndex(
(item) => item.fullPath === tag.fullPath
);
store.dispatch("tags/delTag", tag).then(({ tagList }) => {
if (state.isActive(tag)) {
state.toLastView(tagList, tag, closedTagIndex - 1);
}
});
},
closeOthersTags() {
router.push(state.selectedTag);
store.dispatch("tags/delOtherTags", state.selectedTag).then(() => {
state.moveToCurrentTag();
});
},
closeAllTags(view) {
const closedTagIndex = tagList.value.findIndex(
(item) => item.fullPath === view.fullPath
);
store.dispatch("tags/delAllTags").then(({ tagList }) => {
if (state.affixTags.some((tag) => tag.path === view.path)) {
return;
}
state.toLastView(tagList, view, closedTagIndex - 1);
});
},
toLastView(tagList, view, lastTagIndex) {
const lastTag = tagList[lastTagIndex];
if (!!lastTag) {
router.push(lastTag.fullPath);
} else {
router.push("/");
}
},
openMenu(tag, e) {
state.left = e.clientX;
state.top = e.clientY;
state.visible = true;
state.selectedTag = tag;
},
closeMenu() {
state.visible = false;
},
handleScroll() {
state.closeMenu();
},
});
watch(route, (newRoute, oldRoute) => {
console.log("监听路由", newRoute, oldRoute);
state.saveActivePosition(oldRoute); // tag
state.addTags();
state.moveToCurrentTag(() => {
if (state.isReload) {
state.isReload = false;
state.refreshSelectedTag(this.$route);
}
});
});
onMounted(() => {
state.initTags();
state.addTags();
document.addEventListener("click", state.closeMenu);
});
onBeforeUnmount(() => {
document.removeEventListener("click", state.closeMenu);
});
return {
tagList,
routes,
tags,
scrollBar,
...toRefs(state),
};
},
});
</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, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
.tags-view-wrapper {
.tags-view-item {
display: inline-block;
position: relative;
cursor: pointer;
line-height: 31px;
border-left: 1px solid #e6e6e6;
border-right: 1px solid #e6e6e6;
color: #5c5c5c;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: -1px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
background-color: #f6f7f6;
color: #333;
border-color: #f6f7f6;
border-top: 2px solid #333;
border-left: 1px solid #e6e6e6;
// &::before {
// content: '';
// width: 8px;
// height: 8px;
// position: relative;
// }
}
}
}
.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, 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,
.el-icon-refresh {
margin-left: 2px;
width: 16px;
height: 16px;
vertical-align: 2px;
border-radius: 50%;
text-align: center;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(0.8);
display: inline-block;
vertical-align: -2px;
}
&:hover {
background-color: #333;
color: #fff;
}
}
}
}
</style>

View File

@ -14,18 +14,13 @@
</el-breadcrumb> </el-breadcrumb>
</template> </template>
<script> <script>
import { import { defineComponent, ref, onBeforeMount, watch } from "vue";
defineComponent, import { useRouter } from "vue-router";
ref,
onBeforeMount,
getCurrentInstance,
watch,
} from "vue";
export default defineComponent({ export default defineComponent({
setup() { setup() {
const { ctx } = getCurrentInstance(); const router = useRouter();
const route = ctx.$router.currentRoute; // 使useRoutewatch const route = router.currentRoute; // 使useRoutewatch
const breadcrumbs = ref([]); const breadcrumbs = ref([]);
const getBreadcrumbs = (route) => { const getBreadcrumbs = (route) => {

View File

@ -4,11 +4,9 @@
<div class="right"> <div class="right">
<div class="top"> <div class="top">
<topbar /> <topbar />
<tabsbar /> <tagsbar />
</div>
<div class="main">
<router-view />
</div> </div>
<content />
</div> </div>
</div> </div>
</template> </template>
@ -16,13 +14,15 @@
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import Sidebar from "./components/Sidebar/index.vue"; import Sidebar from "./components/Sidebar/index.vue";
import Topbar from "./components/Topbar/index.vue"; import Topbar from "./components/Topbar/index.vue";
import Tabsbar from "./components/Tabsbar/index.vue"; import Tagsbar from "./components/Tagsbar/index.vue";
import Content from "./components/Content/index.vue";
export default defineComponent({ export default defineComponent({
components: { components: {
Sidebar, Sidebar,
Topbar, Topbar,
Tabsbar, Tagsbar,
Content,
}, },
}); });
</script> </script>
@ -39,12 +39,6 @@ export default defineComponent({
.top { .top {
background: #fff; background: #fff;
} }
.main {
flex: 1;
background: #f0f2f5;
padding: 16px;
overflow: auto;
}
} }
} }
</style> </style>

View File

@ -7,7 +7,7 @@ import home from './modules/home'
import test from './modules/test' import test from './modules/test'
// 左侧菜单(左侧菜单请配置在此,否则无法显示) // 左侧菜单
export const allMenus = [ export const allMenus = [
...home, ...home,
...test, ...test,

View File

@ -3,11 +3,13 @@ import { createStore } from "vuex";
import app from "./modules/app"; import app from "./modules/app";
import account from "./modules/account"; import account from "./modules/account";
import menu from "./modules/menu"; import menu from "./modules/menu";
import tags from "./modules/tags";
export default createStore({ export default createStore({
modules: { modules: {
app, app,
account, account,
menu menu,
tags
}, },
}); });

153
src/store/modules/tags.js Normal file
View File

@ -0,0 +1,153 @@
const state = {
tagList: [],
cacheList: [],
activePosition: 0
}
const mutations = {
ADD_TAG_LIST: (state, tag) => {
if (state.tagList.some(v => v.path === tag.path)) return;
state.tagList.splice(
state.activePosition + 1,
0,
Object.assign({}, tag, {
title: tag.meta.title || 'no-name'
})
)
},
ADD_CACHE_LIST: (state, tag) => {
if (state.cacheList.includes(tag.name)) return
if (!tag.meta.noCache) {
state.cacheList.push(tag.name)
}
},
DEL_TAG_LIST: (state, tag) => {
state.tagList = state.tagList.filter(v => v.path !== tag.path)
},
DEL_CACHE_LIST: (state, tag) => {
state.cacheList = state.cacheList.filter(v => v !== tag.name)
},
DEL_OTHER_TAG_LIST: (state, tag) => {
state.tagList = state.tagList.filter(v => !!v.meta.affix || v.path === tag.path)
},
DEL_OTHER_CACHE_LIST: (state, tag) => {
state.cacheList = state.cacheList.filter(v => v === tag.name)
},
DEL_ALL_TAG_LIST: state => {
state.tagList = state.tagList.filter(v => !!v.meta.affix)
},
DEL_ALL_CACHE_LIST: state => {
state.cacheList = []
},
UPDATE_TAG_LIST: (state, tag) => {
const index = state.tagList.findIndex(v => v.path === tag.path);
if (index > -1) {
state.tagList[index] = Object.assign({}, tag)
}
},
SAVE_ACTIVE_POSITION: (state, index) => {
state.activePosition = index
},
}
const actions = {
saveActivePosition ({ commit }, index) {
commit('SAVE_ACTIVE_POSITION', index)
},
addTag ({ dispatch }, tag) {
dispatch('addTagList', tag)
dispatch('addCacheList', tag)
},
addTagList ({ commit }, tag) {
commit('ADD_TAG_LIST', tag)
},
addCacheList ({ commit }, tag) {
commit('ADD_CACHE_LIST', tag)
},
delTag ({ dispatch, state }, tag) {
return new Promise(resolve => {
dispatch('delTagList', tag)
dispatch('delCacheList', tag)
resolve({
tagList: [...state.tagList],
cacheList: [...state.cacheList]
})
})
},
delTagList ({ commit, state }, tag) {
return new Promise(resolve => {
commit('DEL_TAG_LIST', tag)
resolve([...state.tagList])
})
},
delCacheList ({ commit, state }, tag) {
return new Promise(resolve => {
commit('DEL_CACHE_LIST', tag)
resolve([...state.cacheList])
})
},
delOtherTags ({ dispatch, state }, tag) {
return new Promise(resolve => {
dispatch('delOtherTagList', tag)
dispatch('delOtherCacheList', tag)
resolve({
tagList: [...state.tagList],
cacheList: [...state.cacheList]
})
})
},
delOtherTagList ({ commit, state }, tag) {
return new Promise(resolve => {
commit('DEL_OTHER_TAG_LIST', tag)
resolve([...state.tagList])
})
},
delOtherCacheList ({ commit, state }, tag) {
return new Promise(resolve => {
commit('DEL_OTHER_CACHE_LIST', tag)
resolve([...state.cacheList])
})
},
delAllTags ({ dispatch, state }) {
return new Promise(resolve => {
dispatch('delAllTagList')
dispatch('delAllCacheList')
resolve({
tagList: [...state.tagList],
cacheList: [...state.cacheList]
})
})
},
delAllTagList ({ commit, state }) {
return new Promise(resolve => {
commit('DEL_ALL_TAG_LIST')
resolve([...state.tagList])
})
},
delAllCacheList ({ commit, state }) {
return new Promise(resolve => {
commit('DEL_ALL_CACHE_LIST')
resolve([...state.cacheList])
})
},
updateTagList ({ commit }, tag) {
commit('UPDATE_TAG_LIST', tag)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@ -2,27 +2,24 @@
<div class="error"> <div class="error">
<template v-if="error === '403'"> <template v-if="error === '403'">
<span class="code-403">403</span> <span class="code-403">403</span>
<img <svg-icon
src="~@/assets/error-icons/403.svg" name="error-icons-403"
alt=""
class="error-img" class="error-img"
> />
<h2 class="title">您无权访问此页面</h2> <h2 class="title">您无权访问此页面</h2>
</template> </template>
<template v-else-if="error === '500'"> <template v-else-if="error === '500'">
<img <svg-icon
src="~@/assets/error-icons/500.svg" name="error-icons-500"
alt=""
class="error-img" class="error-img"
> />
<h2 class="title">服务器出错了</h2> <h2 class="title">服务器出错了</h2>
</template> </template>
<template v-else-if="error === '404'"> <template v-else-if="error === '404'">
<img <svg-icon
src="~@/assets/error-icons/404.svg" name="error-icons-404"
alt=""
class="error-img" class="error-img"
> />
<h2 class="title">您访问的页面不存在</h2> <h2 class="title">您访问的页面不存在</h2>
</template> </template>
@ -66,7 +63,7 @@ export default defineComponent({
color: #ee5c42; color: #ee5c42;
} }
.error-img { .error-img {
width: 320px; font-size: 320px;
pointer-events: none; pointer-events: none;
} }
.title { .title {