mirror of
https://github.com/Tencent/tdesign-vue-next-starter.git
synced 2024-12-23 09:06:48 +08:00
Merge branch 'develop' of github.com:Tencent/tdesign-vue-next-starter into main
This commit is contained in:
commit
caea951c56
|
@ -74,7 +74,7 @@ import { MenuRoute } from '@/types/interface';
|
|||
|
||||
import Notice from './Notice.vue';
|
||||
import Search from './Search.vue';
|
||||
import MenuContent from './MenuContent';
|
||||
import MenuContent from './MenuContent.vue';
|
||||
|
||||
const props = defineProps({
|
||||
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, 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 });
|
||||
};
|
||||
|
||||
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, idx)}>
|
||||
<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 />
|
||||
<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';
|
||||
|
||||
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 FormIcon from '@/assets/assets-slide-form.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';
|
||||
|
||||
export default [
|
||||
|
|
Loading…
Reference in New Issue
Block a user