mirror of
https://github.com/Tencent/tdesign-vue-next-starter.git
synced 2024-12-23 14:33:42 +08:00
Merge branch 'develop' of github.com:Tencent/tdesign-vue-next-starter into main
This commit is contained in:
commit
8cba092ee9
|
@ -27,7 +27,7 @@
|
||||||
"qrcode.vue": "^3.2.2",
|
"qrcode.vue": "^3.2.2",
|
||||||
"qs": "^6.10.5",
|
"qs": "^6.10.5",
|
||||||
"tdesign-icons-vue-next": "^0.1.1",
|
"tdesign-icons-vue-next": "^0.1.1",
|
||||||
"tdesign-vue-next": "0.20.2",
|
"tdesign-vue-next": "0.20.3",
|
||||||
"tvision-color": "^1.3.1",
|
"tvision-color": "^1.3.1",
|
||||||
"vue": "^3.2.31",
|
"vue": "^3.2.31",
|
||||||
"vue-clipboard3": "^2.0.0",
|
"vue-clipboard3": "^2.0.0",
|
||||||
|
|
|
@ -74,7 +74,7 @@ import { MenuRoute } from '@/types/interface';
|
||||||
|
|
||||||
import Notice from './Notice.vue';
|
import Notice from './Notice.vue';
|
||||||
import Search from './Search.vue';
|
import Search from './Search.vue';
|
||||||
import MenuContent from './MenuContent';
|
import MenuContent from './MenuContent.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
theme: {
|
theme: {
|
||||||
|
|
113
src/layouts/components/LayoutContent.vue
Normal file
113
src/layouts/components/LayoutContent.vue
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
<template>
|
||||||
|
<t-layout :class="`${prefix}-layout`">
|
||||||
|
<t-tabs
|
||||||
|
v-if="settingStore.isUseTabsRouter"
|
||||||
|
theme="card"
|
||||||
|
:class="`${prefix}-layout-tabs-nav`"
|
||||||
|
:value="$route.path"
|
||||||
|
:style="{ position: 'sticky', top: 0, width: '100%' }"
|
||||||
|
@change="handleChangeCurrentTab"
|
||||||
|
@remove="handleRemove"
|
||||||
|
>
|
||||||
|
<t-tab-panel
|
||||||
|
v-for="(routeItem, index) in tabRouters"
|
||||||
|
:key="`${routeItem.path}_${index}`"
|
||||||
|
:value="routeItem.path"
|
||||||
|
:removable="!routeItem.isHome"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<t-dropdown
|
||||||
|
trigger="context-menu"
|
||||||
|
:min-column-width="128"
|
||||||
|
:popup-props="{ overlayClassName: 'route-tabs-dropdown' }"
|
||||||
|
>
|
||||||
|
<template v-if="!routeItem.isHome">
|
||||||
|
{{ routeItem.title }}
|
||||||
|
</template>
|
||||||
|
<t-icon v-else name="home" />
|
||||||
|
<template #dropdown>
|
||||||
|
<t-dropdown-menu v-if="$route.path === routeItem.path">
|
||||||
|
<t-dropdown-item @click="() => handleRefresh(routeItem, index)">
|
||||||
|
<t-icon name="refresh" />
|
||||||
|
刷新
|
||||||
|
</t-dropdown-item>
|
||||||
|
<t-dropdown-item v-if="index > 0" @click="() => handleCloseAhead(routeItem.path, index)">
|
||||||
|
<t-icon name="arrow-left" />
|
||||||
|
关闭左侧
|
||||||
|
</t-dropdown-item>
|
||||||
|
<t-dropdown-item
|
||||||
|
v-if="index < tabRouters.length - 1"
|
||||||
|
@click="() => handleCloseBehind(routeItem.path, index)"
|
||||||
|
>
|
||||||
|
<t-icon name="arrow-right" />
|
||||||
|
关闭右侧
|
||||||
|
</t-dropdown-item>
|
||||||
|
<t-dropdown-item @click="() => handleCloseOther(routeItem.path, index)">
|
||||||
|
<t-icon name="close-circle" />
|
||||||
|
关闭其它
|
||||||
|
</t-dropdown-item>
|
||||||
|
</t-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</t-dropdown>
|
||||||
|
</template>
|
||||||
|
</t-tab-panel>
|
||||||
|
</t-tabs>
|
||||||
|
<t-content :class="`${prefix}-content-layout`">
|
||||||
|
<l-breadcrumb v-if="settingStore.showBreadcrumb" />
|
||||||
|
<l-content />
|
||||||
|
</t-content>
|
||||||
|
<t-footer v-if="settingStore.showFooter" :class="`${prefix}-footer-layout`">
|
||||||
|
<l-footer />
|
||||||
|
</t-footer>
|
||||||
|
</t-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { nextTick, computed } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useSettingStore, useTabsRouterStore } from '@/store';
|
||||||
|
import { prefix } from '@/config/global';
|
||||||
|
import { TRouterInfo } from '@/types/interface';
|
||||||
|
|
||||||
|
import LContent from './Content.vue';
|
||||||
|
import LBreadcrumb from './Breadcrumb.vue';
|
||||||
|
import LFooter from './Footer.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const tabsRouterStore = useTabsRouterStore();
|
||||||
|
const tabRouters = computed(() => tabsRouterStore.tabRouters.filter((route) => route.isAlive || route.isHome));
|
||||||
|
|
||||||
|
const handleChangeCurrentTab = (path: string) => {
|
||||||
|
const { tabRouters } = tabsRouterStore;
|
||||||
|
const route = tabRouters.find((i) => i.path === path);
|
||||||
|
router.push({ path, query: route.query });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = ({ value: path, index }) => {
|
||||||
|
const { tabRouters } = tabsRouterStore;
|
||||||
|
const nextRouter = tabRouters[index + 1] || tabRouters[index - 1];
|
||||||
|
|
||||||
|
tabsRouterStore.subtractCurrentTabRouter({ path, routeIdx: index });
|
||||||
|
if (path === route.path) router.push({ path: nextRouter.path, query: nextRouter.query });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefresh = (route: TRouterInfo, routeIdx: number) => {
|
||||||
|
tabsRouterStore.toggleTabRouterAlive(routeIdx);
|
||||||
|
nextTick(() => {
|
||||||
|
tabsRouterStore.toggleTabRouterAlive(routeIdx);
|
||||||
|
router.replace({ path: route.path, query: route.query });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleCloseAhead = (path: string, routeIdx: number) => {
|
||||||
|
tabsRouterStore.subtractTabRouterAhead({ path, routeIdx });
|
||||||
|
};
|
||||||
|
const handleCloseBehind = (path: string, routeIdx: number) => {
|
||||||
|
tabsRouterStore.subtractTabRouterBehind({ path, routeIdx });
|
||||||
|
};
|
||||||
|
const handleCloseOther = (path: string, routeIdx: number) => {
|
||||||
|
tabsRouterStore.subtractTabRouterOther({ path, routeIdx });
|
||||||
|
};
|
||||||
|
</script>
|
34
src/layouts/components/LayoutHeader.vue
Normal file
34
src/layouts/components/LayoutHeader.vue
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<l-header
|
||||||
|
v-if="settingStore.showHeader"
|
||||||
|
:show-logo="settingStore.showHeaderLogo"
|
||||||
|
:theme="settingStore.displayMode"
|
||||||
|
:layout="settingStore.layout"
|
||||||
|
:is-fixed="settingStore.isHeaderFixed"
|
||||||
|
:menu="headerMenu"
|
||||||
|
:is-compact="settingStore.isSidebarCompact"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { usePermissionStore, useSettingStore } from '@/store';
|
||||||
|
import LHeader from './Header.vue';
|
||||||
|
|
||||||
|
const permissionStore = usePermissionStore();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const { routers: menuRouters } = storeToRefs(permissionStore);
|
||||||
|
const headerMenu = computed(() => {
|
||||||
|
if (settingStore.layout === 'mix') {
|
||||||
|
if (settingStore.splitMenu) {
|
||||||
|
return menuRouters.value.map((menu) => ({
|
||||||
|
...menu,
|
||||||
|
children: [],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return menuRouters.value;
|
||||||
|
});
|
||||||
|
</script>
|
37
src/layouts/components/LayoutSideNav.vue
Normal file
37
src/layouts/components/LayoutSideNav.vue
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<l-side-nav
|
||||||
|
v-if="settingStore.showSidebar"
|
||||||
|
:show-logo="settingStore.showSidebarLogo"
|
||||||
|
:layout="settingStore.layout"
|
||||||
|
:is-fixed="settingStore.isSidebarFixed"
|
||||||
|
:menu="sideMenu"
|
||||||
|
:theme="settingStore.mode"
|
||||||
|
:is-compact="settingStore.isSidebarCompact"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { usePermissionStore, useSettingStore } from '@/store';
|
||||||
|
import LSideNav from './SideNav.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const permissionStore = usePermissionStore();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const { routers: menuRouters } = storeToRefs(permissionStore);
|
||||||
|
|
||||||
|
const sideMenu = computed(() => {
|
||||||
|
const { layout, splitMenu } = settingStore;
|
||||||
|
let newMenuRouters = menuRouters.value;
|
||||||
|
if (layout === 'mix' && splitMenu) {
|
||||||
|
newMenuRouters.forEach((menu) => {
|
||||||
|
if (route.path.indexOf(menu.path) === 0) {
|
||||||
|
newMenuRouters = menu.children.map((subMenu) => ({ ...subMenu, path: `${menu.path}/${subMenu.path}` }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return newMenuRouters;
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,98 +0,0 @@
|
||||||
import { defineComponent, PropType, computed, h } from 'vue';
|
|
||||||
import { prefix } from '@/config/global';
|
|
||||||
import { MenuRoute } from '@/types/interface';
|
|
||||||
import { getActive } from '@/router';
|
|
||||||
|
|
||||||
const getMenuList = (list: MenuRoute[], basePath?: string): MenuRoute[] => {
|
|
||||||
if (!list) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
// 如果meta中有orderNo则按照从小到大排序
|
|
||||||
list.sort((a, b) => {
|
|
||||||
return (a.meta?.orderNo || 0) - (b.meta?.orderNo || 0);
|
|
||||||
});
|
|
||||||
return list
|
|
||||||
.map((item) => {
|
|
||||||
const path = basePath ? `${basePath}/${item.path}` : item.path;
|
|
||||||
return {
|
|
||||||
path,
|
|
||||||
title: item.meta?.title,
|
|
||||||
icon: item.meta?.icon || '',
|
|
||||||
children: getMenuList(item.children, path),
|
|
||||||
meta: item.meta,
|
|
||||||
redirect: item.redirect,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((item) => item.meta && item.meta.hidden !== true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderIcon = (item) => {
|
|
||||||
if (typeof item.icon === 'string') {
|
|
||||||
return () => item.icon && <t-icon name={item.icon}></t-icon>;
|
|
||||||
}
|
|
||||||
if (item.icon && typeof item.icon.render === 'function') {
|
|
||||||
return () =>
|
|
||||||
h(item.icon.render(), {
|
|
||||||
class: 't-icon',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return () => '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPath = (active, item) => {
|
|
||||||
if (active.startsWith(item.path)) {
|
|
||||||
return active;
|
|
||||||
}
|
|
||||||
return item.meta?.single ? item.redirect : item.path;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useRenderNav = (active: string, list: Array<MenuRoute>) => {
|
|
||||||
return list.map((item) => {
|
|
||||||
if (!item.children || !item.children.length || item.meta?.single) {
|
|
||||||
const href = item.path.match(/(http|https):\/\/([\w.]+\/?)\S*/);
|
|
||||||
if (href) {
|
|
||||||
return (
|
|
||||||
<t-menu-item href={href?.[0]} name={item.path} value={getPath(active, item)} icon={renderIcon(item)}>
|
|
||||||
{item.title}
|
|
||||||
</t-menu-item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<t-menu-item name={item.path} value={getPath(active, item)} to={item.path} icon={renderIcon(item)}>
|
|
||||||
{item.title}
|
|
||||||
</t-menu-item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<t-submenu name={item.path} value={item.path} title={item.title} icon={renderIcon(item)}>
|
|
||||||
{item.children && useRenderNav(active, item.children)}
|
|
||||||
</t-submenu>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
navData: {
|
|
||||||
type: Array as PropType<MenuRoute[]>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const active = computed(() => getActive());
|
|
||||||
const list = computed(() => {
|
|
||||||
const { navData } = props;
|
|
||||||
return getMenuList(navData);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
prefix,
|
|
||||||
active,
|
|
||||||
list,
|
|
||||||
useRenderNav,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
return <div>{this.useRenderNav(this.active, this.list)}</div>;
|
|
||||||
},
|
|
||||||
});
|
|
102
src/layouts/components/MenuContent.vue
Normal file
102
src/layouts/components/MenuContent.vue
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<template v-for="item in list" :key="item.path">
|
||||||
|
<template v-if="!item.children || !item.children.length || item.meta?.single">
|
||||||
|
<t-menu-item v-if="getHref(item)" :href="getHref(item)?.[0]" :name="item.path" :value="getPath(item)">
|
||||||
|
<template #icon>
|
||||||
|
<t-icon v-if="beIcon(item)" :name="item.icon" />
|
||||||
|
<component :is="beRender(item).render" v-else-if="beRender(item).can" class="t-icon" />
|
||||||
|
</template>
|
||||||
|
{{ item.title }}
|
||||||
|
</t-menu-item>
|
||||||
|
<t-menu-item v-else :name="item.path" :value="getPath(item)" :to="item.path">
|
||||||
|
<template #icon>
|
||||||
|
<t-icon v-if="beIcon(item)" :name="item.icon" />
|
||||||
|
<component :is="beRender(item).render" v-else-if="beRender(item).can" class="t-icon" />
|
||||||
|
</template>
|
||||||
|
{{ item.title }}
|
||||||
|
</t-menu-item>
|
||||||
|
</template>
|
||||||
|
<t-submenu v-else :name="item.path" :value="item.path" :title="item.title">
|
||||||
|
<template #icon>
|
||||||
|
<t-icon v-if="beIcon(item)" :name="item.icon" />
|
||||||
|
<component :is="beRender(item).render" v-else-if="beRender(item).can" class="t-icon" />
|
||||||
|
</template>
|
||||||
|
<menu-content v-if="item.children" :nav-data="item.children" />
|
||||||
|
</t-submenu>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, PropType } from 'vue';
|
||||||
|
import isObject from 'lodash/isObject';
|
||||||
|
import { MenuRoute } from '@/types/interface';
|
||||||
|
import { getActive } from '@/router';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
navData: {
|
||||||
|
type: Array as PropType<MenuRoute[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const active = computed(() => getActive());
|
||||||
|
const list = computed(() => {
|
||||||
|
const { navData } = props;
|
||||||
|
return getMenuList(navData);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getMenuList = (list: MenuRoute[], basePath?: string): MenuRoute[] => {
|
||||||
|
if (!list) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// 如果meta中有orderNo则按照从小到大排序
|
||||||
|
list.sort((a, b) => {
|
||||||
|
return (a.meta?.orderNo || 0) - (b.meta?.orderNo || 0);
|
||||||
|
});
|
||||||
|
return list
|
||||||
|
.map((item) => {
|
||||||
|
const path = basePath ? `${basePath}/${item.path}` : item.path;
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
title: item.meta?.title,
|
||||||
|
icon: item.meta?.icon || '',
|
||||||
|
children: getMenuList(item.children, path),
|
||||||
|
meta: item.meta,
|
||||||
|
redirect: item.redirect,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item) => item.meta && item.meta.hidden !== true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHref = (item: MenuRoute) => {
|
||||||
|
return item.path.match(/(http|https):\/\/([\w.]+\/?)\S*/);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPath = (item) => {
|
||||||
|
if (active.value.startsWith(item.path)) {
|
||||||
|
return active.value;
|
||||||
|
}
|
||||||
|
return item.meta?.single ? item.redirect : item.path;
|
||||||
|
};
|
||||||
|
|
||||||
|
const beIcon = (item: MenuRoute) => {
|
||||||
|
return item.icon && typeof item.icon === 'string';
|
||||||
|
};
|
||||||
|
|
||||||
|
const beRender = (item: MenuRoute) => {
|
||||||
|
if (isObject(item.icon) && typeof item.icon.render === 'function') {
|
||||||
|
return {
|
||||||
|
can: true,
|
||||||
|
render: item.icon.render,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
can: false,
|
||||||
|
render: null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
|
@ -1,169 +0,0 @@
|
||||||
import { defineComponent, PropType, computed, onMounted } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import union from 'lodash/union';
|
|
||||||
import { prefix } from '@/config/global';
|
|
||||||
import pgk from '../../../package.json';
|
|
||||||
import MenuContent from './MenuContent';
|
|
||||||
import AssetLogo from '@/assets/assets-t-logo.svg?component';
|
|
||||||
import AssetLogoFull from '@/assets/assets-logo-full.svg?component';
|
|
||||||
import { useSettingStore } from '@/store';
|
|
||||||
import { getActive, getRoutesExpanded } from '@/router';
|
|
||||||
|
|
||||||
const MIN_POINT = 992 - 1;
|
|
||||||
|
|
||||||
const useComputed = (props) => {
|
|
||||||
const collapsed = computed(() => useSettingStore().isSidebarCompact);
|
|
||||||
|
|
||||||
const active = computed(() => getActive());
|
|
||||||
|
|
||||||
const defaultExpanded = computed(() => {
|
|
||||||
const path = getActive();
|
|
||||||
const parentPath = path.substring(0, path.lastIndexOf('/'));
|
|
||||||
const expanded = getRoutesExpanded();
|
|
||||||
return union(expanded, parentPath === '' ? [] : [parentPath]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const sideNavCls = computed(() => {
|
|
||||||
const { isCompact } = props;
|
|
||||||
return [
|
|
||||||
`${prefix}-sidebar-layout`,
|
|
||||||
{
|
|
||||||
[`${prefix}-sidebar-compact`]: isCompact,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
const menuCls = computed(() => {
|
|
||||||
const { showLogo, isFixed, layout } = props;
|
|
||||||
return [
|
|
||||||
`${prefix}-side-nav`,
|
|
||||||
{
|
|
||||||
[`${prefix}-side-nav-no-logo`]: !showLogo,
|
|
||||||
[`${prefix}-side-nav-no-fixed`]: !isFixed,
|
|
||||||
[`${prefix}-side-nav-mix-fixed`]: layout === 'mix' && isFixed,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
const layoutCls = computed(() => {
|
|
||||||
const { layout } = props;
|
|
||||||
return [`${prefix}-side-nav-${layout}`, `${prefix}-sidebar-layout`];
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
active,
|
|
||||||
defaultExpanded,
|
|
||||||
collapsed,
|
|
||||||
sideNavCls,
|
|
||||||
menuCls,
|
|
||||||
layoutCls,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'SideNav',
|
|
||||||
components: {
|
|
||||||
AssetLogoFull,
|
|
||||||
AssetLogo,
|
|
||||||
MenuContent,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
menu: {
|
|
||||||
type: Array as PropType<string[]>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
showLogo: {
|
|
||||||
type: Boolean as PropType<boolean>,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
isFixed: {
|
|
||||||
type: Boolean as PropType<boolean>,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
layout: {
|
|
||||||
type: String as PropType<string>,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
headerHeight: {
|
|
||||||
type: String as PropType<string>,
|
|
||||||
default: '64px',
|
|
||||||
},
|
|
||||||
theme: {
|
|
||||||
type: String as PropType<string>,
|
|
||||||
default: 'light',
|
|
||||||
},
|
|
||||||
isCompact: {
|
|
||||||
type: Boolean as PropType<boolean>,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const router = useRouter();
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
|
|
||||||
const changeCollapsed = () => {
|
|
||||||
settingStore.updateConfig({
|
|
||||||
isSidebarCompact: !settingStore.isSidebarCompact,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const autoCollapsed = () => {
|
|
||||||
const isCompact = window.innerWidth <= MIN_POINT;
|
|
||||||
settingStore.updateConfig({
|
|
||||||
isSidebarCompact: isCompact,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
autoCollapsed();
|
|
||||||
window.onresize = () => {
|
|
||||||
autoCollapsed();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const goHome = () => {
|
|
||||||
router.push('/dashboard/base');
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
prefix,
|
|
||||||
...useComputed(props),
|
|
||||||
autoCollapsed,
|
|
||||||
changeCollapsed,
|
|
||||||
goHome,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div class={this.sideNavCls}>
|
|
||||||
<t-menu
|
|
||||||
class={this.menuCls}
|
|
||||||
theme={this.theme}
|
|
||||||
value={this.active}
|
|
||||||
default-expanded={this.defaultExpanded}
|
|
||||||
collapsed={this.collapsed}
|
|
||||||
v-slots={{
|
|
||||||
logo: () =>
|
|
||||||
this.showLogo && (
|
|
||||||
<span class={`${prefix}-side-nav-logo-wrapper`} onClick={this.goHome}>
|
|
||||||
{this.collapsed ? (
|
|
||||||
<asset-logo class={`${prefix}-side-nav-logo-t-logo`} />
|
|
||||||
) : (
|
|
||||||
<asset-logo-full class={`${prefix}-side-nav-logo-tdesign-logo`} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
operations: () => (
|
|
||||||
<span class="version-container">
|
|
||||||
{!this.collapsed && 'TDesign Starter'} {pgk.version}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<menu-content navData={this.menu} />
|
|
||||||
</t-menu>
|
|
||||||
<div class={`${prefix}-side-nav-placeholder${this.collapsed ? '-hidden' : ''}`}></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
126
src/layouts/components/SideNav.vue
Normal file
126
src/layouts/components/SideNav.vue
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
<template>
|
||||||
|
<div :class="sideNavCls">
|
||||||
|
<t-menu :class="menuCls" :theme="theme" :value="active" :collapsed="collapsed" :default-expanded="defaultExpanded">
|
||||||
|
<template #logo>
|
||||||
|
<span v-if="showLogo" :class="`${prefix}-side-nav-logo-wrapper`" @click="goHome">
|
||||||
|
<component :is="getLogo()" :class="`${prefix}-side-nav-logo-${collapsed ? 't' : 'tdesign'}-logo`" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<menu-content :nav-data="menu" />
|
||||||
|
<template #operations>
|
||||||
|
<span class="version-container"> {{ !collapsed && 'TDesign Starter' }} {{ pgk.version }} </span>
|
||||||
|
</template>
|
||||||
|
</t-menu>
|
||||||
|
<div :class="`${prefix}-side-nav-placeholder${collapsed ? '-hidden' : ''}`"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, PropType } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import union from 'lodash/union';
|
||||||
|
|
||||||
|
import { useSettingStore } from '@/store';
|
||||||
|
import { prefix } from '@/config/global';
|
||||||
|
import pgk from '../../../package.json';
|
||||||
|
import { MenuRoute } from '@/types/interface';
|
||||||
|
import { getActive, getRoutesExpanded } from '@/router';
|
||||||
|
|
||||||
|
import AssetLogo from '@/assets/assets-t-logo.svg?component';
|
||||||
|
import AssetLogoFull from '@/assets/assets-logo-full.svg?component';
|
||||||
|
import MenuContent from './MenuContent.vue';
|
||||||
|
|
||||||
|
const MIN_POINT = 992 - 1;
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
menu: {
|
||||||
|
type: Array as PropType<MenuRoute[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
showLogo: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
isFixed: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
headerHeight: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: '64px',
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: 'light',
|
||||||
|
},
|
||||||
|
isCompact: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const collapsed = computed(() => useSettingStore().isSidebarCompact);
|
||||||
|
|
||||||
|
const active = computed(() => getActive());
|
||||||
|
|
||||||
|
const defaultExpanded = computed(() => {
|
||||||
|
const path = getActive();
|
||||||
|
const parentPath = path.substring(0, path.lastIndexOf('/'));
|
||||||
|
const expanded = getRoutesExpanded();
|
||||||
|
return union(expanded, parentPath === '' ? [] : [parentPath]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sideNavCls = computed(() => {
|
||||||
|
const { isCompact } = props;
|
||||||
|
return [
|
||||||
|
`${prefix}-sidebar-layout`,
|
||||||
|
{
|
||||||
|
[`${prefix}-sidebar-compact`]: isCompact,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const menuCls = computed(() => {
|
||||||
|
const { showLogo, isFixed, layout } = props;
|
||||||
|
return [
|
||||||
|
`${prefix}-side-nav`,
|
||||||
|
{
|
||||||
|
[`${prefix}-side-nav-no-logo`]: !showLogo,
|
||||||
|
[`${prefix}-side-nav-no-fixed`]: !isFixed,
|
||||||
|
[`${prefix}-side-nav-mix-fixed`]: layout === 'mix' && isFixed,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
|
||||||
|
const autoCollapsed = () => {
|
||||||
|
const isCompact = window.innerWidth <= MIN_POINT;
|
||||||
|
settingStore.updateConfig({
|
||||||
|
isSidebarCompact: isCompact,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
autoCollapsed();
|
||||||
|
window.onresize = () => {
|
||||||
|
autoCollapsed();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
router.push('/dashboard/base');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLogo = () => {
|
||||||
|
if (collapsed.value) return AssetLogo;
|
||||||
|
return AssetLogoFull;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
|
@ -1,273 +0,0 @@
|
||||||
import { defineComponent, computed, nextTick, onMounted, watch, onBeforeUnmount } from 'vue';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
import { usePermissionStore, useSettingStore, useTabsRouterStore } from '@/store';
|
|
||||||
|
|
||||||
import LayoutHeader from './components/Header.vue';
|
|
||||||
import LayoutBreadcrumb from './components/Breadcrumb.vue';
|
|
||||||
import LayoutFooter from './components/Footer.vue';
|
|
||||||
import LayoutSideNav from './components/SideNav';
|
|
||||||
import LayoutContent from './components/Content.vue';
|
|
||||||
import Setting from './setting.vue';
|
|
||||||
|
|
||||||
import { prefix } from '@/config/global';
|
|
||||||
import { TRouterInfo } from '@/types/interface';
|
|
||||||
|
|
||||||
import '@/style/layout.less';
|
|
||||||
|
|
||||||
const name = `${prefix}-base-layout`;
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name,
|
|
||||||
setup() {
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const permissionStore = usePermissionStore();
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
const tabsRouterStore = useTabsRouterStore();
|
|
||||||
const { routers: menuRouters } = storeToRefs(permissionStore);
|
|
||||||
const setting = storeToRefs(settingStore);
|
|
||||||
|
|
||||||
const mainLayoutCls = computed(() => [
|
|
||||||
{
|
|
||||||
't-layout--with-sider': settingStore.showSidebar,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const headerMenu = computed(() => {
|
|
||||||
if (settingStore.layout === 'mix') {
|
|
||||||
if (settingStore.splitMenu) {
|
|
||||||
return menuRouters.value.map((menu) => ({
|
|
||||||
...menu,
|
|
||||||
children: [],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return menuRouters.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const sideMenu = computed(() => {
|
|
||||||
const { layout, splitMenu } = settingStore;
|
|
||||||
let newMenuRouters = menuRouters.value;
|
|
||||||
if (layout === 'mix' && splitMenu) {
|
|
||||||
newMenuRouters.forEach((menu) => {
|
|
||||||
if (route.path.indexOf(menu.path) === 0) {
|
|
||||||
newMenuRouters = menu.children.map((subMenu) => ({ ...subMenu, path: `${menu.path}/${subMenu.path}` }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return newMenuRouters;
|
|
||||||
});
|
|
||||||
|
|
||||||
const appendNewRoute = () => {
|
|
||||||
const {
|
|
||||||
path,
|
|
||||||
query,
|
|
||||||
meta: { title },
|
|
||||||
name,
|
|
||||||
} = route;
|
|
||||||
tabsRouterStore.appendTabRouterList({ path, query, title: title as string, name, isAlive: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTabRouterListCache = () => {
|
|
||||||
tabsRouterStore.initTabRouterList(JSON.parse(localStorage.getItem('tabRouterList')));
|
|
||||||
};
|
|
||||||
const setTabRouterListCache = () => {
|
|
||||||
const { tabRouters } = tabsRouterStore;
|
|
||||||
localStorage.setItem('tabRouterList', JSON.stringify(tabRouters));
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
appendNewRoute();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 如果不需要持久化标签页可以注释掉以下的 onMounted 和 onBeforeUnmount 的内容
|
|
||||||
onMounted(() => {
|
|
||||||
if (localStorage.getItem('tabRouterList')) getTabRouterListCache();
|
|
||||||
window.addEventListener('beforeunload', setTabRouterListCache);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
window.removeEventListener('beforeunload', setTabRouterListCache);
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.path,
|
|
||||||
() => {
|
|
||||||
appendNewRoute();
|
|
||||||
document.querySelector(`.${prefix}-layout`).scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRemove = ({ value: path, index }) => {
|
|
||||||
const { tabRouters } = tabsRouterStore;
|
|
||||||
const nextRouter = tabRouters[index + 1] || tabRouters[index - 1];
|
|
||||||
|
|
||||||
tabsRouterStore.subtractCurrentTabRouter({ path, routeIdx: index });
|
|
||||||
if (path === route.path) {
|
|
||||||
router.push({ path: nextRouter.path, query: nextRouter.query });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleChangeCurrentTab = (path: string) => {
|
|
||||||
const { tabRouters } = tabsRouterStore;
|
|
||||||
const route = tabRouters.find((i) => i.path === path);
|
|
||||||
router.push({ path, query: route.query });
|
|
||||||
};
|
|
||||||
const handleRefresh = (route: TRouterInfo) => {
|
|
||||||
tabsRouterStore.toggleTabRouterAlive(route.routeIdx);
|
|
||||||
nextTick(() => {
|
|
||||||
tabsRouterStore.toggleTabRouterAlive(route.routeIdx);
|
|
||||||
router.replace({ path: route.path, query: route.query });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const handleCloseAhead = (path: string, routeIdx: number) => {
|
|
||||||
tabsRouterStore.subtractTabRouterAhead({ path, routeIdx });
|
|
||||||
};
|
|
||||||
const handleCloseBehind = (path: string, routeIdx: number) => {
|
|
||||||
tabsRouterStore.subtractTabRouterBehind({ path, routeIdx });
|
|
||||||
};
|
|
||||||
const handleCloseOther = (path: string, routeIdx: number) => {
|
|
||||||
tabsRouterStore.subtractTabRouterOther({ path, routeIdx });
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderSidebar = () => {
|
|
||||||
return (
|
|
||||||
settingStore.showSidebar && (
|
|
||||||
<LayoutSideNav
|
|
||||||
showLogo={settingStore.showSidebarLogo}
|
|
||||||
layout={settingStore.layout}
|
|
||||||
isFixed={settingStore.isSidebarFixed}
|
|
||||||
menu={sideMenu.value}
|
|
||||||
theme={settingStore.displayMode}
|
|
||||||
isCompact={settingStore.isSidebarCompact}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderHeader = () => {
|
|
||||||
return (
|
|
||||||
settingStore.showHeader && (
|
|
||||||
<LayoutHeader
|
|
||||||
showLogo={settingStore.showHeaderLogo}
|
|
||||||
theme={settingStore.displayMode}
|
|
||||||
layout={settingStore.layout}
|
|
||||||
isFixed={settingStore.isHeaderFixed}
|
|
||||||
menu={headerMenu.value}
|
|
||||||
isCompact={settingStore.isSidebarCompact}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFooter = () => {
|
|
||||||
return (
|
|
||||||
<t-footer class={`${prefix}-footer-layout`}>
|
|
||||||
<LayoutFooter />
|
|
||||||
</t-footer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderContent = () => {
|
|
||||||
const { showBreadcrumb, showFooter, isUseTabsRouter } = settingStore;
|
|
||||||
const tabRouters = tabsRouterStore.tabRouters.filter((route) => route.isAlive || route.isHome);
|
|
||||||
return (
|
|
||||||
// <t-layout class={[`${prefix}-layout`]} key={route.name}> 如果存在多个滚动列表之间切换时,页面不刷新导致的样式问题 请设置key 但会导致多标签tab页的缓存失效
|
|
||||||
<t-layout class={[`${prefix}-layout`]}>
|
|
||||||
{isUseTabsRouter && (
|
|
||||||
<t-tabs
|
|
||||||
theme="card"
|
|
||||||
class={`${prefix}-layout-tabs-nav`}
|
|
||||||
value={route.path}
|
|
||||||
onChange={handleChangeCurrentTab}
|
|
||||||
style={{ width: '100%', position: 'sticky', top: 0 }}
|
|
||||||
onRemove={handleRemove}
|
|
||||||
>
|
|
||||||
{tabRouters.map((router: TRouterInfo, idx: number) => (
|
|
||||||
<t-tab-panel
|
|
||||||
value={router.path}
|
|
||||||
key={`${router.path}_${idx}`}
|
|
||||||
v-slots={{
|
|
||||||
label: () => (
|
|
||||||
<t-dropdown
|
|
||||||
trigger="context-menu"
|
|
||||||
minColumnWidth={128}
|
|
||||||
popupProps={{ overlayClassName: 'router-tabs-dropdown' }}
|
|
||||||
v-slots={{
|
|
||||||
dropdown: () =>
|
|
||||||
router.path === route.path ? (
|
|
||||||
<t-dropdown-menu>
|
|
||||||
<t-dropdown-item onClick={() => handleRefresh(router)}>
|
|
||||||
<t-icon name="refresh" />
|
|
||||||
刷新
|
|
||||||
</t-dropdown-item>
|
|
||||||
{idx > 1 && (
|
|
||||||
<t-dropdown-item onClick={() => handleCloseAhead(router.path, idx)}>
|
|
||||||
<t-icon name="arrow-left" />
|
|
||||||
关闭左侧
|
|
||||||
</t-dropdown-item>
|
|
||||||
)}
|
|
||||||
{idx < tabRouters.length - 1 && (
|
|
||||||
<t-dropdown-item onClick={() => handleCloseBehind(router.path, idx)}>
|
|
||||||
<t-icon name="arrow-right" />
|
|
||||||
关闭右侧
|
|
||||||
</t-dropdown-item>
|
|
||||||
)}
|
|
||||||
<t-dropdown-item onClick={() => handleCloseOther(router.path, idx)}>
|
|
||||||
<t-icon name="close-circle" />
|
|
||||||
关闭其它
|
|
||||||
</t-dropdown-item>
|
|
||||||
</t-dropdown-menu>
|
|
||||||
) : null,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!router.isHome ? router.title : <t-icon name="home" />}
|
|
||||||
</t-dropdown>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
removable={!router.isHome}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</t-tabs>
|
|
||||||
)}
|
|
||||||
<t-content class={`${prefix}-content-layout`}>
|
|
||||||
{showBreadcrumb && <LayoutBreadcrumb />}
|
|
||||||
<LayoutContent />
|
|
||||||
</t-content>
|
|
||||||
{showFooter && renderFooter()}
|
|
||||||
</t-layout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
setting,
|
|
||||||
mainLayoutCls,
|
|
||||||
renderSidebar,
|
|
||||||
renderHeader,
|
|
||||||
renderContent,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
const { layout } = this.setting;
|
|
||||||
const header = this.renderHeader();
|
|
||||||
const sidebar = this.renderSidebar();
|
|
||||||
const content = this.renderContent();
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{layout === 'side' ? (
|
|
||||||
<t-layout class={this.mainLayoutCls} key="side">
|
|
||||||
<t-aside>{sidebar}</t-aside>
|
|
||||||
<t-layout>{[header, content]}</t-layout>
|
|
||||||
</t-layout>
|
|
||||||
) : (
|
|
||||||
<t-layout key="no-side">
|
|
||||||
{header}
|
|
||||||
<t-layout class={this.mainLayoutCls}>{[sidebar, content]}</t-layout>
|
|
||||||
</t-layout>
|
|
||||||
)}
|
|
||||||
<Setting />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
93
src/layouts/index.vue
Normal file
93
src/layouts/index.vue
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<template v-if="setting.layout.value === 'side'">
|
||||||
|
<t-layout key="side" :class="mainLayoutCls">
|
||||||
|
<t-aside><layout-side-nav /></t-aside>
|
||||||
|
<t-layout>
|
||||||
|
<t-header><layout-header /></t-header>
|
||||||
|
<t-content><layout-content /></t-content>
|
||||||
|
</t-layout>
|
||||||
|
</t-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<t-layout key="no-side">
|
||||||
|
<t-header><layout-header /> </t-header>
|
||||||
|
<t-layout :class="mainLayoutCls">
|
||||||
|
<layout-side-nav />
|
||||||
|
<layout-content />
|
||||||
|
</t-layout>
|
||||||
|
</t-layout>
|
||||||
|
</template>
|
||||||
|
<setting-com />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, watch, onBeforeUnmount } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useSettingStore, useTabsRouterStore } from '@/store';
|
||||||
|
|
||||||
|
import SettingCom from './setting.vue';
|
||||||
|
import LayoutHeader from './components/LayoutHeader.vue';
|
||||||
|
import LayoutContent from './components/LayoutContent.vue';
|
||||||
|
import LayoutSideNav from './components/LayoutSideNav.vue';
|
||||||
|
|
||||||
|
import { prefix } from '@/config/global';
|
||||||
|
|
||||||
|
import '@/style/layout.less';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const tabsRouterStore = useTabsRouterStore();
|
||||||
|
const setting = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const mainLayoutCls = computed(() => [
|
||||||
|
{
|
||||||
|
't-layout--with-sider': settingStore.showSidebar,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appendNewRoute = () => {
|
||||||
|
const {
|
||||||
|
path,
|
||||||
|
query,
|
||||||
|
meta: { title },
|
||||||
|
name,
|
||||||
|
} = route;
|
||||||
|
tabsRouterStore.appendTabRouterList({ path, query, title: title as string, name, isAlive: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTabRouterListCache = () => {
|
||||||
|
tabsRouterStore.initTabRouterList(JSON.parse(localStorage.getItem('tabRouterList')));
|
||||||
|
};
|
||||||
|
const setTabRouterListCache = () => {
|
||||||
|
const { tabRouters } = tabsRouterStore;
|
||||||
|
localStorage.setItem('tabRouterList', JSON.stringify(tabRouters));
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
appendNewRoute();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果不需要持久化标签页可以注释掉以下的 onMounted 和 onBeforeUnmount 的内容
|
||||||
|
onMounted(() => {
|
||||||
|
if (localStorage.getItem('tabRouterList')) getTabRouterListCache();
|
||||||
|
window.addEventListener('beforeunload', setTabRouterListCache);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('beforeunload', setTabRouterListCache);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
() => {
|
||||||
|
appendNewRoute();
|
||||||
|
document.querySelector(`.${prefix}-layout`).scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
|
@ -1,4 +1,4 @@
|
||||||
import Layout from '@/layouts';
|
import Layout from '@/layouts/index.vue';
|
||||||
import DashboardIcon from '@/assets/assets-slide-dashboard.svg';
|
import DashboardIcon from '@/assets/assets-slide-dashboard.svg';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Layout from '@/layouts';
|
import Layout from '@/layouts/index.vue';
|
||||||
import ListIcon from '@/assets/assets-slide-list.svg';
|
import ListIcon from '@/assets/assets-slide-list.svg';
|
||||||
import FormIcon from '@/assets/assets-slide-form.svg';
|
import FormIcon from '@/assets/assets-slide-form.svg';
|
||||||
import DetailIcon from '@/assets/assets-slide-detail.svg';
|
import DetailIcon from '@/assets/assets-slide-detail.svg';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Layout from '@/layouts';
|
import Layout from '@/layouts/index.vue';
|
||||||
import LogoutIcon from '@/assets/assets-slide-logout.svg';
|
import LogoutIcon from '@/assets/assets-slide-logout.svg';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
|
@ -20,6 +20,7 @@ export default [
|
||||||
{
|
{
|
||||||
path: '/loginRedirect',
|
path: '/loginRedirect',
|
||||||
name: 'loginRedirect',
|
name: 'loginRedirect',
|
||||||
|
redirect: '/login',
|
||||||
meta: { title: '登录页', icon: LogoutIcon },
|
meta: { title: '登录页', icon: LogoutIcon },
|
||||||
component: () => import('@/layouts/blank.vue'),
|
component: () => import('@/layouts/blank.vue'),
|
||||||
children: [
|
children: [
|
||||||
|
|
Loading…
Reference in New Issue
Block a user