This commit is contained in:
huzhushan 2021-04-09 13:56:34 +08:00
parent f4eaebdb41
commit 8da758f494
35 changed files with 656 additions and 396 deletions

View File

@ -23,7 +23,7 @@ export default [
data: { data: {
id: 1, id: 1,
name: 'zhangsan', name: 'zhangsan',
role: 'visitor', 'role|1': ['admin', 'visitor'], // 随机返回一个角色admin或visitor
avatar: "@image('48x48', '#fb0a2a')" avatar: "@image('48x48', '#fb0a2a')"
} }
}, },

View File

@ -2,7 +2,7 @@
$mainColor: #409eff; // 网站主题色 $mainColor: #409eff; // 网站主题色
// 菜单配置 // 侧边栏
$menuBg: #304156; // 菜单背景颜色 $menuBg: #304156; // 菜单背景颜色
$menuTextColor: #fff; // 菜单文字颜色 $menuTextColor: #fff; // 菜单文字颜色
$menuActiveTextColor: $mainColor; // 已选中菜单文字颜色 $menuActiveTextColor: $mainColor; // 已选中菜单文字颜色

View File

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

View File

@ -10,13 +10,7 @@
:text-color="variables.menuTextColor" :text-color="variables.menuTextColor"
:active-text-color="variables.menuActiveTextColor" :active-text-color="variables.menuActiveTextColor"
> >
<submenu v-for="menu in menus" :key="menu.url" :menu="menu" />
<submenu
v-for="menu in menus"
:key="menu.url"
:menu="menu"
/>
</el-menu> </el-menu>
</el-scrollbar> </el-scrollbar>
</template> </template>

View File

@ -1,11 +1,12 @@
<template> <template>
<div <div
class="left" class="left"
:class="{collapse:collapse}" :class="{ collapse: collapse, mobile: device === 'mobile' }"
> >
<logo /> <logo />
<menus :collapse="collapse" /> <menus :collapse="collapse" />
</div> </div>
<div class="mask" @click="closeSidebar"></div>
</template> </template>
<script> <script>
@ -22,9 +23,16 @@ export default defineComponent({
setup() { setup() {
const store = useStore(); const store = useStore();
const collapse = computed(() => !!store.state.app.sidebar.collapse); const collapse = computed(() => !!store.state.app.sidebar.collapse);
const device = computed(() => store.state.app.device);
const closeSidebar = () => {
store.commit("app/setCollapse", 1);
};
return { return {
collapse, collapse,
device,
closeSidebar,
}; };
}, },
}); });
@ -44,5 +52,27 @@ export default defineComponent({
display: none; display: none;
} }
} }
&.mobile {
height: 100%;
position: fixed;
left: 0;
top: 0;
z-index: 10;
& + .mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 9;
}
&.collapse {
transform: translateX(-100%);
& + .mask {
display: none;
}
}
}
} }
</style> </style>

View File

@ -1,26 +0,0 @@
<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,81 @@
import { onMounted, onBeforeUnmount, reactive, toRefs, nextTick } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useStore } from 'vuex'
import { isAffix } from './useTags'
export const useContextMenu = (tagList) => {
const store = useStore()
const router = useRouter()
const route = useRoute()
const state = reactive({
visible: false,
top: 0,
left: 0,
selectedTag: {},
openMenu (tag, e) {
state.visible = true;
state.left = e.clientX;
state.top = e.clientY;
state.selectedTag = tag;
},
closeMenu () {
state.visible = false;
},
refreshSelectedTag (tag) {
store.dispatch("tags/delCacheList", tag).then(() => {
const { fullPath } = tag;
nextTick(() => {
router.replace({
path: "/redirect" + fullPath,
});
});
});
},
closeTag (tag) {
if (isAffix(tag)) return;
const closedTagIndex = tagList.value.findIndex(
(item) => item.fullPath === tag.fullPath
);
store.dispatch("tags/delTag", tag).then(() => {
if (isActive(tag)) {
toLastTag(closedTagIndex - 1);
}
});
},
closeOtherTags () {
store.dispatch("tags/delOtherTags", state.selectedTag).then(() => {
router.push(state.selectedTag);
});
},
closeAllTags () {
store.dispatch("tags/delAllTags").then(() => {
router.push("/");
});
}
})
const isActive = (tag) => {
return tag.fullPath === route.fullPath;
}
const toLastTag = (lastTagIndex) => {
const lastTag = tagList.value[lastTagIndex];
if (!!lastTag) {
router.push(lastTag.fullPath);
} else {
router.push("/");
}
}
onMounted(() => {
document.addEventListener("click", state.closeMenu);
});
onBeforeUnmount(() => {
document.removeEventListener("click", state.closeMenu);
});
return toRefs(state)
}

