update
This commit is contained in:
parent
f4eaebdb41
commit
8da758f494
@ -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')"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
$mainColor: #409eff; // 网站主题色
|
$mainColor: #409eff; // 网站主题色
|
||||||
|
|
||||||
// 菜单配置
|
// 侧边栏
|
||||||
$menuBg: #304156; // 菜单背景颜色
|
$menuBg: #304156; // 菜单背景颜色
|
||||||
$menuTextColor: #fff; // 菜单文字颜色
|
$menuTextColor: #fff; // 菜单文字颜色
|
||||||
$menuActiveTextColor: $mainColor; // 已选中菜单文字颜色
|
$menuActiveTextColor: $mainColor; // 已选中菜单文字颜色
|
||||||
|
|||||||
@ -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>
|
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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>
|
|
||||||
81
src/layout/components/Tagsbar/hooks/useContextMenu.js
Normal file
81
src/layout/components/Tagsbar/hooks/useContextMenu.js
Normal 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)
|
||||||
|
}
|
||||||
39
src/layout/components/Tagsbar/hooks/useScrollbar.js
Normal file
39
src/layout/components/Tagsbar/hooks/useScrollbar.js
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
94
src/layout/components/Tagsbar/hooks/useTags.js
Normal file
94
src/layout/components/Tagsbar/hooks/useTags.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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; // 这里不使用useRoute获取当前路由,否则下面watch监听路由的时候会有警告
|
const route = router.currentRoute; // 这里不使用useRoute获取当前路由,否则下面watch监听路由的时候会有警告
|
||||||
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>
|
||||||
@ -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();
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
46
src/layout/hooks/useResizeHandler.js
Normal file
46
src/layout/hooks/useResizeHandler.js
Normal 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)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -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>
|
||||||
@ -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;
|
||||||
@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
8
src/router/modules/redirect.js
Normal file
8
src/router/modules/redirect.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const Redirect = () => import("@/views/redirect/index.vue");
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: '/redirect/:path(.*)',
|
||||||
|
component: Redirect,
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -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, // 不缓存页面
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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: {},
|
||||||
};
|
};
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
8
src/views/redirect/index.vue
Normal file
8
src/views/redirect/index.vue
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
created() {
|
||||||
|
this.$router.replace(this.$route.fullPath.replace(/^\/redirect/, ""));
|
||||||
|
},
|
||||||
|
render() {},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -1,3 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
添加
|
<h2>该页面入口不在菜单中显示</h2>
|
||||||
|
<div>
|
||||||
|
如果不需要在菜单中显示:<br />
|
||||||
|
需要配置路由增加属性hidden: true,注意不是在meta中增加该属性,而是跟meta同级
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -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
17
src/views/test/Cache.vue
Normal 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", // 该name必须跟路由配置的name一致
|
||||||
|
setup() {
|
||||||
|
console.log("cache");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<h1>二级菜单</h1>
|
<h1>二级菜单</h1>
|
||||||
|
<h3>二级菜单的页面是不会缓存的</h3>
|
||||||
<router-view />
|
<router-view />
|
||||||
</template>
|
</template>
|
||||||
1
src/views/test/NoAuth.vue
Normal file
1
src/views/test/NoAuth.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template>该页面只有admin能访问</template>
|
||||||
17
src/views/test/Nocache.vue
Normal file
17
src/views/test/Nocache.vue
Normal 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", // 该name必须跟路由配置的name一致,不一致或者不设置name则不缓存
|
||||||
|
setup() {
|
||||||
|
console.log("nocache");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -1,3 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
列表
|
<h2>列表</h2>
|
||||||
|
<router-link to="/test/add">
|
||||||
|
<el-button type="primary">添加一条</el-button>
|
||||||
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
@ -1,3 +1 @@
|
|||||||
<template>
|
<template>Page1</template>
|
||||||
Page1
|
|
||||||
</template>
|
|
||||||
|
|||||||
@ -1,3 +1 @@
|
|||||||
<template>
|
<template>Page2</template>
|
||||||
Page2
|
|
||||||
</template>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user