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: {
id: 1,
name: 'zhangsan',
role: 'visitor',
'role|1': ['admin', 'visitor'], // 随机返回一个角色admin或visitor
avatar: "@image('48x48', '#fb0a2a')"
}
},

View File

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

View File

@ -1,16 +1,9 @@
<template>
<div class="main">
<router-view v-slot="{ Component }">
<keep-alive :include="cacheList">
<component
:is="Component"
:key="key"
/>
<component :is="Component" :key="key" />
</keep-alive>
</router-view>
</div>
</template>
<script>
import { computed, defineComponent } from "vue";
@ -22,7 +15,7 @@ export default defineComponent({
const store = useStore();
const route = useRoute();
const cacheList = computed(() => store.state.tags.cacheList);
const key = computed(() => route.path);
const key = computed(() => route.fullPath);
return {
cacheList,
@ -31,12 +24,3 @@ export default defineComponent({
},
});
</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"
: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-scrollbar>
</template>

View File

@ -1,11 +1,12 @@
<template>
<div
class="left"
:class="{collapse:collapse}"
:class="{ collapse: collapse, mobile: device === 'mobile' }"
>
<logo />
<menus :collapse="collapse" />
</div>
<div class="mask" @click="closeSidebar"></div>
</template>
<script>
@ -22,9 +23,16 @@ export default defineComponent({
setup() {
const store = useStore();
const collapse = computed(() => !!store.state.app.sidebar.collapse);
const device = computed(() => store.state.app.device);
const closeSidebar = () => {
store.commit("app/setCollapse", 1);
};
return {
collapse,
device,
closeSidebar,
};
},
});
@ -44,5 +52,27 @@ export default defineComponent({
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>

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,251 +1,93 @@
<template>
<div
id="tags-view-container"
class="tags-view-container"
>
<scroll-bar
ref="scrollBar"
class="tags-view-wrapper"
@scroll="handleScroll"
<div class="tags-container">
<el-scrollbar
ref="scrollContainer"
:vertical="false"
class="scroll-container"
@wheel.prevent="onScroll"
>
<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):''"
v-for="(tag, i) in tagList"
:key="tag.fullPath"
:to="tag"
:ref="(el) => setItemRef(i, el)"
custom
v-slot="{ navigate, isExactActive }"
>
<div
class="tags-item"
:class="isExactActive ? 'active' : ''"
@click="navigate"
@click.middle="closeTag(tag)"
@contextmenu.prevent="openMenu(tag, $event)"
>
{{ tag.title }}
<span
v-if="!isAffix(tag)"
class="el-icon-refresh"
@click.prevent.stop="refreshSelectedTag(tag)"
/>
<span class="title">{{ tag.title }}</span>
<span
v-if="!isAffix(tag)"
class="el-icon-close"
@click.prevent.stop="closeSelectedTag(tag)"
@click.prevent.stop="closeTag(tag)"
/>
</div>
</router-link>
</scroll-bar>
</el-scrollbar>
</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="closeSelectedTag(selectedTag)"
>关闭</li>
<li @click="closeOthersTags">关闭其他</li>
<li @click="closeAllTags(selectedTag)">关闭全部</li>
<li v-if="!isAffix(selectedTag)" @click="closeTag(selectedTag)">关闭</li>
<li @click="closeOtherTags">关闭其他</li>
<li @click="closeAllTags">关闭全部</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";
import { defineComponent } from "vue";
import { useTags } from "./hooks/useTags";
import { useContextMenu } from "./hooks/useContextMenu";
export default defineComponent({
name: "Tagsbar",
components: { ScrollBar },
setup() {
const store = useStore();
const router = useRouter();
const route = router.currentRoute;
const tags = useTags();
const contextMenu = useContextMenu(tags.tagList);
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);
});
const onScroll = (e) => {
tags.handleScroll(e);
contextMenu.closeMenu.value();
};
return {
tagList,
routes,
tags,
scrollBar,
...toRefs(state),
onScroll,
...tags,
...contextMenu,
};
},
});
</script>
<style lang="scss" scoped>
.tags-view-container {
height: 34px;
.tags-container {
height: 32px;
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 {
border-bottom: 1px solid #eaeaea;
.scroll-container {
white-space: nowrap;
overflow: hidden;
::v-deep(.el-scrollbar__bar) {
bottom: 0px;
}
}
.tags-item {
display: inline-block;
position: relative;
cursor: pointer;
line-height: 31px;
height: 32px;
line-height: 32px;
box-sizing: border-box;
border-left: 1px solid #e6e6e6;
border-right: 1px solid #e6e6e6;
color: #5c5c5c;
@ -253,6 +95,8 @@ export default defineComponent({
padding: 0 8px;
font-size: 12px;
margin-left: -1px;
vertical-align: bottom;
cursor: pointer;
&:first-of-type {
margin-left: 15px;
}
@ -260,50 +104,19 @@ export default defineComponent({
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;
// }
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;
}
}
.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 {
.el-icon-close {
color: #5c5c5c;
margin-left: 2px;
width: 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>

View File

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

View File

@ -8,6 +8,7 @@
<script>
import { defineComponent, computed } from "vue";
import { useStore } from "vuex";
export default defineComponent({
setup() {
const store = useStore();

View File

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

View File

@ -1,6 +1,7 @@
<template>
<div class="header">
<div class="navigation">
<logo v-if="device === 'mobile'" class="mobile" />
<hamburger />
<breadcrumbs />
</div>
@ -10,17 +11,28 @@
</div>
</template>
<script>
import { defineComponent } from "vue";
import { defineComponent, computed } from "vue";
import Logo from "@/layout/components/Sidebar/Logo.vue";
import Hamburger from "./Hamburger.vue";
import Breadcrumbs from "./Breadcrumbs.vue";
import Userinfo from "./Userinfo.vue";
import { useStore } from "vuex";
export default defineComponent({
components: {
Logo,
Hamburger,
Breadcrumbs,
Userinfo,
},
setup() {},
setup() {
const store = useStore();
const device = computed(() => store.state.app.device);
return {
device,
};
},
});
</script>
<style lang="scss" scoped>
@ -32,6 +44,17 @@ export default defineComponent({
.navigation {
display: flex;
align-items: center;
overflow: hidden;
}
}
.mobile {
padding-right: 0;
::v-deep(.logo) {
max-width: 24px;
max-height: 24px;
}
::v-deep(.title) {
display: none;
}
}
</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,9 +6,11 @@
<topbar />
<tagsbar />
</div>
<div class="main">
<content />
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
@ -16,14 +18,19 @@ import Sidebar from "./components/Sidebar/index.vue";
import Topbar from "./components/Topbar/index.vue";
import Tagsbar from "./components/Tagsbar/index.vue";
import Content from "./components/Content/index.vue";
import { useResizeHandler } from "./hooks/useResizeHandler";
export default defineComponent({
name: "layout",
components: {
Sidebar,
Topbar,
Tagsbar,
Content,
},
setup() {
useResizeHandler();
},
});
</script>
@ -34,11 +41,18 @@ export default defineComponent({
.right {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
.top {
background: #fff;
}
.main {
flex: 1;
background: #f0f2f5;
padding: 16px;
overflow: auto;
}
}
}
</style>

View File

@ -1,6 +1,7 @@
// index.js
import { createRouter, createWebHashHistory } from "vue-router"
import redirect from './modules/redirect'
import error from './modules/error'
import login from './modules/login'
import home from './modules/home'
@ -20,10 +21,18 @@ const router = createRouter({
path: '/',
redirect: '/home',
},
...redirect, // 统一的重定向配置
...login,
...allMenus,
...error
],
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { left: 0, top: 0 }
}
},
});
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");
export default [
{
path: '/error',
component: layout,
component: Layout,
children: [
{
path: '403',
name: 'error-forbidden',
component: Error,
meta: { title: '403' },
props: {
error: '403'
}
@ -19,6 +20,7 @@ export default [
path: '500',
name: 'error-server-error',
component: Error,
meta: { title: '500' },
props: {
error: '500'
}
@ -27,6 +29,7 @@ export default [
path: '404',
name: 'error-not-found',
component: Error,
meta: { title: '404' },
props: {
error: '404'
}

View File

@ -1,11 +1,11 @@
// home.js
import layout from '@/layout/index.vue'
const Layout = () => import('@/layout/index.vue')
const Home = () => import("@/views/home/index.vue");
export default [
{
path: '/home',
component: layout,
component: Layout,
name: "Dashboard",
meta: {
title: "Dashboard",
@ -18,6 +18,7 @@ export default [
component: Home,
meta: {
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 Add = () => import("@/views/test/Add.vue");
const Auth = () => import("@/views/test/Auth.vue");
const NoAuth = () => import("@/views/test/NoAuth.vue");
const Nest = () => import("@/views/test/Nest.vue");
const NestPage1 = () => import("@/views/test/nest/Page1.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 [
{
path: '/test',
component: layout,
component: Layout,
name: "test",
meta: {
title: "测试页面",
@ -40,9 +43,38 @@ export default [
path: "auth",
name: "testAuth",
component: Auth,
meta: {
title: "权限测试",
roles: ["admin", "visitor"],
}
},
{
path: "noauth",
name: "testNoAuth",
component: NoAuth,
meta: {
title: "权限页面",
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的方法
export const TOKEN = "TOKEN";
export const TOKEN = "VEA-TOKEN";
const COLLAPSE = "VEA-COLLAPSE";
export default {
namespaced: true,
@ -7,8 +8,9 @@ export default {
title: 'Vue3 Element Admin',
authorization: getItem(TOKEN),
sidebar: {
collapse: getItem('collapse')
}
collapse: getItem(COLLAPSE)
},
device: 'desktop',
},
mutations: {
setToken (state, data) {
@ -18,19 +20,22 @@ export default {
},
clearToken (state) {
state.authorization = '';
// 保存到localStorage
removeItem(TOKEN);
},
setCollapse (state, data) {
state.sidebar.collapse = data;
// 保存到localStorage
setItem('collapse', data);
setItem(COLLAPSE, data);
},
clearCollapse (state) {
state.sidebar.collapse = '';
// 保存到localStorage
removeItem('collapse');
}
removeItem(COLLAPSE);
},
setDevice (state, device) {
state.device = device
},
},
actions: {},
};

View File

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

View File

@ -1,12 +1,7 @@
<template>
<div class="login">
<el-form
class="form"
:model="model"
:rules="rules"
ref="loginForm"
>
<h1 class="title">欢迎登录ERP系统</h1>
<el-form class="form" :model="model" :rules="rules" ref="loginForm">
<h1 class="title">Vue3 Element Admin</h1>
<el-form-item prop="userName">
<el-input
class="text"
@ -29,10 +24,12 @@
</el-form-item>
<el-form-item>
<el-button
:loading="loading"
type="primary"
class="btn"
@click="submit"
>{{btnText}}</el-button>
>{{ btnText }}</el-button
>
</el-form-item>
</el-form>
</div>
@ -87,20 +84,18 @@ export default defineComponent({
if (valid) {
state.loading = true;
const { code, data, message } = await Login(state.model);
state.loading = false;
if (+code === 200) {
ctx.$message.success({
message: "登录成功",
duration: 500,
onClose: () => {
duration: 1000,
});
const targetPath = route.query.redirect;
router.push(!!targetPath ? targetPath : "/");
},
});
store.commit("app/setToken", data);
} else {
ctx.$message.error(message);
}
state.loading = false;
}
});
},
@ -124,6 +119,8 @@ export default defineComponent({
.form {
width: 520px;
max-width: 100%;
padding: 0 24px;
box-sizing: border-box;
margin: 160px auto 0;
.title {
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>
添加
<h2>该页面入口不在菜单中显示</h2>
<div>
如果不需要在菜单中显示<br />
需要配置路由增加属性hidden: true注意不是在meta中增加该属性而是跟meta同级
</div>
</template>

View File

@ -1,3 +1,8 @@
<template>
权限页面
<h2>
当前用户角色:{{
$store.state.account.userinfo && $store.state.account.userinfo.role
}}
</h2>
<router-link to="/test/noauth">点击进入只有admin才能访问的页面</router-link>
</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>
<h1>二级菜单</h1>
<h3>二级菜单的页面是不会缓存的</h3>
<router-view />
</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>
列表
<h2>列表</h2>
<router-link to="/test/add">
<el-button type="primary">添加一条</el-button>
</router-link>
</template>

View File

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

View File

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