View File

@ -0,0 +1,39 @@
import { ref } from 'vue'
export const useScrollbar = (tagsItem) => {
const scrollContainer = ref(null);
const handleScroll = (e) => {
const eventDelta = e.wheelDelta || -e.deltaY;
scrollContainer.value.wrap.scrollLeft -= eventDelta / 4;
};
const moveToTarget = (currentTag) => {
const containerWidth = scrollContainer.value.scrollbar.offsetWidth;
const scrollWrapper = scrollContainer.value.wrap;
const tagList = tagsItem.value;
let firstTag = null;
let lastTag = null;
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 {
const el = currentTag.$el.nextElementSibling
scrollWrapper.scrollLeft = el.offsetLeft + el.offsetWidth > containerWidth ? el.offsetLeft - el.offsetWidth : 0
}
};
return {
scrollContainer,
handleScroll,
moveToTarget,
};
}

View File

@ -0,0 +1,94 @@
import { useScrollbar } from "./useScrollbar";
import { onMounted, watch, computed, ref, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex'
export const isAffix = (tag) => {
return !!tag.meta && !!tag.meta.affix;
};
export const useTags = () => {
const store = useStore()
const router = useRouter();
const route = router.currentRoute;
const routes = computed(() => router.getRoutes());
const tagList = computed(() => store.state.tags.tagList);
const tagsItem = ref([])
const setItemRef = (i, el) => {
tagsItem.value[i] = el
}
const scrollbar = useScrollbar(tagsItem);
watch(() => tagList.value.length, () => {
tagsItem.value = []
})
const filterAffixTags = (routes) => {
return routes.filter((route) => isAffix(route));
};
const initTags = () => {
const affixTags = filterAffixTags(routes.value);
for (const tag of affixTags) {
if (!!tag.name) {
store.dispatch("tags/addTagList", tag);
}
}
};
const addTag = () => {
const tag = route.value;
if (!!tag.name && tag.matched[0].components.default.name === "layout") {
store.dispatch("tags/addTag", tag);
}
};
const saveActivePosition = (tag) => {
const index = tagList.value.findIndex(
(item) => item.fullPath === tag.fullPath
);
store.dispatch("tags/saveActivePosition", Math.max(0, index));
};
const moveToCurrentTag = () => {
nextTick(() => {
for (const tag of tagsItem.value) {
if (!!tag && (tag.to.path === route.value.path)) {
scrollbar.moveToTarget(tag);
if (tag.to.fullPath !== route.value.fullPath) {
store.dispatch("tags/updateTagList", route.value);
}
break;
}
}
});
};
onMounted(() => {
initTags();
addTag();
moveToCurrentTag();
});
watch(route, (newRoute, oldRoute) => {
saveActivePosition(oldRoute); // 保存标签的位置
addTag();
moveToCurrentTag();
});
return {
tagList,
setItemRef,
isAffix,
...scrollbar
}
}

View File

@ -1,309 +1,122 @@
<template> <template>
<div <div class="tags-container">
id="tags-view-container" <el-scrollbar
class="tags-view-container" ref="scrollContainer"
> :vertical="false"
<scroll-bar class="scroll-container"
ref="scrollBar" @wheel.prevent="onScroll"
class="tags-view-wrapper"
@scroll="handleScroll"
> >
<router-link <router-link
v-for="tag in tagList" v-for="(tag, i) in tagList"
ref="tags" :key="tag.fullPath"
:key="tag.path" :to="tag"
:class="isActive(tag)?'active':''" :ref="(el) => setItemRef(i, el)"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" custom
tag="span" v-slot="{ navigate, isExactActive }"
class="tags-view-item"
@click.middle="!isAffix(tag)?closeSelectedTag(tag):''"
@contextmenu.prevent="openMenu(tag,$event)"
> >
{{ tag.title }} <div
<span class="tags-item"
v-if="!isAffix(tag)" :class="isExactActive ? 'active' : ''"
class="el-icon-refresh" @click="navigate"
@click.prevent.stop="refreshSelectedTag(tag)" @click.middle="closeTag(tag)"
/> @contextmenu.prevent="openMenu(tag, $event)"
<span >
v-if="!isAffix(tag)" <span class="title">{{ tag.title }}</span>
class="el-icon-close" <span
@click.prevent.stop="closeSelectedTag(tag)" v-if="!isAffix(tag)"
/> class="el-icon-close"
@click.prevent.stop="closeTag(tag)"
/>
</div>
</router-link> </router-link>
</scroll-bar> </el-scrollbar>
<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> </div>
<ul
v-show="visible"
:style="{ left: left + 'px', top: top + 'px' }"
class="contextmenu"
>
<li @click="refreshSelectedTag(selectedTag)">刷新</li>
<li v-if="!isAffix(selectedTag)" @click="closeTag(selectedTag)">关闭</li>
<li @click="closeOtherTags">关闭其他</li>
<li @click="closeAllTags">关闭全部</li>
</ul>
</template> </template>
<script> <script>
import ScrollBar from "./ScrollBar.vue"; import { defineComponent } from "vue";
import path from "path"; import { useTags } from "./hooks/useTags";
import { import { useContextMenu } from "./hooks/useContextMenu";
computed,
defineComponent,
reactive,
watch,
toRefs,
onMounted,
onBeforeUnmount,
ref,
nextTick,
} from "vue";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
export default defineComponent({ export default defineComponent({
name: "Tagsbar", name: "Tagsbar",
components: { ScrollBar },
setup() { setup() {
const store = useStore(); const tags = useTags();
const router = useRouter(); const contextMenu = useContextMenu(tags.tagList);
const route = router.currentRoute;
const tags = ref(null); const onScroll = (e) => {
const scrollBar = ref(null); tags.handleScroll(e);
contextMenu.closeMenu.value();
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 { return {
tagList, onScroll,
routes, ...tags,
tags, ...contextMenu,
scrollBar,
...toRefs(state),
}; };
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.tags-view-container { .tags-container {
height: 34px; height: 32px;
width: 100%; width: 100%;
background: #fff; background: #fff;
border-bottom: 1px solid #d8dce5; border-bottom: 1px solid #eaeaea;
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"> .scroll-container {
//reset element css of el-icon-close white-space: nowrap;
.tags-view-wrapper { overflow: hidden;
.tags-view-item { ::v-deep(.el-scrollbar__bar) {
.el-icon-close, bottom: 0px;
.el-icon-refresh { }
}
.tags-item {
display: inline-block;
height: 32px;
line-height: 32px;
box-sizing: border-box;
border-left: 1px solid #e6e6e6;
border-right: 1px solid #e6e6e6;
color: #5c5c5c;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: -1px;
vertical-align: bottom;
cursor: pointer;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
color: $mainColor;
border-bottom: 2px solid $mainColor;
}
.title {
display: inline-block;
vertical-align: top;
max-width: 200px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.el-icon-close {
color: #5c5c5c;
margin-left: 2px; margin-left: 2px;
width: 16px; width: 16px;
height: 16px; height: 16px;
@ -324,4 +137,26 @@ export default defineComponent({
} }
} }
} }
.contextmenu {
margin: 0;
background: #fff;
z-index: 3000;
position: fixed;
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);
white-space: nowrap;
li {
margin: 0;
padding: 8px 16px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
</style> </style>

View File

@ -2,23 +2,27 @@
<el-breadcrumb <el-breadcrumb
separator="/" separator="/"
class="breadcrumb" class="breadcrumb"
:class="{ mobile: device === 'mobile' }"
> >
<el-breadcrumb-item <el-breadcrumb-item
v-for="(item, index) in breadcrumbs" v-for="(item, index) in breadcrumbs"
:key="index" :key="index"
:class="{no_link: index === breadcrumbs.length - 1}" :class="{ no_link: index === breadcrumbs.length - 1 }"
:to="index < breadcrumbs.length - 1 ? item.path : ''" :to="index < breadcrumbs.length - 1 ? item.path : ''"
> >
{{item.meta.title}} {{ item.meta.title }}
</el-breadcrumb-item> </el-breadcrumb-item>
</el-breadcrumb> </el-breadcrumb>
</template> </template>
<script> <script>
import { defineComponent, ref, onBeforeMount, watch } from "vue"; import { defineComponent, computed, ref, onBeforeMount, watch } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useStore } from "vuex";
export default defineComponent({ export default defineComponent({
setup() { setup() {
const store = useStore();
const device = computed(() => store.state.app.device);
const router = useRouter(); const router = useRouter();
const route = router.currentRoute; // 使useRoutewatch const route = router.currentRoute; // 使useRoutewatch
const breadcrumbs = ref([]); const breadcrumbs = ref([]);
@ -45,6 +49,7 @@ export default defineComponent({
}); });
return { return {
device,
breadcrumbs, breadcrumbs,
}; };
}, },
@ -54,15 +59,23 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.breadcrumb { .breadcrumb {
margin-left: 10px; margin-left: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
::v-deep(a), ::v-deep(a),
::v-deep(.is-link) { ::v-deep(.is-link) {
font-weight: normal; font-weight: normal;
} }
::v-deep(.el-breadcrumb__item) {
float: none;
}
.no_link { .no_link {
::v-deep(.el-breadcrumb__inner) { ::v-deep(.el-breadcrumb__inner) {
color: #97a8be !important; color: #97a8be !important;
} }
} }
&.mobile {
display: none;
}
} }
</style> </style>

View File

@ -1,13 +1,14 @@
<template> <template>
<i <i
class="fold-btn el-icon-s-fold" class="fold-btn el-icon-s-fold"
:class="{collapse:collapse}" :class="{ collapse: collapse }"
@click="handleToggleMenu" @click="handleToggleMenu"
></i> ></i>
</template> </template>
<script> <script>
import { defineComponent, computed } from "vue"; import { defineComponent, computed } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
export default defineComponent({ export default defineComponent({
setup() { setup() {
const store = useStore(); const store = useStore();

View File

@ -1,14 +1,13 @@
<template> <template>
<el-dropdown> <el-dropdown trigger="click">
<div class="userinfo"> <div class="userinfo">
<template v-if="!userinfo"> <template v-if="!userinfo">
<i class="el-icon-user" /> admin <i class="el-icon-user" />
admin
</template> </template>
<template v-else> <template v-else>
<img <img class="avatar" :src="userinfo.avatar" />
class="avatar" {{ userinfo.name }}
:src="userinfo.avatar"
/> {{userinfo.name}}
</template> </template>
</div> </div>
<template #dropdown> <template #dropdown>

View File

@ -1,6 +1,7 @@
<template> <template>
<div class="header"> <div class="header">
<div class="navigation"> <div class="navigation">
<logo v-if="device === 'mobile'" class="mobile" />
<hamburger /> <hamburger />
<breadcrumbs /> <breadcrumbs />
</div> </div>
@ -10,17 +11,28 @@
</div> </div>
</template> </template>
<script> <script>
import { defineComponent } from "vue"; import { defineComponent, computed } from "vue";
import Logo from "@/layout/components/Sidebar/Logo.vue";
import Hamburger from "./Hamburger.vue"; import Hamburger from "./Hamburger.vue";
import Breadcrumbs from "./Breadcrumbs.vue"; import Breadcrumbs from "./Breadcrumbs.vue";
import Userinfo from "./Userinfo.vue"; import Userinfo from "./Userinfo.vue";
import { useStore } from "vuex";
export default defineComponent({ export default defineComponent({
components: { components: {
Logo,
Hamburger, Hamburger,
Breadcrumbs, Breadcrumbs,
Userinfo, Userinfo,
}, },
setup() {}, setup() {
const store = useStore();
const device = computed(() => store.state.app.device);
return {
device,
};
},
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -32,6 +44,17 @@ export default defineComponent({
.navigation { .navigation {
display: flex; display: flex;
align-items: center; align-items: center;
overflow: hidden;
}
}
.mobile {
padding-right: 0;
::v-deep(.logo) {
max-width: 24px;
max-height: 24px;
}
::v-deep(.title) {
display: none;
} }
} }
</style> </style>

View File

@ -0,0 +1,46 @@
import { onBeforeMount, onBeforeUnmount, watch } from "vue"
import { useRouter } from "vue-router"
import { useStore } from "vuex"
const WIDTH = 768
export const useResizeHandler = () => {
const store = useStore()
const router = useRouter()
const route = router.currentRoute
const isMobile = () => {
return window.innerWidth < WIDTH
}
const resizeHandler = () => {
if (isMobile()) {
store.commit('app/setDevice', 'mobile')
store.commit('app/setCollapse', 1)
} else {
store.commit('app/setDevice', 'desktop')
store.commit('app/setCollapse', 0)
}
}
onBeforeMount(() => {
if (isMobile()) {
store.commit('app/setDevice', 'mobile')
store.commit('app/setCollapse', 1)
}
window.addEventListener('resize', resizeHandler)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeHandler)
})
// // 监听路由的时候不能使用useRoute获取路由否则会有警告
// watch(route, () => {
// if (store.state.app.device === 'mobile' && !store.state.app.sidebar.collapse) {
// store.commit('app/setCollapse', 1)
// }
// })
}

View File

@ -6,7 +6,9 @@
<topbar /> <topbar />
<tagsbar /> <tagsbar />
</div> </div>
<content /> <div class="main">
<content />
</div>
</div> </div>
</div> </div>
</template> </template>
@ -16,14 +18,19 @@ import Sidebar from "./components/Sidebar/index.vue";
import Topbar from "./components/Topbar/index.vue"; import Topbar from "./components/Topbar/index.vue";
import Tagsbar from "./components/Tagsbar/index.vue"; import Tagsbar from "./components/Tagsbar/index.vue";
import Content from "./components/Content/index.vue"; import Content from "./components/Content/index.vue";
import { useResizeHandler } from "./hooks/useResizeHandler";
export default defineComponent({ export default defineComponent({
name: "layout",
components: { components: {
Sidebar, Sidebar,
Topbar, Topbar,
Tagsbar, Tagsbar,
Content, Content,
}, },
setup() {
useResizeHandler();
},
}); });
</script> </script>
@ -34,11 +41,18 @@ export default defineComponent({
.right { .right {
flex: 1; flex: 1;
overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.top { .top {
background: #fff; background: #fff;
} }
.main {
flex: 1;
background: #f0f2f5;
padding: 16px;
overflow: auto;
}
} }
} }
</style> </style>

View File

@ -1,6 +1,7 @@
// index.js // index.js
import { createRouter, createWebHashHistory } from "vue-router" import { createRouter, createWebHashHistory } from "vue-router"
import redirect from './modules/redirect'
import error from './modules/error' import error from './modules/error'
import login from './modules/login' import login from './modules/login'
import home from './modules/home' import home from './modules/home'
@ -20,10 +21,18 @@ const router = createRouter({
path: '/', path: '/',
redirect: '/home', redirect: '/home',
}, },
...redirect, // 统一的重定向配置
...login, ...login,
...allMenus, ...allMenus,
...error ...error
], ],
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { left: 0, top: 0 }
}
},
}); });
export default router; export default router;

View File

@ -1,16 +1,17 @@
import layout from '@/layout/index.vue' const Layout = () => import('@/layout/index.vue')
const Error = () => import("@/views/error/index.vue"); const Error = () => import("@/views/error/index.vue");
export default [ export default [
{ {
path: '/error', path: '/error',
component: layout, component: Layout,
children: [ children: [
{ {
path: '403', path: '403',
name: 'error-forbidden', name: 'error-forbidden',
component: Error, component: Error,
meta: { title: '403' },
props: { props: {
error: '403' error: '403'
} }
@ -19,6 +20,7 @@ export default [
path: '500', path: '500',
name: 'error-server-error', name: 'error-server-error',
component: Error, component: Error,
meta: { title: '500' },
props: { props: {
error: '500' error: '500'
} }
@ -27,6 +29,7 @@ export default [
path: '404', path: '404',
name: 'error-not-found', name: 'error-not-found',
component: Error, component: Error,
meta: { title: '404' },
props: { props: {
error: '404' error: '404'
} }

View File

@ -1,11 +1,11 @@
// home.js // home.js
import layout from '@/layout/index.vue' const Layout = () => import('@/layout/index.vue')
const Home = () => import("@/views/home/index.vue"); const Home = () => import("@/views/home/index.vue");
export default [ export default [
{ {
path: '/home', path: '/home',
component: layout, component: Layout,
name: "Dashboard", name: "Dashboard",
meta: { meta: {
title: "Dashboard", title: "Dashboard",
@ -18,6 +18,7 @@ export default [
component: Home, component: Home,
meta: { meta: {
title: "首页", title: "首页",
affix: true
} }
} }
] ]

View File

@ -0,0 +1,8 @@
const Redirect = () => import("@/views/redirect/index.vue");
export default [
{
path: '/redirect/:path(.*)',
component: Redirect,
}
]

View File

@ -1,15 +1,18 @@
import layout from '@/layout/index.vue' const Layout = () => import('@/layout/index.vue')
const List = () => import("@/views/test/index.vue"); const List = () => import("@/views/test/index.vue");
const Add = () => import("@/views/test/Add.vue"); const Add = () => import("@/views/test/Add.vue");
const Auth = () => import("@/views/test/Auth.vue"); const Auth = () => import("@/views/test/Auth.vue");
const NoAuth = () => import("@/views/test/NoAuth.vue");
const Nest = () => import("@/views/test/Nest.vue"); const Nest = () => import("@/views/test/Nest.vue");
const NestPage1 = () => import("@/views/test/nest/Page1.vue"); const NestPage1 = () => import("@/views/test/nest/Page1.vue");
const NestPage2 = () => import("@/views/test/nest/Page2.vue"); const NestPage2 = () => import("@/views/test/nest/Page2.vue");
const Iscache = () => import("@/views/test/Cache.vue");
const Nocache = () => import("@/views/test/Nocache.vue");
export default [ export default [
{ {
path: '/test', path: '/test',
component: layout, component: Layout,
name: "test", name: "test",
meta: { meta: {
title: "测试页面", title: "测试页面",
@ -40,9 +43,38 @@ export default [
path: "auth", path: "auth",
name: "testAuth", name: "testAuth",
component: Auth, component: Auth,
meta: {
title: "权限测试",
roles: ["admin", "visitor"],
}
},
{
path: "noauth",
name: "testNoAuth",
component: NoAuth,
meta: { meta: {
title: "权限页面", title: "权限页面",
roles: ["admin"], roles: ["admin"],
},
hidden: true
},
{
path: "cache",
name: "test-cache",
component: Iscache,
meta: {
title: "该页面可缓存",
roles: ["admin", "visitor"]
}
},
{
path: "nocache",
name: "test-no-cache",
component: Nocache,
meta: {
title: "该页面不缓存",
roles: ["admin", "visitor"],
noCache: true, // 不缓存页面
} }
}, },
{ {

View File

@ -1,5 +1,6 @@
import { getItem, setItem, removeItem } from "@/utils/storage"; //getItem和setItem是封装的操作localStorage的方法 import { getItem, setItem, removeItem } from "@/utils/storage"; //getItem和setItem是封装的操作localStorage的方法
export const TOKEN = "TOKEN"; export const TOKEN = "VEA-TOKEN";
const COLLAPSE = "VEA-COLLAPSE";
export default { export default {
namespaced: true, namespaced: true,
@ -7,8 +8,9 @@ export default {
title: 'Vue3 Element Admin', title: 'Vue3 Element Admin',
authorization: getItem(TOKEN), authorization: getItem(TOKEN),
sidebar: { sidebar: {
collapse: getItem('collapse') collapse: getItem(COLLAPSE)
} },
device: 'desktop',
}, },
mutations: { mutations: {
setToken (state, data) { setToken (state, data) {
@ -18,19 +20,22 @@ export default {
}, },
clearToken (state) { clearToken (state) {
state.authorization = ''; state.authorization = '';
// 保存到localStorage
removeItem(TOKEN); removeItem(TOKEN);
}, },
setCollapse (state, data) { setCollapse (state, data) {
state.sidebar.collapse = data; state.sidebar.collapse = data;
// 保存到localStorage // 保存到localStorage
setItem('collapse', data); setItem(COLLAPSE, data);
}, },
clearCollapse (state) { clearCollapse (state) {
state.sidebar.collapse = ''; state.sidebar.collapse = '';
// 保存到localStorage
removeItem('collapse'); removeItem(COLLAPSE);
} },
setDevice (state, device) {
state.device = device
},
}, },
actions: {}, actions: {},
}; };

View File

@ -1,20 +1,28 @@
import { getItem, setItem, removeItem } from "@/utils/storage"; //getItem和setItem是封装的操作localStorage的方法
const TAGLIST = 'VEA-TAGLIST'
const state = { const state = {
tagList: [], tagList: getItem(TAGLIST) || [],
cacheList: [], cacheList: [],
activePosition: 0 activePosition: 0
} }
const mutations = { const mutations = {
ADD_TAG_LIST: (state, tag) => { ADD_TAG_LIST: (state, { path, fullPath, name, meta }) => {
if (state.tagList.some(v => v.path === tag.path)) return; if (state.tagList.some(v => v.path === path)) return false;
state.tagList.splice( state.tagList.splice(
state.activePosition + 1, state.activePosition + 1,
0, 0,
Object.assign({}, tag, { Object.assign({}, { path, fullPath, name, meta }, {
title: tag.meta.title || 'no-name' title: meta.title || '未命名',
fullPath: fullPath || path
}) })
) )
// 保存到localStorage
setItem(TAGLIST, state.tagList);
}, },
ADD_CACHE_LIST: (state, tag) => { ADD_CACHE_LIST: (state, tag) => {
if (state.cacheList.includes(tag.name)) return if (state.cacheList.includes(tag.name)) return
@ -25,6 +33,8 @@ const mutations = {
DEL_TAG_LIST: (state, tag) => { DEL_TAG_LIST: (state, tag) => {
state.tagList = state.tagList.filter(v => v.path !== tag.path) state.tagList = state.tagList.filter(v => v.path !== tag.path)
// 保存到localStorage
setItem(TAGLIST, state.tagList);
}, },
DEL_CACHE_LIST: (state, tag) => { DEL_CACHE_LIST: (state, tag) => {
state.cacheList = state.cacheList.filter(v => v !== tag.name) state.cacheList = state.cacheList.filter(v => v !== tag.name)
@ -32,6 +42,8 @@ const mutations = {
DEL_OTHER_TAG_LIST: (state, tag) => { DEL_OTHER_TAG_LIST: (state, tag) => {
state.tagList = state.tagList.filter(v => !!v.meta.affix || v.path === tag.path) state.tagList = state.tagList.filter(v => !!v.meta.affix || v.path === tag.path)
// 保存到localStorage
setItem(TAGLIST, state.tagList);
}, },
DEL_OTHER_CACHE_LIST: (state, tag) => { DEL_OTHER_CACHE_LIST: (state, tag) => {
state.cacheList = state.cacheList.filter(v => v === tag.name) state.cacheList = state.cacheList.filter(v => v === tag.name)
@ -39,6 +51,8 @@ const mutations = {
DEL_ALL_TAG_LIST: state => { DEL_ALL_TAG_LIST: state => {
state.tagList = state.tagList.filter(v => !!v.meta.affix) state.tagList = state.tagList.filter(v => !!v.meta.affix)
// 保存到localStorage
removeItem(TAGLIST);
}, },
DEL_ALL_CACHE_LIST: state => { DEL_ALL_CACHE_LIST: state => {
state.cacheList = [] state.cacheList = []
@ -47,7 +61,9 @@ const mutations = {
UPDATE_TAG_LIST: (state, tag) => { UPDATE_TAG_LIST: (state, tag) => {
const index = state.tagList.findIndex(v => v.path === tag.path); const index = state.tagList.findIndex(v => v.path === tag.path);
if (index > -1) { if (index > -1) {
state.tagList[index] = Object.assign({}, tag) state.tagList[index] = Object.assign({}, state.tagList[index], tag)
// 保存到localStorage
setItem(TAGLIST, state.tagList);
} }
}, },

View File

@ -1,6 +1,16 @@
<template> <template>
<div class="home">home</div> <div class="home">home</div>
</template> </template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "home",
setup() {},
});
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
.home { .home {
color: $mainColor; color: $mainColor;

View File

@ -1,12 +1,7 @@
<template> <template>
<div class="login"> <div class="login">
<el-form <el-form class="form" :model="model" :rules="rules" ref="loginForm">
class="form" <h1 class="title">Vue3 Element Admin</h1>
:model="model"
:rules="rules"
ref="loginForm"
>
<h1 class="title">欢迎登录ERP系统</h1>
<el-form-item prop="userName"> <el-form-item prop="userName">
<el-input <el-input
class="text" class="text"
@ -29,10 +24,12 @@
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button <el-button
:loading="loading"
type="primary" type="primary"
class="btn" class="btn"
@click="submit" @click="submit"
>{{btnText}}</el-button> >{{ btnText }}</el-button
>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
@ -87,20 +84,18 @@ export default defineComponent({
if (valid) { if (valid) {
state.loading = true; state.loading = true;
const { code, data, message } = await Login(state.model); const { code, data, message } = await Login(state.model);
state.loading = false;
if (+code === 200) { if (+code === 200) {
ctx.$message.success({ ctx.$message.success({
message: "登录成功", message: "登录成功",
duration: 500, duration: 1000,
onClose: () => {
const targetPath = route.query.redirect;
router.push(!!targetPath ? targetPath : "/");
},
}); });
const targetPath = route.query.redirect;
router.push(!!targetPath ? targetPath : "/");
store.commit("app/setToken", data); store.commit("app/setToken", data);
} else { } else {
ctx.$message.error(message); ctx.$message.error(message);
} }
state.loading = false;
} }
}); });
}, },
@ -124,6 +119,8 @@ export default defineComponent({
.form { .form {
width: 520px; width: 520px;
max-width: 100%; max-width: 100%;
padding: 0 24px;
box-sizing: border-box;
margin: 160px auto 0; margin: 160px auto 0;
.title { .title {
color: #fff; color: #fff;

View File

@ -0,0 +1,8 @@
<script>
export default {
created() {
this.$router.replace(this.$route.fullPath.replace(/^\/redirect/, ""));
},
render() {},
};
</script>

View File

@ -1,3 +1,7 @@
<template> <template>
添加 <h2>该页面入口不在菜单中显示</h2>
<div>
如果不需要在菜单中显示<br />
需要配置路由增加属性hidden: true注意不是在meta中增加该属性而是跟meta同级
</div>
</template> </template>

View File

@ -1,3 +1,8 @@
<template> <template>
权限页面 <h2>
当前用户角色:{{
$store.state.account.userinfo && $store.state.account.userinfo.role
}}
</h2>
<router-link to="/test/noauth">点击进入只有admin才能访问的页面</router-link>
</template> </template>

17
src/views/test/Cache.vue Normal file
View File

@ -0,0 +1,17 @@
<template>
<dl>
<dt>页面缓存必须满足以下条件</dt>
<dd>1. 路由配置name属性</dd>
<dd>2. 当前页面设置name属性并且跟路由配置的name属性一致否则无法缓存</dd>
</dl>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "test-cache", // namename
setup() {
console.log("cache");
},
});
</script>

View File

@ -1,4 +1,5 @@
<template> <template>
<h1>二级菜单</h1> <h1>二级菜单</h1>
<h3>二级菜单的页面是不会缓存的</h3>
<router-view /> <router-view />
</template> </template>

View File

@ -0,0 +1 @@
<template>该页面只有admin能访问</template>

View File

@ -0,0 +1,17 @@
<template>
<dl>
<dt>有以下两种方式设置页面不缓存</dt>
<dd>- 当前页面不设置name属性</dd>
<dd>- 或者路由配置的meta增加noCache: true</dd>
</dl>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "test-no-cache", // namenamename
setup() {
console.log("nocache");
},
});
</script>

View File

@ -1,3 +1,6 @@
<template> <template>
列表 <h2>列表</h2>
<router-link to="/test/add">
<el-button type="primary">添加一条</el-button>
</router-link>
</template> </template>

View File

@ -1,3 +1 @@
<template> <template>Page1</template>
Page1
</template>

View File

@ -1,3 +1 @@
<template> <template>Page2</template>
Page2
</template>